fix(spec): prescribe per action type when object-form params is refused — bodyExtra for api, target interpolation + openIn for url (#6828) - #7375
Conversation
…used (#6828) `params` has always been `z.array(ActionParamSchema)`, so the object form has always been refused. #5777 replaced the unactionable "expected array, received object" with a message naming `bodyExtra` — right for a `type:'api'` action's static request body, and wrong for every other type. On a `type:'url'` action the object form meant a third thing: objectui's ActionRunner read a non-array `params` as the `${param.X}` interpolation scope for `target`, and `params.newTab` as a legacy new-tab flag. Neither is a request-body key, which is why the `inline-action-api-params-to-body-extra` conversion guards on `type === 'api'` (rewriting a url action's object `params` would be lossy; ADR-0087 D2 requires losslessness). The maintainer's 2026-08-10 ruling retires the url meaning rather than giving it a key — both halves already have sanctioned spellings: put static values in the `target` string, and open a new tab with the declared `openIn: 'new-tab'`. The refusal message now carries both arms, and the authoring docs state the refusal where inline and url actions are described. No acceptance-face movement: `params` is still `z.array(ActionParamSchema)`, the object form is still refused with `invalid_type` at path `params`, and the array form still parses on every action type. Message and docs only. Pinned on both arms and on both the inline and registered surfaces (`inline-action.test.ts`). Reverse-verified: reverting the message turns the three url-arm pins RED on message content. Closes #6828 Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vsi2JQ41Z9ct1z8jC4MjVW
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
Closes #6828
Executes the maintainer's 2026-08-10 ruling (comment 5237221393), verbatim:
No new key enters the vocabulary. This is a message-and-docs change.
Premise check (re-verified on
main@f188ed6, not inherited from the card)paramsis stillz.array(ActionParamSchema, …)packages/spec/src/ui/action.zod.ts:907(the card's:834had drifted; re-anchored by content)bodyExtraunconditionally — see the before/after belowinline-action-api-params-to-body-extraconversion still guards ontype === 'api'packages/spec/src/conversions/registry.ts— unchanged, and untouched hereBefore / after
Before (post-#5777) — one prescription, applied to every action type:
A
type:'url'author reading that is sent to an api request-body key that is neither an interpolation scope nor a new-tab control. Wrong instruction — and the one the ruling's rationale rests on.After — the same opening and the same api arm, plus the url arm (the new sentence marked
>>):Both retired readings have a sanctioned spelling already:
${param.X}scopetargetstring itself;${param.X}interpolates what the params dialog collected,${ctx.X}the action contextparams.newTabopenIn: 'new-tab'— declared, and already read with priority by the runnerNo acceptance-face movement
paramsis stillz.array(ActionParamSchema); the object form is still refused withinvalid_typeat pathparamson both the inline and registered surfaces; the array form still parses on every action type (pinned). Nothing was added to or removed from the vocabulary, and no.describe()text changed — so no generated reference moved and no regen was needed. The shape was always refused; only what the refusal says changed.Pins (
packages/spec/src/ui/inline-action.test.ts)Both arms, by path + code + message content:
invalid_type@params,expected: 'array', message contains`target` string,openIn: 'new-tab',RETIRED, not renamed (#6828)params.newTabinvalid_type@params, message containsparams.newTabandopenIn: 'new-tab'invalid_type@params, message still containsbodyExtra: { … }and(#5777)ActionSchema— the field factory is shared, so a future fix cannot branch on the inline shape alonetype:'url'action withparams: [ … ]+openIn: 'new-tab'still parsesReverse verification: reverting only the message (
git checkout HEAD -- action.zod.ts) turns the three url-arm pins RED on message content —3 failed | 25 passed; restored,28 passed. The api pin is green on both sides by design: its job is that the url arm did not displace #5777's answer, which is a non-regression guard, not a new claim.Implementation note — the branch is in the text, not in a runtime
switchZod cannot see a sibling from a property-level error map: it receives only
{ code, expected, input, inst, path }for the offending value, and an object-level.check()/.superRefine()— which would seetype— is skipped once a property has already failed (probed directly on zod 4.4.3, the pinned version). Readingtypeat the refusal site would mean restructuringActionSchemabehind az.preprocess, which erasesz.input<typeof ActionSchema>(the authoring typedefineActionpublishes) and would move surfaces this issue must not move. So one message carries both arms, each explicitly addressed to itstype, and the constraint is recorded in the JSDoc so the next reader does not re-derive it.Docs (the ruling's closing sentence)
content/docs/ui/actions.mdx— a warn callout under "Collect input and shape the UX": a three-row "you wanted / write this instead" table (api payload →bodyExtra; url interpolation → thetargetstring; new tab →openIn), and the statement that the url meanings were retired, not renamed.content/docs/protocol/objectui/actions.mdx— in URL Actions, next to the${param.X}/${ctx.X}sentence that describes the mechanism. Also separates the two new-tab keys, which that page had conflated:openInfor a static url,opensInNewTabfor an async handler that redirects.content/docs/references/**is untouched (generated, and nodescribe()moved);content/docs/releases/**anddocs/adr/**are untouched.Follow-up filed
objectstack-ai/objectui#4097 —
ActionRunner'sinterpolateTargetnon-arrayparamsscope read and the legacyparams.newTabescape hatch are dead vocabulary under this ruling. Filed unassigned, contract-first behind this PR; objectui was not edited here. Dedup-searched (interpolateTarget params newTab,ActionRunner params,bodyExtra,6828) — no existing card; #2043 is the adjacent origin ofopenIn, not a duplicate.Changeset
.changeset/action-params-url-refusal-guidance.md— patch on@objectstack/spec. Precedent: a guidance-only change that moves no acceptance face is a patch (cf.adr-0105-d9-refuse-on-directory-less-types,@objectstack/spec: patchinaction-body-type-gatewhere the behavioural halves took the minor). No key added, no key removed, no accepted input newly refused or newly accepted.Gates run
viteston@objectstack/spec(362 files / 9473 tests, green, underflock /tmp/os-heavy-verify.lock) ·eslint --no-inline-configon the changed files ·tsc --noEmitonpackages/spec·check:doc-authoring·check:docs-audit-scope·check:quick-reference-counts·check:empty-changeset·check:adr-0087-registration.🤖 Generated with Claude Code
https://claude.ai/code/session_01Vsi2JQ41Z9ct1z8jC4MjVW