Found while implementing #5637 (element:record_picker's label / placeholder). Out of that card's fence — filed, not fixed there.
This is the same family, opposite direction. #5590 and #5637 are "the contract admits the map, the renderer drops it, so the declaration correctly stays narrow until the render site catches up". element:text_input is the case where the render site already caught up and the declaration never moved.
Measured
packages/components/src/renderers/basic/text-input.tsx resolves all three keys at their read sites:
96: const label = pickLocalized(props.label, language);
97: const placeholder = pickLocalized(props.placeholder, language);
98: const description = pickLocalized(props.description, language);
The contract for all three is the I18nLabel union — measured on the installed @objectstack/spec17.1.0 pin, ElementTextInputPropsSchema:
label union [string | record] | map safeParse: OK
placeholder union [string | record] | map safeParse: OK
description union [string | record] | map safeParse: OK
But the ComponentMeta entries still declare one arm each (text-input.tsx:146, :147, :183):
{name: 'label',type: 'string',label: 'Label'},{name: 'placeholder',type: 'string',label: 'Placeholder'},…{name: 'description',type: 'string',label: 'Description'},Driven through the same manifestFromConfigs + validateTree path the console specimen test uses, with { en: 'Owner', 'zh-CN': '负责人' }:
PROBE label declaredArms="string" codes=["type-mismatch :: <element:text_input> prop \"label\" expected a string"]
PROBE placeholder declaredArms="string" codes=["type-mismatch :: <element:text_input> prop \"placeholder\" expected a string"]
PROBE description declaredArms="string" codes=["type-mismatch :: <element:text_input> prop \"description\" expected a string"]
Why this is the defect and not a harmless narrowing
ComponentInput.type in packages/types/src/base.ts states the rule in both directions: declare an arm the renderer drops and you advertise a shape that never reaches the screen; withhold an arm the renderer resolves and the manifest gate reports type-mismatch on a legal write. Its own words for the second half — "one platform authority contradicting itself on the write it just recommended, at warning severity — which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss the unknown-prop and type-mismatch reports that ARE real."
Here the author writes a map, the renderer renders it correctly in the viewer's language, and the gate warns anyway. That is precisely the direction #5590's changeset called "the false declaration in the other direction".
Not a duplicate of the defaultValue specimen
apps/console/src/__tests__/component-input-union-specimens.test.ts:214 asserts that an I18N_MAP on element:text_input.defaultValuedoes report type-mismatch, and that assertion is correct and must stay: defaultValue's spec type is string | number, a key whose contract has no object arm. It is the control that keeps the widening per-key. The three keys above are a different set with a different contract, and none of them is pinned anywhere today.
What a fix would decide
Mechanically it looks like the emptyText move run backwards: widen the three inputs arms to ['string', 'object'] and rewrite the descriptions from what the renderer does. Two things worth a triage look rather than assuming:
- Whether all three travel together, or
description is separable — it has a different destination than the two label-ish keys. - Whether the console specimen file should gain a pin for this block, since the specimens are where "declared arms == contract arms" is currently guarded, and a fix with no pin can silently regress the same way.
Filed unassigned and unqueued.
Found while implementing #5637 (
element:record_picker'slabel/placeholder). Out of that card's fence — filed, not fixed there.This is the same family, opposite direction. #5590 and #5637 are "the contract admits the map, the renderer drops it, so the declaration correctly stays narrow until the render site catches up".
element:text_inputis the case where the render site already caught up and the declaration never moved.Measured
packages/components/src/renderers/basic/text-input.tsxresolves all three keys at their read sites:The contract for all three is the
I18nLabelunion — measured on the installed@objectstack/spec17.1.0 pin,ElementTextInputPropsSchema:But the
ComponentMetaentries still declare one arm each (text-input.tsx:146,:147,:183):Driven through the same
manifestFromConfigs+validateTreepath the console specimen test uses, with{ en: 'Owner', 'zh-CN': '负责人' }:Why this is the defect and not a harmless narrowing
ComponentInput.typeinpackages/types/src/base.tsstates the rule in both directions: declare an arm the renderer drops and you advertise a shape that never reaches the screen; withhold an arm the renderer resolves and the manifest gate reportstype-mismatchon a legal write. Its own words for the second half — "one platform authority contradicting itself on the write it just recommended, at warning severity — which is worse than it sounds, because noise on legal writes trains authors (AI authors included) to dismiss theunknown-propandtype-mismatchreports that ARE real."Here the author writes a map, the renderer renders it correctly in the viewer's language, and the gate warns anyway. That is precisely the direction #5590's changeset called "the false declaration in the other direction".
Not a duplicate of the
defaultValuespecimenapps/console/src/__tests__/component-input-union-specimens.test.ts:214asserts that anI18N_MAPonelement:text_input.defaultValuedoes reporttype-mismatch, and that assertion is correct and must stay:defaultValue's spec type isstring | number, a key whose contract has no object arm. It is the control that keeps the widening per-key. The three keys above are a different set with a different contract, and none of them is pinned anywhere today.What a fix would decide
Mechanically it looks like the
emptyTextmove run backwards: widen the threeinputsarms to['string', 'object']and rewrite the descriptions from what the renderer does. Two things worth a triage look rather than assuming:descriptionis separable — it has a different destination than the two label-ish keys.Filed unassigned and unqueued.