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
42 changes: 28 additions & 14 deletions .changeset/7014-select-option-spec-claim-correction.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,41 @@ real boundary (objectui#7014).
published JSDoc — the text that reaches your editor tooltips through `.d.ts` — which was
asserting the opposite of what the spec does.

⚠️ **The boundary this entry describes has since MOVED, and this paragraph is the
correction.** `@objectstack/spec` 17.3.0 declared both keys — the maintainer's 2026-08-25
Option-A ruling on objectui#6140 / objectui#6153 — and this repo's pin has moved past it.
Everything below was measured correctly against 17.2.0 and is kept as the dated record of
why the comments were rewritten; ⛔ do not read it as the contract you are authoring
against today. `rows` and `options[].description` are **authorable now**, and the doc
comments were corrected again in objectui#7635, which releases alongside this entry.

Three doc comments claimed the installed `@objectstack/spec` DECLARES a key that it in
fact **refuses by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
fact **refused by name**. Measured on `@objectstack/spec@17.2.0`, each paired with a
control that accepts the same payload minus the key:

- `SelectOptionMetadata.description` said it "Aligns `@objectstack/spec`
`SelectOptionSchema.description`". That schema is `.strict()` over exactly
`{label, value, color, default, visibleWhen}`; `description` fails with
`unrecognized_keys`.
`SelectOptionSchema.description`". At 17.2.0 that schema was `.strict()` over exactly
`{label, value, color, default, visibleWhen}` and `description` failed with
`unrecognized_keys`. (17.3.0 added it — the comment was right about the destination and
wrong about the date.)
- `MarkdownFieldMetadata.rows` and `HtmlFieldMetadata.rows` said `@objectstack/spec`
`FieldSchema.rows` declares the key "authorable on exactly the multiline editor
types". `FieldSchema` refuses `rows` by name on all four of
textarea/markdown/html/richtext.
types". At 17.2.0 `FieldSchema` refused `rows` by name on all four of
textarea/markdown/html/richtext. (17.3.0 declares it for exactly those four, so this
claim too was early rather than wrong.)

The keys themselves stay declared and stay consumed — `LookupField` searches an option's
`description` (objectui#6153) and `RichTextField` reads `rows` (objectui#6140). Only the
attribution was wrong, and it mattered in a specific way: `FieldSchema` routes a select
field's `options` through the strict option schema, so authoring `description` on an
option makes `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write. They now say these are
objectui-side read-model extensions that must never reach authored object metadata.
attribution was wrong, and at 17.2.0 it mattered in a specific way: `FieldSchema` routed a
select field's `options` through the strict option schema, so authoring `description` on
an option made `PUT /api/v1/meta/object/:name` fail the **whole field** with a 422
`INVALID_METADATA`. The comments were inviting exactly that write, so they were rewritten
to call these objectui-side read-model extensions. ⚠️ That rewrite is what 17.3.0
falsified: the write now SUCCEEDS on both keys, and the comments no longer say otherwise.

A new pin (`select-option-spec-extension-7014.test.ts`) asserts the spec's option key set
and each by-name refusal, so if the spec ever adopts one of these names the claim is
re-opened loudly instead of silently becoming true.
A pin (`select-option-spec-extension-7014.test.ts`) asserts the option key set and each
by-name refusal, so that an adoption re-opens the claim loudly instead of silently making
it true. ⭐ That is exactly what happened — the pin was re-pointed to the 17.3.0 boundary
when the spec adopted both names, and it is why this entry could be corrected before it
shipped rather than after. The keys still outside the option vocabulary, and still the
live half of that pin, are `icon` and `disabled`.
18 changes: 18 additions & 0 deletions .changeset/7635-spec-173-comment-repairs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
'@object-ui/types': patch
'@object-ui/fields': patch
---

docs(types,fields): the `rows` / `options[].description` docblocks stop asserting a refusal the contract no longer performs

Five shipped doc comments told a reader that `@objectstack/spec` REFUSES two keys BY NAME. It declares both. The sentences were measured correctly against 17.2.0 and outlived the contract they described — `@objectstack/spec` 17.3.0 implemented the maintainer's 2026-08-25 Option-A ruling on objectui#6140 / objectui#6153 and declared them, and this repo's pin has since moved past it. Docblocks in `packages/types/src` ship in `dist/*.d.ts`, so the false prose was reaching consumers.

Corrected, each against the installed artifact rather than against the changelog:

- **`rows`** — `FieldSchema` accepts it on `textarea` / `markdown` / `html` / `richtext` as an integer of at least 1. It is TYPE-GATED: on a field type with no rows-sized editor surface the refusal arrives as a cross-field refinement naming the key, deliberately not `unrecognized_keys`, so "declared" must not be read as "declared everywhere". `MarkdownFieldMetadata.rows`, `HtmlFieldMetadata.rows` and the `RichtextFieldMetadata` cross-reference now say so.
- **`options[].description`** — `SelectOptionSchema` accepts it as a string, and a field whose `options` carry it parses whole, so it may now be authored. `SelectOptionMetadata` and its interface docblock now put the still-refused keys where the emphasis belongs: `icon` and `disabled`, which are what keeps "the schema still refuses something" a live fact.
- **`select-option.ts`** no longer enumerates the spec's option keys. The enumeration is what went stale — the keys arrive through the `Omit` by reference, so a list written above the derivation can only ever disagree with it — and it is deliberately not replaced with a longer list.

No runtime behaviour, type surface or export changes; every assertion in the repaired files passes unchanged before and after, which is the point — nothing mechanical was watching these sentences.

Two of the five sites are now watched. They are written as single-line claims about the installed pin, which moves them out of `scripts/check-installed-spec-pin-claims.mjs`'s ledger of known-stale debt and into the population that gate re-derives at every bump; their ledger entries are deleted in the same change, as that gate's both-direction ratchet requires. The remaining three sit on facts no instrument reads, and say so rather than reading as live.
14 changes: 11 additions & 3 deletions packages/fields/src/widgets/LookupField.optionDescription.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
* `opt.description` alongside the label, and `recordToOption` emits the same
* key for fetched records. What the card changed is the CONTRACT:
* `SelectOptionMetadata` (the declared type of `LookupFieldMetadata.options`)
* now declares `description?: string` as an objectui-side extension - the
* installed `@objectstack/spec` 17.2.0 has no such key and REFUSES it BY NAME
* on `SelectOptionSchema` (objectui#7014) - so the fixture below is an ANNOTATED
* now declares `description?: string` - so the fixture below is an ANNOTATED
* literal — the excess-property check that used to refuse this exact document
* is the compile half of the pin, and the search behaviour is the runtime
* half. Behaviour unchanged by design; the test would have passed before the
* declaration only with the literal laundered through a cast.
*
* ⚠️ This header called the key an objectui-side extension the spec REFUSED BY
* NAME, measured on 17.2.0 (objectui#7014). `@objectstack/spec` 17.3.0 declared
* it on `SelectOptionSchema` and the sentence became false in the other
* direction; objectui#7635 corrected it. The key is now authorable on both
* faces, which changes NOTHING about this file — the search behaviour under
* test never depended on the authoring door, and that independence is exactly
* why the boundary could move without reddening a single assertion here. The
* contract half is re-derived by
* `packages/types/src/__tests__/select-option-spec-extension-7014.test.ts`.
*/

import * as React from 'react';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,14 @@
* pinned as refused — that door has to be seen to stay shut.
*
* ⚠️ The prose in `packages/types/src/field-types.ts` and
* `packages/types/src/select-option.ts` still describes the 17.2.0 boundary and
* is now false in the other direction. Correcting it is objectui#7635's
* declared surface (the comment/prose sites), not this file's — recorded here
* so the two halves are not repaired twice or, worse, once.
* `packages/types/src/select-option.ts` described the 17.2.0 boundary and was
* false in the other direction for as long as this note stood. objectui#7635
* corrected those sites, so both halves now read the same boundary — this file
* asserts it, and their docblocks point here instead of restating it. ⭐ The
* repair also moved two of them INTO the reach of
* `scripts/check-installed-spec-pin-claims.mjs`: written as a claim about the
* installed pin on one line, they are now re-derived by that gate at every
* bump, rather than sitting in its ledger as known-stale debt.
*
* Every assertion pairs its verdict with a CONTROL that changes exactly one
* thing, so a red here reads "the key's status changed" and never "the fixture
Expand Down
82 changes: 53 additions & 29 deletions packages/types/src/field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,13 +258,26 @@ export interface MarkdownFieldMetadata extends BaseFieldMetadata {
* no rich-content type declared it, so the running widget honoured a key an
* annotated literal rejected. Follows the `TextareaFieldMetadata` precedent.
*
* WARNING - NOT a spec key. Measured on the installed `@objectstack/spec`
* 17.2.0: `FieldSchema` REFUSES `rows` BY NAME (`unrecognized_keys`) on all
* four of textarea/markdown/html/richtext, with the same payload minus
* `rows` accepted as the control. It is an objectui render hint and must not
* be written into authored object metadata (objectui#7014).
* A DECLARED spec key as of `@objectstack/spec` 17.3.0, which implements that
* same ruling. Measured on the installed `@objectstack/spec` 17.4.0:
* `FieldSchema` ACCEPTS `rows` on all four of textarea/markdown/html/
* richtext, as an integer of at least 1 — a non-integer answers
* `invalid_type` and 0 answers `too_small`, so "declared" does not mean "any
* number". ⚠️ It is TYPE-GATED, not universal: on a field type that renders
* no rows-sized editor surface the refusal arrives as a cross-field
* REFINEMENT naming the key — deliberately NOT `unrecognized_keys` — so a
* `text` field carrying `rows` still fails the whole field. Authored object
* metadata may carry it on these four types and nowhere else.
*
* ⛔ This reverses what this docblock asserted between objectui#7014 and
* objectui#7635: at 17.2.0 the key WAS refused by name, and the sentence
* saying so outlived the contract it described. Both directions, each behind
* an accepting control, are re-derived by
* `__tests__/select-option-spec-extension-7014.test.ts` — read that file's
* verdict rather than trusting this paragraph.
* The four inert editor keys the same ruling measured (`toolbar`/`preview`/
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them.
* `minHeight`/`maxHeight`) stay deliberately undeclared — nothing reads them,
* and the same file holds that door shut.
*/
rows?: number;
}
Expand All @@ -279,8 +292,9 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* Height of the INLINE editor, in text rows. Same declaration as
* `MarkdownFieldMetadata.rows` (objectui#6140 Option A ruling — see the
* docblock there): `RichTextField` reads it for all three registry keys it
* serves. WARNING - NOT a spec key either; see the measured refusal in the
* docblock there (objectui#7014).
* serves. It is a DECLARED spec key on this type too, type-gated to the four
* multiline editor types; see the measured reading in the docblock there
* (objectui#7014, corrected for the 17.3.0 boundary by objectui#7635).
*/
rows?: number;
}
Expand Down Expand Up @@ -363,10 +377,11 @@ export interface HtmlFieldMetadata extends BaseFieldMetadata {
* that enforces it stayed live — a fresh instance of the asymmetry this member
* exists to end.
*
* ⚠️ The `rows` docblocks on the two siblings still describe the
* `@objectstack/spec` 17.2.0 boundary, where `rows` was refused by name. That
* prose is objectui#7635's declared surface, not this member's; the 17.3.0
* reading above is stated for `richtext` only and is not a correction of it.
* The `rows` docblocks on the two siblings described the `@objectstack/spec`
* 17.2.0 boundary, where `rows` was refused by name, and outlived it;
* objectui#7635 corrected them in the same change that corrected this
* paragraph, so all three now read the same boundary and none of them is the
* odd one out.
*/
export interface RichtextFieldMetadata extends BaseFieldMetadata {
type: 'richtext';
Expand Down Expand Up @@ -479,17 +494,18 @@ import type { SelectOptionBase } from './select-option.js';
/**
* Select option — the OBJECT-METADATA face of the one select-option contract
* (objectui#7014). It extends {@link SelectOptionBase}, which derives the spec
* keys (`label`, `value`, `color`, `default`) from `@objectstack/spec/data` by
* reference and carries objectui's `visibleWhen` wire shape plus the two
* objectui-only keys `disabled` and `icon`. This face restates none of them; it
* adds exactly the one key below and keeps the spec's `value` — a lowercase
* machine identifier — as declared.
* keys from `@objectstack/spec/data` by reference and carries objectui's
* `visibleWhen` wire shape plus the two objectui-only keys `disabled` and
* `icon`. This face restates none of them; it adds exactly the one key below
* and keeps the spec's `value` — a lowercase machine identifier — as declared.
*
* This is the declared element type of a select field's and a lookup field's
* `options`, so it is the runtime READ model the renderers consume. It is WIDER
* than what may be authored: `description`, `disabled` and `icon` are each
* refused BY NAME by the spec's strict `SelectOptionSchema`, which a field's
* `options` are routed through.
* `options`, so it is the runtime READ model the renderers consume. It is still
* WIDER than what may be authored, but by TWO keys rather than three:
* `disabled` and `icon` are refused BY NAME by the spec's strict
* `SelectOptionSchema`, which a field's `options` are routed through, while
* `description` became authorable at `@objectstack/spec` 17.3.0 (objectui#7635
* corrected this paragraph; the member docblock below carries the measurement).
*/
export interface SelectOptionMetadata extends SelectOptionBase {
/**
Expand All @@ -501,14 +517,22 @@ export interface SelectOptionMetadata extends SelectOptionBase {
* never declared it, so the behaviour was real for a key no annotated
* literal could carry. Renderers may show it as supporting text.
*
* WARNING - objectui-side extension, NOT a spec key. Measured on the
* installed `@objectstack/spec` 17.2.0: `SelectOptionSchema` is `.strict()`
* over exactly `{label, value, color, default, visibleWhen}` and REFUSES
* `description` BY NAME (`unrecognized_keys`), with the same option minus
* the key accepted as the control. `FieldSchema` routes `options` through
* that schema, so writing this key into authored object metadata fails the
* whole field. It lives on the runtime READ model the renderers consume and
* must never reach the metadata payload. Pinned by
* A DECLARED `SelectOptionSchema` key as of `@objectstack/spec` 17.3.0,
* which implements the objectui#6153 half of that ruling. Measured on the
* installed `@objectstack/spec` 17.4.0: an option carrying `description`
* is ACCEPTED, as a string (a non-string answers `invalid_type`; the empty
* string is valid), and a field whose `options` carry it parses whole. ⇒ it
* may now be written into authored object metadata, which is the point of
* the ruling — the key was consumed by `LookupField` long before the
* authoring door admitted it.
*
* ⛔ This reverses what this docblock asserted between objectui#7014 and
* objectui#7635, when the key was refused by name and this comment said so.
* The option keys that are STILL outside the vocabulary are `icon` and
* `disabled` — declared on {@link SelectOptionBase} as objectui-only
* extensions and refused by name — and they are what keeps "the schema still
* refuses something" a live fact rather than an assumption. The verdict is
* re-derived, both directions behind accepting controls, by
* `__tests__/select-option-spec-extension-7014.test.ts` (objectui#7014).
*/
description?: string;
Expand Down
20 changes: 14 additions & 6 deletions packages/types/src/select-option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,21 @@ import type { SelectOption as SpecSelectOption } from '@objectstack/spec/data';
* WARNING - this type is WIDER than the authoring contract, and it is not a
* description of what an authored object document may carry. It is the runtime
* READ MODEL the renderers consume. The spec's `SelectOptionSchema` is a strict
* object over exactly `label`, `value`, `color`, `default` and `visibleWhen`,
* and it refuses `disabled` and `icon` BY NAME (`unrecognized_keys`); a field's
* `options` are routed through that schema, so either key written into authored
* object metadata fails the WHOLE field at publish time. Pinned, each refusal
* behind an accepting control, by
* object, and it refuses `disabled` and `icon` BY NAME (`unrecognized_keys`); a
* field's `options` are routed through that schema, so either key written into
* authored object metadata fails the WHOLE field at publish time. Each refusal,
* behind an accepting control, is re-derived by
* `__tests__/select-option-spec-extension-7014.test.ts` and
* `__tests__/select-option-tier1-convergence-7014.test.ts`.
* `__tests__/select-option-tier1-convergence-7014.test.ts` — read their verdict
* rather than a key list copied into this paragraph, which is how the previous
* copy went stale.
*
* ⚠️ That list USED to be spelled out here as "exactly `label`, `value`,
* `color`, `default` and `visibleWhen`", and `@objectstack/spec` 17.3.0 falsified
* it by declaring a sixth key, `description` (objectui#6153; corrected by
* objectui#7635). The enumeration is deliberately not replaced with a longer
* one: the keys arrive through the `Omit` BY REFERENCE, so a list written here
* can only ever disagree with the derivation it sits above.
*/
export interface SelectOptionBase extends Omit<SpecSelectOption, 'visibleWhen'> {
/**
Expand Down
16 changes: 0 additions & 16 deletions scripts/check-installed-spec-pin-claims.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -638,14 +638,6 @@ export const LEDGER = [
class: "stale",
why: "\"installed spec (17.2.0) rather than assumed\" \u2014 stamps a `GetMetaItemLayeredResponseSchema` reading.",
},
{
file: "packages/fields/src/widgets/LookupField.optionDescription.test.tsx",
package: "@objectstack/spec",
version: "17.2.0",
sites: 1,
class: "stale",
why: "\"installed @objectstack/spec 17.2.0 has no such key and REFUSES it BY NAME\" \u2014 both halves are re-measurable and both have to be.",
},
{
file: "packages/i18n/src/utils/spec-formatters.ts",
package: "@objectstack/spec",
Expand Down Expand Up @@ -806,14 +798,6 @@ export const LEDGER = [
class: "stale",
why: "\"the pin is still 17.2.0, which PRE-dates the ...\" \u2014 the whole sentence is an argument from the pin's age.",
},
{
file: "packages/types/src/field-types.ts",
package: "@objectstack/spec",
version: "17.2.0",
sites: 1,
class: "stale",
why: "\"installed @objectstack/spec 17.2.0: `SelectOptionSchema` is `.strict()`\" \u2014 re-parse the schema before moving the number.",
},
{
file: "packages/types/src/mobile.ts",
package: "@objectstack/spec",
Expand Down
Loading