From 2925aea781d82af6913677e804c48b3b5a674afa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 01:23:23 +0000 Subject: [PATCH] docs(spec): narrow the expression-bindable `button` row to the spelling its key reaches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` is keyed on the component type string exactly as authored, and `expressionBindableTextKeysFor` does an own-property lookup on it. The `button` row's evidence citation named `action/action-button.tsx`, which registers under `action:button` — a different key, with no row, answering the empty set. The citation therefore promised coverage the lookup cannot deliver. The machine face does not move: the map, the lookup and every export are unchanged, and `check:api-surface` is green against a fresh build. This is the protocol prose catching up with its own evidence table. - `button` row cites `form/button.tsx` alone. - New docblock section: the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set BY CONSTRUCTION — and prefix-stripping is the wrong repair, since it would also grant rows to `element:button` and `page:card`, whose renderers read config out of the evaluated `properties` bag and never touch these keys at the node's top level. - `action:button` and `ui:button` recorded as deliberately out on the two measured grounds (zero corpus pull; the module's own admission rule), with the ordinary reopen path. - The test file carried the same retracted citation in a comment; corrected, and the namespaced spellings are now pinned to the empty set so a later prefix-stripping change cannot move the machine face in silence. - Reference page regenerated by `gen:docs`, never hand-edited. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01GDA48PuRFrHyRfdkBz8m21 --- .changeset/spotty-buttons-narrow-their-row.md | 11 ++++++++ .../ui/expression-bindable-text-keys.mdx | 28 ++++++++++++++++++- .../ui/expression-bindable-text-keys.test.ts | 23 ++++++++++++--- .../ui/expression-bindable-text-keys.zod.ts | 28 ++++++++++++++++++- 4 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 .changeset/spotty-buttons-narrow-their-row.md diff --git a/.changeset/spotty-buttons-narrow-their-row.md b/.changeset/spotty-buttons-narrow-their-row.md new file mode 100644 index 0000000000..e738d1c067 --- /dev/null +++ b/.changeset/spotty-buttons-narrow-their-row.md @@ -0,0 +1,11 @@ +--- +'@objectstack/spec': patch +--- + +Correct the `EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT` docblock so it stops promising coverage the lookup does not deliver. + +The `button` row cited two objectui renderers as its evidence — `form/button.tsx` and `action/action-button.tsx` — but the map is keyed on the type string exactly as authored, and `action-button.tsx` registers under `action:button`, a different key with no row. Prose only: the map, `expressionBindableTextKeysFor` and every exported type are byte-identical, `action:button` still answers the empty set, and no accept surface widens. + +- The `button` row now cites `form/button.tsx` alone. +- A new docblock section records that the table is keyed on the authored type string, so namespace-prefixed spellings (`action:button`, `ui:button`, the `mcp:` family) answer the empty set by construction rather than by oversight — and that prefix-stripping is the wrong repair, because it would in the same motion grant rows to `element:button` and `page:card`, whose renderers never read these keys at the node's top level. +- `action:button` and `ui:button` are recorded as deliberately out on two measured grounds — zero pull in the objectui corpus, and the module's own admission rule that each row arrives with its own measurement — alongside the unchanged reopen path for a named requirement. diff --git a/content/docs/references/ui/expression-bindable-text-keys.mdx b/content/docs/references/ui/expression-bindable-text-keys.mdx index 4b03fe9ffa..54edf42b84 100644 --- a/content/docs/references/ui/expression-bindable-text-keys.mdx +++ b/content/docs/references/ui/expression-bindable-text-keys.mdx @@ -59,7 +59,7 @@ the evaluated-but-blank half of the objectui#4795 table: |-------------|----------------------------------------------------------| | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) | | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) | - | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) | + | `button` | `schema.label` (`form/button.tsx`) | These are the measured motivating cases from objectui#4795 (dashboard workhorses). Other registered renderers also read keys from this closed set @@ -78,6 +78,32 @@ channel as `content` alone and declared `text.value` OUT on those grounds — this omission is deliberate, not pending measurement. Adding a row is additive and spec-first; do it here, never as a renderer-side inference. +## Rows are keyed on the AUTHORED type string, prefix and all + +`expressionBindableTextKeysFor` does an own-property lookup on the +type string exactly as authored, so a namespace-prefixed spelling — +`action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from +the bare name and answers the empty set BY CONSTRUCTION, not by oversight. +Nothing strips a prefix between the two, and nothing should: a stripping +rule would in the same motion grant rows to `element:button` and +`page:card`, whose renderers take their config from the evaluated +`properties` bag and never read these keys at the node's top level. + +`action:button` is therefore deliberately OUT, and `ui:button` with it +(objectstack#13672); the `button` row above covers the bare `button` +spelling alone, which is why its citation names `form/button.tsx` only. Two +measured grounds, the same two kinds every other row runs on — zero pull +(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5 +`action:button` nodes, 0 of them carrying an expression in `label`, and 0 +`ui:`-prefixed spellings of any kind), and this module's own admission rule +that a row arrives with its own measurement while a type string with no row +IS the empty set. Not a judgement that the key is inert: +`action/action-button.tsx` renders the author-written `schema.label` +directly (measured 2026-08-31), so the read-back half is real and only the +pull is missing. The reopen path is the ordinary one: a named requirement +for an expression-bound `action:button` label adds a row here, carrying that +row's own measurement. + **Source:** `packages/spec/src/ui/expression-bindable-text-keys.zod.ts` diff --git a/packages/spec/src/ui/expression-bindable-text-keys.test.ts b/packages/spec/src/ui/expression-bindable-text-keys.test.ts index 67b9479387..1cfbfd2175 100644 --- a/packages/spec/src/ui/expression-bindable-text-keys.test.ts +++ b/packages/spec/src/ui/expression-bindable-text-keys.test.ts @@ -59,7 +59,10 @@ describe('EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT — measured carriage', () it('matches the renderer read points measured at the objectui pin', () => { // data-display/statistic.tsx reads schema.label / .value / .description; // layout/card.tsx reads schema.title / .description; - // form/button.tsx (and action/action-button.tsx) read schema.label. + // form/button.tsx reads schema.label — and the row covers that BARE + // `button` spelling alone: action/action-button.tsx registers as + // `action:button`, a different key this map deliberately does not + // carry (objectstack#13672; see the module docblock). expect(EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).toEqual({ statistic: ['label', 'value', 'description'], card: ['title', 'description'], @@ -110,9 +113,21 @@ describe('expressionBindableTextKeysFor — the mechanical per-type answer', () it('answers the empty set for every unlisted type — closed, never inferred', () => { // `text` binds through its own `content` leg; `element:*` / `page:*` - // config rides the evaluated `properties` bag — none of them get rows - // inferred from what their renderers happen to read. - for (const type of ['text', 'element:text', 'page:card', 'alert', '']) { + // config rides the evaluated `properties` bag; and a namespace-prefixed + // spelling is a DIFFERENT key from the bare name, so `action:button` and + // `ui:button` answer empty by construction — the pair the objectstack#13672 + // ruling recorded as deliberately out, pinned here so a later prefix- + // stripping "fix" cannot move the machine face in silence. None of them get + // rows inferred from what their renderers happen to read. + for (const type of [ + 'text', + 'element:text', + 'page:card', + 'action:button', + 'ui:button', + 'alert', + '', + ]) { const keys = expressionBindableTextKeysFor(type); expect(keys).toEqual([]); expect(Object.isFrozen(keys)).toBe(true); diff --git a/packages/spec/src/ui/expression-bindable-text-keys.zod.ts b/packages/spec/src/ui/expression-bindable-text-keys.zod.ts index 726f382627..88ece8d6b7 100644 --- a/packages/spec/src/ui/expression-bindable-text-keys.zod.ts +++ b/packages/spec/src/ui/expression-bindable-text-keys.zod.ts @@ -55,7 +55,7 @@ * |-------------|----------------------------------------------------------| * | `statistic` | `schema.label`, `schema.value`, `schema.description` (`data-display/statistic.tsx`) | * | `card` | `schema.title`, `schema.description` (`layout/card.tsx`) | - * | `button` | `schema.label` (`form/button.tsx`, `action/action-button.tsx`) | + * | `button` | `schema.label` (`form/button.tsx`) | * * These are the measured motivating cases from objectui#4795 (dashboard * workhorses). Other registered renderers also read keys from this closed set @@ -73,6 +73,32 @@ * channel as `content` alone and declared `text.value` OUT on those grounds — * this omission is deliberate, not pending measurement. Adding a row is * additive and spec-first; do it here, never as a renderer-side inference. + * + * ## Rows are keyed on the AUTHORED type string, prefix and all + * + * {@link expressionBindableTextKeysFor} does an own-property lookup on the + * type string exactly as authored, so a namespace-prefixed spelling — + * `action:button`, `ui:button`, the `mcp:` family — is a DIFFERENT key from + * the bare name and answers the empty set BY CONSTRUCTION, not by oversight. + * Nothing strips a prefix between the two, and nothing should: a stripping + * rule would in the same motion grant rows to `element:button` and + * `page:card`, whose renderers take their config from the evaluated + * `properties` bag and never read these keys at the node's top level. + * + * `action:button` is therefore deliberately OUT, and `ui:button` with it + * (objectstack#13672); the `button` row above covers the bare `button` + * spelling alone, which is why its citation names `form/button.tsx` only. Two + * measured grounds, the same two kinds every other row runs on — zero pull + * (the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5 + * `action:button` nodes, 0 of them carrying an expression in `label`, and 0 + * `ui:`-prefixed spellings of any kind), and this module's own admission rule + * that a row arrives with its own measurement while a type string with no row + * IS the empty set. Not a judgement that the key is inert: + * `action/action-button.tsx` renders the author-written `schema.label` + * directly (measured 2026-08-31), so the read-back half is real and only the + * pull is missing. The reopen path is the ordinary one: a named requirement + * for an expression-bound `action:button` label adds a row here, carrying that + * row's own measurement. */ import { z } from 'zod';