feat(spec)!: an action:group / action:menu member refuses a non-array params unless its type is api, with the action:button prescription (#21855) - #21869
Conversation
… params on a non-api type The member's params takes the ActionParam[] input list only, unless its type is api (the request-payload window). The refusal carries the member prescription: author an action with static parameter values as its own action:button node. D3 entry ui-action-group-menu-member-params-array-only with its step-18 rationale fragment (order 83); registry region regenerated. Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…nd its D3 registration Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ING, registered Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…ction drops Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…description Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ Co-authored-by: Claude <noreply@anthropic.com>
…mber-params-object-refused
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 138 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 45c248503b07a3fa816895f02dbf2bd3bd810838 && git checkout 45c248503b07a3fa816895f02dbf2bd3bd810838
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9f9510f25e6aa65aa61ce3effb42706fabcab92e 810b1c4b1863509fc8570cefa2581346cc44a5c0 && git checkout -B drift-repro 9f9510f25e6aa65aa61ce3effb42706fabcab92e && git merge --no-ff 810b1c4b1863509fc8570cefa2581346cc44a5c0
node scripts/docs-audit/affected-docs.mjs --json 9f9510f25e6aa65aa61ce3effb42706fabcab92e |
Fixes #21855
Clause-②: yes (narrowing)
What this does
An
action:group/action:menumember'sparamsnow takes the array form (ActionParam[], the input list) only, unless the member'stypeisapi. Any otherparamsvalue on a non-apimember (an object, a string, a number ornull) is refused at the component-props gate, atactions.N.params, with the member prescription in the file's existing voice: static values belong on anaction:buttonnode, whoseparamsobject carries them.This is the card's step, under the governing text it cites:
paramsis declaredActionParam[]-only (object form refused by name) and no node-levelparamskey is declared, yetaction:button,navigate_editand the published guide use an object values bag objectui#10289 (5825589480), verbatim: "⛔paramsnever carries two shapes, and ⛔ no new value-bag key is declared."drillDown.report, object-formcustomFields, formsections, object-timelineitems,action:group/action:menumembers #21704 fork 5 A (5979239990) refused a member'sproperties.paramsand kept the member'sparamsasz.unknown(), the button row's value schema.propertiesprescription incomponent.zod.tsalready pointed ataction:buttonfor static values.action:group/action:menumember with an objectparamson a non-apitype passes the component-props gate, and the container drops it at run time objectui#11638 (5990495682, amended by5991243780) withdrew the other direction (the container reading an objectparams) and filed this card for the gate half.apimember,paramsan arrayapimember (an absenttypeincluded),paramsnot an arraycustomatactions.N.params, with the prescriptiontype: 'api'member, anyparamsaction:button/action:iconnode, objectparamsThe refusal message, for a
navigate_editmenu member:How
packages/spec/src/ui/component.zod.ts.actionContainerMemberParamsFitType(container), goes on both member builders (buildActionGroupMember,buildActionMenuMember) through.superRefine.paramsstaysz.unknown(), so it keeps the enumeration pin'srunnerline. Its.describe()now states the accept set and still says "forwarded to the runner"..objectui-shapin0abd4f9f8. The objectui renderer directory is byte-identical there, at2e818d0b5and at objectuimainf1a177c41(git diff --quiet).strictObject's unknown-key refusal stays terminal, so a member already refused for a key is not judged a second time (pinned).params, no export moved, and theapiwindow is untouched.The ADR-0087 kit (the #21702 / PR #21712 and #21464 / PR #21764 shape)
packages/spec/src/migrations/entries/semantic/18.ui-action-group-menu-member-params-array-only.ts.STEP18_RATIONALEfragment at order 83, inserted where its id sorts. 83 is the next free order: the highest onmainis 82, re-read at5b2d189e28and again at2df3d13d16after the merge.registry.ts's generated region, written bygen:migration-registry.@objectstack/specminorchangeset,.changeset/21855-action-member-params-array-only.md. It carries theClause-②line, theadr-0087: registered ui-action-group-menu-member-params-array-onlydisposition marker and a FROM → TO table.spec-changes.jsonanddocs/protocol-upgrade-guide.md. Both project the registry from the support floor up to the current protocol major (17), so a step-18 entry is not in either yet.check:spec-changesandcheck:upgrade-guideare green with both files untouched, as on the two precedents.packages/spec/dropped-refinements.baseline.jsongainsui/ActionGroupPropsandui/ActionMenuProps(siteactions.elementeach), exactly asbuild-schemasprinted them. Itsmeasuredcounts go 218 → 220 schemas and 676 → 678 sites. The JSON Schema projection has no arm for acustomcheck; the S-final stage declared its timeline refinement the same way.content/docs/references/ui/component.mdxwas regenerated bycheck:generated --fix. That run proved this the only stale artifact; the change is the two memberparamsrows.Census, re-run before writing (at base
5b2d189e28), with a lit controlThe question: which
action:group/action:menumembers author a non-arrayparamson a non-apitype?Instrument (scratchpad
census.cjs):.ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.jsonand fenced Markdown code. YAML is read as text.paramskey in every file that names either block, with its value kind and its owner'stype. Same-file constants are resolved. A member is classified automatically when itsactionsowner (flat, inside a node'spropertiesbag, or through a same-file constant array) carries the blocktype.paramson an element of anyactionsarray corpus-wide, to catch members built in files that never name a block.mount(surface, entry),schema(member({ … }))) are resolved that way.apiobject members (flat, insideproperties, through a const array, in a Markdown fence), oneapimember and one array member. The instrument found and classified all six. Separately, the hand-read loose pass reached objectui's own helper-position drop probes, below.paramskeys thereparamson a non-apitype5b2d189e28element:buttonaction conversion fixtures, schema source, the liveness ledger'sparamsrow, CHANGELOG quotations, and oneproperties.paramsrefusal probe.0abd4f9f8action-entry-object-params-10462.test.tsx:144,:193andaction-container-member-params-10290.test.tsx:196. Thetype: 'api'controls beside them stay accepted.mainf1a177c414054ec2680git ls-remoteasks for credentials, the API answers 403, andadd_repo(read) answers "you don't have access"Deployed metadata was not measured. So the change narrows a zero-writer spelling, and
Clause-②: yes (narrowing)holds.Tests
packages/spec/src/ui/component-action-member-params-array-only.pin.test.ts, 39 tests:[{ code: 'custom', path: 'actions.N.params' }]exactly. The values are an object onnavigate_edit, on an absenttypeand onurl; an empty object; a string; a number; andnull. One more case puts the issue on the second member. The message names the container, the member'stypeand theaction:buttonprescription.apiandapimembers, an empty array, theapimember's object and stringparams, noparams, andbodyExtra. A CONTROL showsaction:button/action:iconnodes keep their objectparams.7a28c5194a(thecomponent.zod.tsblob is unchanged since, through the merge):scripts/ablation-replace.mjsreplaced the refinement's guard with a barereturn(anchor ×1 → ×0, blobd8565fd7a8→930000300e), inside a driver carrying its ownEXIT INT TERMrestore trap on the absolute path.Tests 18 failed | 160 passed (178). The 18 were exactly the §1 cases.d8565fd7a8equals HEAD's, andgit diff HEADis empty../component.zodrelatively, so it reachessrcand nodistis involved.validateComponentProps, fromsrc, ran over this branch's built@objectstack/spec:navigate_editgroup member and a menu member with notype, each with an objectparams, each give onecomponent-props-invalidfinding (warning) atpages[0].regions[0].components[0].properties.actions.0.params, carrying the message above;scriptmember, an object on anapimember, and an object on anaction:buttonnode.Verification
All at head
810b1c4b18(the merge ofmain2df3d13d16, below) unless noted.@objectstack/spec,vitest run --project local --maxWorkers=2(the package'stestscript), under the verify lock:Test Files 616 passed (616),Tests 18426 passed | 1 todo. Before the merge, ata6f230772f, both projects (localandrepo):Test Files 669 passed (669),Tests 19325 passed | 1 todo.pnpm --filter @objectstack/spec typecheck, run ata6f230772f: exit 0. That coverstsc --noEmit,check:scripts-typecheckandcheck:test-typecheck: OK(52 files / 246 errors / 135 signatures held, unchanged).tsc -p tsconfig.test.json --listFilesOnlynames the new pin, and the D3 entry is in thetsconfig.jsonprogram.@objectstack/lint, whole suite, the component-props gate's package:Test Files 119 passed (119),Tests 5627 passed.@objectstack/specbuild, thencheck:generated:All 15 generated artifacts are up to date. Thecheck:generated --fixrun before the merge provedcheck:docsthe only stale artifact, and only it was regenerated.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 114 commands at810b1c4b18, against merge base2df3d13d1(7 paths). Each was run with its exit code recorded before any pipe.--ranreconciled 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN, and all 114 exited 0.PREREQUISITE NOT MET(exit 3) because workspace packages were unbuilt: lint'scheck:doc-formula-expressionsandcheck:doc-security-posture, spec'scheck:skill-examples,check:docs-transcript-drift,check:dual-build-cjs-loadsandcheck:lean-entry-closure. Afterturbo run build --filter=!@objectstack/docs --concurrency=2they were re-run at the same head, each reaching its own verdict with exit 0. The record holds the re-runs.@objectstack/hono#buildfailed in its DTS-emitted check. Thedist/index.d.tsit named missing was on disk right after, and a rebuild was green (31 successful, 31 total). The adapter is outside this diff.check-adr-0087-registration --base origin/main(one declared-breaking changeset,registered ui-action-group-menu-member-params-array-only),check-changeset-no-major,check-empty-changeset,check:migration-registry,check:spec-changes,check:upgrade-guide,check:authorable-surface,check:api-surface,check:docs,check:strictness-ledger,check:doc-authoring,check:issue-citations,check:cross-package-test-inputs,check:nul-bytes,check:type-check-debt.eslint.config.mjs's**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}block covers all four changed.tsfiles.eslint --no-inline-config --format json: 4 files, 0 errors, 0 warnings.parserOptions.project, as its own comment states), so this diff cannot move a verdict on an untouched file.origin/mainmoved 4 commits after the base (5b2d189e28→2df3d13d16). It was merged throughscripts/pm/os-regen-merge.sh(⛔ no rebase), and none of those commits touches a file of this PR. A re-fetch just before this PR showed 4 more commits onmain, none touching these files, so no second merge.pnpm lint, the Console Pin Gate, the Dogfood Regression Gate and therepovitest project after the merge. Reason: CI-owned.Acceptance notes
nullparamson a non-apimember is refused as well as an object. The container drops each of those the same way (readActionEntryParamValuesreturns nothing for any non-array value on a non-apitype), and the census found none of them either. If the reviewer reads the card as objects only, the change is one condition in the refinement's guard plus the string, number andnullcases in §1.apiwindow is untouched, and ending it is not this PR's job. Anapimember's objectparamsstays accepted. When protocol 18 ends that window, the member refinement'stype === 'api'arm is the one line that moves. Carrier: whoever ends that window. Noted, not filed.readMemberStaticParamValuesstill reads a member'sproperties.params, which the spec refuses. [finding] spec(ui): anaction:group/action:menumember with an objectparamson a non-apitype passes the component-props gate, and the container drops it at run time objectui#11638, as re-scoped by triage (5991243780), carries it. No objectui file is touched here.dropped-refinements.baseline.json'smeasured.refinementSitesThatDidProjectreads 369, while this build prints "450 refinement site(s) DID reach the file". No gate reads that number. It is left as found; changing it is outside this card. Carrier: none. Noted, not filed.Generated by Claude Code