Uh oh!
There was an error while loading. Please reload this page.
fix: one shared rule for declared action arrays; mixed id/object arrays refused (objectui#7182) - #7381
Merged
Conversation
…ays, mixed id/object refused Maintainer ruling 2026-09-02 (objectui#7182, option C): an `actions` array on `page:header` or `record:quick_actions` is either all action ids or all inline ActionDef objects; a mixed array is refused loudly, naming the offending index. - `@object-ui/types`: pure `resolveDeclaredActionIds(elements, registered)` beside `actionRendersAt`, with its shape half `classifyDeclaredActions` and the `DeclaredActionsShape` / `DeclaredActionsResolution` / `DeclaredActionsRefusal` result types. - `page:header` (containers.tsx): the per-element normalisation is gone; the header calls the shared function, refuses a mixed array (console.error once, nothing authored drawn), warns on unresolved ids as before. - `record:quick_actions`: the whole-array `every(string)` switch is gone; the bar calls the shared function for both `actionNames` and host `actions`, refuses a mixed array (console.error once, placeholder names the index). - Pins: function cases in types; the header's mixed-array tolerance case is replaced by a refusal pin; a quick_actions id/object equivalence + refusal pin; a two-renderer pin proving both surfaces draw the same buttons for the same id-authored array and refuse the same mixed array. - Changeset (minor, types/components/plugin-detail) and the slotted-pages guide paragraph. Refs: objectui#7182 (ruling comment 5509039514), objectstack#11592. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…ngeset names three behaviour changes Contract review on PR #7381 (objectui#7182), patch round: - `classifyDeclaredActions` and `DeclaredActionsShape` are no longer exported: the renderers' pre-lookup need (kind + ids before the registry exists) is served by `resolveDeclaredActionIds(elements, undefined)`, so both renderers now call that; a second public function and third public type would be a permanent surface for a need the first already serves. The types test pins the classifier's absence from the barrel. - Changeset names the three behaviour changes the review found unannounced: page:header no longer trims a padded id, warns on a blank id instead of skipping it; record:quick_actions renders nothing for an all-id array with no object bound instead of handing bare strings to the engine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC
Contributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
os-project-manager
marked this pull request as ready for review
September 2, 2026 15:58
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#7182
Ruling implemented
Maintainer ruling 2026-09-02 (issue comment 5509039514, decision batch #7, verbatim 「7189 A 其他同意」 — "其他同意" adopts this card's recommendation C): an
actionsarray onpage:headerorrecord:quick_actionsis either all action ids or all inline ActionDef objects; a mixed['convert', { … }]array is refused loudly, naming the offending index, not tolerated by one renderer and half-drawn by the other. One pureresolveDeclaredActionIds(elements, registeredActions)lives in@object-ui/typesbesideactionRendersAtand both renderers call it; the whole-array switch inrecord-quick-actions.tsxand the per-element normalisation incontainers.tsxare both gone.Clause-② is YES per the ruling, so this PR is a draft carrying the reviewable contract increment and is labelled
needs:contract-review. Contract review returned PASS with two text amendments and one surface question; all three are addressed in the patch round below. Not flipped ready, not enqueued — the PM does that after re-reading the changeset.What changed
packages/types/src/ui-action.ts—resolveDeclaredActionIds(elements, registeredActions): resolution bynamein authored order, first registration wins on a duplicate name; ids that name nothing come back as index/id pairs for the caller to warn about once its own lookup has settled. Called withregisteredActionsundefined(no registry yet) it returns the registry-independent verdict —kindandidsfinal, everything unresolved — which is what both renderers read before deciding whether to request a metadata read at all. Result typesDeclaredActionsResolutionandDeclaredActionsRefusal. The shape classifier and its shape type are module-internal (patch round, see below). Zero-dependency, typed onunknown[]for the same reasonactionRendersAttypeslocationsas plain strings. The element rule is closed: a string is an id, a non-null non-array object is an inline definition, anything else (null, a number, a boolean, a nested array) is refused at its index too. An empty array passes through as an empty object list.packages/types/src/index.ts— barrel exports the one function (besideactionRendersAt) and the two result types. One-authority gate: none of the names had a prior declaration inpackages/,scripts/__tests__/one-authority-per-exported-name-6273.test.tsis green on HEAD,KNOWN_COLLISIONSuntouched.packages/components/src/renderers/layout/containers.tsx(page:header) —headerActionIdsand the per-element loop are replaced by the shared function (once with no registry for the hook-order question, once resolved). A refused array is reported once viaconsole.error(surface, index, the function's message) and none of its authored actions is drawn; host-injected system actions still render. The unresolved-id warning keeps its wording and stays gated on the lookup having settled.packages/plugin-detail/src/renderers/record-quick-actions.tsx(record:quick_actions) — theevery(string)whole-array switch is replaced by the shared function, for bothactionNames(the spec spelling) and hostactions. A refused array is reported once viaconsole.errorand the empty-state placeholder reads… actions refused at index N (see console)instead of "no actions configured".page:headerno longer trims a padded id (' convert 'used to resolve; ids are now compared exactly as authored);page:headerreports a blank''id through the unresolved-id warning instead of silently skipping it;record:quick_actionsrenders nothing (its ordinary empty placeholder) for an all-idactionsarray with no object bound instead of handing the bare strings to the action engine as action definitions.packages/types/src/__tests__/declared-action-ids-7182.test.ts(15 function cases, including the registry-independent verdict and a pin that the classifier is NOT on the barrel);packages/components/src/__tests__/page-header-action-ids.test.tsx(the mixed-array tolerance case is REPLACED by the refusal pin — it pinned the branch this PR deletes; the all-object case also asserts no metadata read);packages/plugin-detail/src/renderers/__tests__/record-quick-actions.declared-action-ids-7182.test.tsx(id/object equivalence with the object render as live control,actionNames, mixed refusal under both keys, typo-vs-refusal control);packages/plugin-detail/src/__tests__/declaredActionIds.twoRenderers-7182.test.tsx(both renderers in one process)..changeset/7182-declared-action-ids-one-rule.md—minoron@object-ui/types,@object-ui/components,@object-ui/plugin-detail; headline in the ruling's own wording; the three behaviour changes above named in a closing paragraph.content/docs/guide/slotted-pages.md— the paragraph that taught the mixed-array tolerance now states the rule.Validation layer — measured (dispatch ZONE 2, assumption 3), corrected in the patch round
Measured on
@objectstack/spec17.2.0 withComponentPropsMapsafeParse:page:header:PageHeaderProps.actionsisz.array(z.string()). A mixed array is refused at pathactions, 1(invalid_type: expected string, received object); an ALL-object array is refused too, atactions, 0. The spec is stricter than C.record:quick_actions:RecordQuickActionsPropsis strict and declaresactionNames(ids) only. Anyactionskey is refused as unrecognized regardless of shape; a mixedactionNamesis refused atactionNames, 1. Theactionsarray reaches this renderer only through host channels (layout:page-headerdelegation, the page synthesizer).Correction (contract review, re-measured on
origin/main): an earlier version of this body said Studio'sclientValidationreads that map. It does not.clientValidationvalidates a page throughPageSchema, whose componentpropertiesisz.record(z.string(), z.unknown())— an open bag — andComponentPropsMaphas zero code consumers in objectui (0 non-test hits underpackages/*/srcandapps/*/src;validateComponentPropsappears only in comments). Consequence, stated plainly: for a Studio-authored page, this PR's runtime refusal is the only refusal there is. The spec-layer refusal exists as a contract statement and reaches an author only where something parsesComponentPropsMap, which nothing in this repo does today. The refusal is a structured return from the pure function, reported by each renderer; no renderer throws.Pins — each named, red then green
Method for the red legs (run on the original commit
7d692df; the patch round changed only the pre-lookup call site and the exports, no resolution or refusal logic), both from the committed tree with an EXIT/INT/TERM trap restoring by absolute path, restore verified bygit diff HEADempty and blob hashes equal to HEAD's: leg A = both renderers restored from BASEd53e472(pre-fix) under the new function and the new pins; leg B = the ids arm ofresolveDeclaredActionIdsablated (byName.get(id)replaced byundefined, markerABLATION_7182_MARKER; on-disk confirmedgrep -cmarker = 1, original line = 0). The reviewer's independent leg C (objects arm removed, 7 red) confirms pin 3 is a genuine control. Tests resolve@object-ui/typesfromsrcthrough the root vitest alias, so the src mutation is what the tests read; the built components bundle is measured separately by the dist pin.actionsandactionNames; two-renderer refusal pin). The pre-fix bar failed insideActionEngine.registerActionon the bare string — the live defect, measured. HEAD: green.actionNamescase; two-renderer id case; plus 3 function cases = 13 red). HEAD: green.declaredActionIds.twoRenderers-7182.test.tsx: header control equals bar control (population inside the intersection of the two chains), header ids equal header control, bar ids equal bar control, header ids equal bar ids. Red in leg B, green on HEAD.Patch round (contract review PASS with amendments)
clientValidationclaim corrected above, with the consequence recorded.classifyDeclaredActionsorDeclaredActionsShapeto be public. The renderers' only pre-lookup need iskindandids, whichresolveDeclaredActionIds(elements, undefined)returns; both renderers now call that, the classifier and its type are module-internal, the barrel exports one function and two types, and the types test pins the classifier's absence from the barrel.origin/main1688986, merge commitb1d670b; none of the 19 files overlap this PR's files) — merge, not rebase, per the repo's no-force-push rule.Verification on HEAD
189f71c(union run after the patch commit)turbo run build --filter='@object-ui/plugin-detail^...', 11/11 —packages/types/distrebuilt WITHOUT the classifier (0 hits forclassifyDeclaredActionsindist/index.d.tsanddist/index.js),packages/components/dist/index.jscarries theactions refused at indexmarker.scripts/__tests__/one-authority-per-exported-name-6273.test.ts—Test Files 5 passed (5),Tests 42 passed (42), exit 0.OBJECTUI_DIST_PINS=1 vitest run --project dist—Test Files 1 passed (1),Tests 3 passed (3), exit 0.type-checkfor@object-ui/types,@object-ui/components,@object-ui/plugin-detail— exit 0 each, script lines echoed; the types test program lists the test file (--listFiles, 1 hit).anys).check:control-bytes✅ ·check:phantom-deps✅ ·check-changeset-presence✅ (8 source files of 3 released packages, 1 changeset) ·check-changeset-no-major✅. Earlier round on7d692df, unchanged by this diff:check:self-import✅ ·check:doc-fences✅ ·check:doc-types✅ ·check:action-forward-parity✅.check:readme-exports(unbuilt packages, population collapsed),check:doc-snippets(needs the full snippet build; the doc change is prose only),check:eager-closure(needs the console vite build; no new package edge). Repo-widepnpm lintand the full test farm are CI's.Not in this PR
record_headerarrays to ids) remainspm:blockedon the published artefact — not a rider.record:alert's single-id CTA lookup, a third hand-written copy of the by-name mechanism at arity one; filed as a finding, not touched here.Implemented by the objectui#7182 dev seat, session
https://claude.ai/code/session_01EMrWaQw3XS5DxTHxp4yRyC(written in prose because a PR edit degrades the footer form).🤖 Generated with Claude Code
Generated by Claude Code