Found while fixing objectui#3174 (the referenceTo vs reference spelling split). Recording only — out of scope there, because retiring it is its own decision with its own blast radius.
The gap
validation?: string is declared on both halves of the action-param contract:
- authoring —
@object-ui/types' ActionParam (packages/types/src/ui-action.ts) - resolved —
@object-ui/core's ActionParamDef (packages/core/src/actions/ActionRunner.ts)
Nothing reads it, on either side:
resolveActionParams() (packages/app-shell/src/utils/resolveActionParams.ts) never copies it onto the resolved param — it is not in RawActionParam at all.paramToField() (packages/app-shell/src/utils/paramToField.ts) never maps it into the field object the widgets consume.buildValidationRules() in @object-ui/fields builds rules from required / minLength / maxLength / pattern / type field metadata. There is no validation branch, so even if it reached the field it would be inert.
And it is not authorable either: ActionParamSchema in @objectstack/spec/ui is .strict() with a custom unknown-key error, and validation is not in its ACTION_PARAM_KEYS. So an authored validation: '...' is a hard parse rejection on the server, while tsc against the public type accepts it.
That is exactly the objectui#3174 shape — declared, inert, and blessed by a type the platform's own parser refuses — minus the second spelling that made #3174 worse.
Why it was left standing in #3174
#3174's fix removed the nine resolved-side picker keys (referenceTo, displayField, idField, descriptionField, titleFormat, lookupColumns, lookupFilters, lookupPageSize, dependsOn) from the authoring ActionParam, and installed a drift guard pinning the rule "the authoring type declares exactly the spec's authorable keys". validation is carried as the one named exception in that guard, so a second undeclared local key cannot appear without a decision — but the exception itself is debt, and this issue is the ledger entry for it.
The decision this needs
- Remove it from
ActionParam (and probably ActionParamDef too), on the enforce-or-remove reading: it can never work as authored, so declaring it can only mislead. Breaking change to two published types; needs a changeset. - Implement it — decide what an "expression" means here (CEL? a formula? a regex?), add it to
ActionParamSchema in @objectstack/spec (cross-repo contract change), have the resolver carry it and paramToField lower it into a real validation rule.
Option 1 is the ADR-0049 default and the cheaper honest answer; option 2 is only worth it if per-param validation is a capability we actually want, in which case the spec is where it starts. Either way the gate is a test asserting that whatever the authoring type declares, the param dialog enforces.
Acceptance
Found while fixing objectui#3174 (the
referenceTovsreferencespelling split). Recording only — out of scope there, because retiring it is its own decision with its own blast radius.The gap
validation?: stringis declared on both halves of the action-param contract:@object-ui/types'ActionParam(packages/types/src/ui-action.ts)@object-ui/core'sActionParamDef(packages/core/src/actions/ActionRunner.ts)Nothing reads it, on either side:
resolveActionParams()(packages/app-shell/src/utils/resolveActionParams.ts) never copies it onto the resolved param — it is not inRawActionParamat all.paramToField()(packages/app-shell/src/utils/paramToField.ts) never maps it into the field object the widgets consume.buildValidationRules()in@object-ui/fieldsbuilds rules fromrequired/minLength/maxLength/pattern/typefield metadata. There is novalidationbranch, so even if it reached the field it would be inert.And it is not authorable either:
ActionParamSchemain@objectstack/spec/uiis.strict()with a custom unknown-key error, andvalidationis not in itsACTION_PARAM_KEYS. So an authoredvalidation: '...'is a hard parse rejection on the server, whiletscagainst the public type accepts it.That is exactly the objectui#3174 shape — declared, inert, and blessed by a type the platform's own parser refuses — minus the second spelling that made #3174 worse.
Why it was left standing in #3174
#3174's fix removed the nine resolved-side picker keys (
referenceTo,displayField,idField,descriptionField,titleFormat,lookupColumns,lookupFilters,lookupPageSize,dependsOn) from the authoringActionParam, and installed a drift guard pinning the rule "the authoring type declares exactly the spec's authorable keys".validationis carried as the one named exception in that guard, so a second undeclared local key cannot appear without a decision — but the exception itself is debt, and this issue is the ledger entry for it.The decision this needs
ActionParam(and probablyActionParamDeftoo), on the enforce-or-remove reading: it can never work as authored, so declaring it can only mislead. Breaking change to two published types; needs a changeset.ActionParamSchemain@objectstack/spec(cross-repo contract change), have the resolver carry it andparamToFieldlower it into a real validation rule.Option 1 is the ADR-0049 default and the cheaper honest answer; option 2 is only worth it if per-param validation is a capability we actually want, in which case the spec is where it starts. Either way the gate is a test asserting that whatever the authoring type declares, the param dialog enforces.
Acceptance
ActionParamandActionParamDefagree with what the runtime does withvalidation.reference, which the public type didn't declare #3174 drift guard inpackages/types/src/__tests__/page-nav-misc-spec-parity.test.tsno longer needs a named exception (or names a different, justified one).