Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.
The mismatch
src/ui/expression-bindable-text-keys.zod.ts documents the button row's evidence as two files (verbatim from the docblock table):
| button | schema.label (form/button.tsx, action/action-button.tsx) |
But the map is keyed on the bare registry name:
EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT = { statistic: [...], card: [...], button: ['label'] }
and expressionBindableTextKeysFor does an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:
| file |
registration |
expressionBindableTextKeysFor(type) |
packages/components/src/renderers/form/button.tsx |
register('button', …, { namespace: 'ui' }) → ui:button and the bare button fallback |
button → ['label'] ✅ |
packages/components/src/renderers/action/action-button.tsx |
register('button', …, { namespace: 'action', skipFallback: true }) → action:button only |
action:button → [] ❌ |
Measured against the installed 17.2.0:
fn('button') -> [ 'label' ]
fn('action:button') -> []
fn('ui:statistic') -> []
ActionButtonRenderer reads schema.label at the node top level (two sites), exactly like the ui:button renderer — so the row's own stated evidence covers it, but the lookup does not reach it.
Why the consumer will not paper over it
The objectui memo passes the authored type string verbatim, deliberately. Prefix-stripping (action:button → button) is the obvious patch and is wrong: it would in the same motion grant rows to element:button and page:card, whose renderers read config out of the bag via readProps() and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").
So the correct place to resolve it is here.
Measured impact today
Small but non-zero: the objectui corpus census (736 JSON documents plus json doc fences, 2747 typed nodes) counts 5 action:button nodes, against 158 bare button, 135 card, 42 statistic, and zero ui:-prefixed spellings. None of the 5 currently carries a ${…} in label, so nothing is visibly broken — this is a latent gap, not a live defect.
Options
- Add an
action:button row (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.
- Declare the map's keys to be bare registry names and give the lookup a documented normalization rule with an explicit deny-list for the namespaces that read from the bag (
element:, page:). More machinery, and the deny-list is the inference the module is trying to avoid.
- Narrow the docblock to cite only
form/button.tsx, recording that action:button is deliberately out.
Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.
Provenance
Measured on objectui origin/main 7a5da1414 with @objectstack/spec@17.2.0 installed. Registration sites and read points are reproducible from that tree.
Found while implementing the objectui half of the expression-bindable text keys (objectui#4795, consuming this module; PR objectstack-ai/objectui#6981). A row-vs-key-spelling mismatch, filed rather than papered over renderer-side.
The mismatch
src/ui/expression-bindable-text-keys.zod.tsdocuments thebuttonrow's evidence as two files (verbatim from the docblock table):But the map is keyed on the bare registry name:
and
expressionBindableTextKeysFordoes an own-property lookup on the type string it is handed. In objectui those two files register under different type strings:expressionBindableTextKeysFor(type)packages/components/src/renderers/form/button.tsxregister('button', …, { namespace: 'ui' })→ui:buttonand the barebuttonfallbackbutton→['label']✅packages/components/src/renderers/action/action-button.tsxregister('button', …, { namespace: 'action', skipFallback: true })→action:buttononlyaction:button→[]❌Measured against the installed 17.2.0:
ActionButtonRendererreadsschema.labelat the node top level (two sites), exactly like theui:buttonrenderer — so the row's own stated evidence covers it, but the lookup does not reach it.Why the consumer will not paper over it
The objectui memo passes the authored
typestring verbatim, deliberately. Prefix-stripping (action:button→button) is the obvious patch and is wrong: it would in the same motion grant rows toelement:buttonandpage:card, whose renderers read config out of the bag viareadProps()and never touch these keys on the node — manufacturing the evaluated-but-not-read-back half objectui#4795 exists to close. And the module's docblock forbids exactly that shape of fix ("never inferred from what a renderer happens to read").So the correct place to resolve it is here.
Measured impact today
Small but non-zero: the objectui corpus census (736 JSON documents plus
jsondoc fences, 2747 typed nodes) counts 5action:buttonnodes, against 158 barebutton, 135card, 42statistic, and zeroui:-prefixed spellings. None of the 5 currently carries a${…}inlabel, so nothing is visibly broken — this is a latent gap, not a live defect.Options
action:buttonrow (['label']). Faithful to the evidence the docblock already cites, keeps the map's "key is the type string" semantics exact and mechanically answerable.element:,page:). More machinery, and the deny-list is the inference the module is trying to avoid.form/button.tsx, recording thataction:buttonis deliberately out.Recommendation: (1) — one line, no new concepts, and it makes the declaration say what its own evidence table already claims. (3) is acceptable if
action:button's label is considered action-declared rather than author-written, but then the docblock should say so rather than citing the file as evidence for a row that does not cover it.Provenance
Measured on objectui
origin/main7a5da1414with@objectstack/spec@17.2.0installed. Registration sites and read points are reproducible from that tree.