fix(runner): render AppAction.items from its declared type — delete the two retired-key as any reads and make the onClick refusal true again - #7720
Merged
os-justin merged 2 commits intoSep 5, 2026
Conversation
…he two `as any` reads `LayoutRenderer` mapped a `type: 'user'` action's `items` and reached `(item as any).onClick` and `(item as any).shortcut` — past the declared element type, which is `AppMenuItem` and has neither key. The zod mirror parses the same array with the legacy eight-member `MenuItemSchema`, which strips both in silence, so no validated document could ever have supplied either. Deleting the two reads is what makes `AppActionSchema.onClick`'s retirement message true again: it tells an author "no renderer reads this key, so nothing could ever run it", and one did. The shared `handlerKeyRefusal` template is left alone — 22 other retired keys carry the same sentence, and the ruling asked for the claim to be made true, not softened. The rationale comments on `AppAction.onClick` and `AppActionSchema.onClick` said "nothing reads `AppComponentSchema.actions[]`". That was false in the other direction — the runner renders both the `'button'` and the `'user'` arm — so they now say what was measured: `actions[]` is read, `onClick` is not. Two pins, one per side of the claim: the runner drives a real menu with both undeclared keys authored and requires neither to be read; `packages/types` asserts the refusal message still carries the measured sentence. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
…out-renderer-retired-onclick
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
|
This was referenced Sep 5, 2026
os-justin
marked this pull request as ready for review
September 5, 2026 11:49
This was referenced Sep 5, 2026
os-justin
deleted the
claude/issue-6854-layout-renderer-retired-onclick
branch
September 5, 2026 12:04
This was referenced Sep 5, 2026
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 free
to 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 #6854
Executes the maintainer ruling recorded at #6854 comment 5548581017 (director seat, decision batch #40, maintainer verbatim 「同意」): option B2 — the runner stops reading a retired key through
as any,AppAction.itemsis not re-typed, and theshortcut-as-authorable question goes to its own card (filed: #7719).Verified at HEAD
96fcb35(origin/mainmerged in atf96a781).Zone 2 first — the census the ruling gated the deletion on
Result: empty. No such host exists in this repository, and none can. Four legs, each with a positive control:
AppComponentSchema.actions[]at all?.actionsacrosspackages/layout,packages/app-shell,packages/runner,apps/console, then classifypackages/runner/src/LayoutRenderer.tsx.layout'sAppSchemaRenderer.tsxcontains zero occurrences ofactions;app-shell'suseNavigationSync.tslikewise; every other hit isobjectDef.actionsor anaction:barnode — a different contractLayoutRendererhas one call site,App.tsx:204appcomes fromMetadataLoader, whose two implementations return animport.meta.glob'd.jsonmodule orres.json()— a JSON document, never a hand-built object.@object-ui/runnerpublishes no library entry (files: ["dist"]from a Vite app; nomain,moduleorexports), so nothing can import the component and hand it one eitheritemsarray whose members carryonClickorshortcutpackages/apps/examples/content/docs/e2eapp.jsoncarrying exactly that shape →CONTROL HIT … .actions[0].items[0],INSTRUMENT LIVEitems:array literal containingonClickorshortcutpackages/apps/examples,.ts+.tsxAppAction—components/src/__tests__/menu-item-onclick-handler.test.tsxandexamples/schema-catalog/test/component-fixture-declared-keys.test.ts, both the overlayMenuItempath (dropdown-menu/context-menu/menubar). Control: same pattern on a synthetic host file → matched,INSTRUMENT LIVELegs 3 and 4 are the ones that could have gone quiet, so both carry a control that fired. The
shortcutread in particular deleted no reachable behaviour: leg 3 found nobody writing the key, andMenuItemSchemastrips it from anything that is validated.The diff
packages/runner/src/LayoutRenderer.tsx— the twoas anyreads deleted, the block re-verified at the line numbers the ruling named (:307,:312-313on4ce14f1):OPEN/CLOSEmarkers instead of typed literally. Read the real bytes in the diff of this PR.The
type === 'separator'branch and theitem.labelread are legal againstAppMenuItemand stay. TheDropdownMenuShortcutimport goes with its only use. Casts in the file: 3 → 0 (the oneas anygrepstill finds is the word inside the new explanatory comment).packages/types— comment-only on published source. Machine-checkable: of the 29 changed lines inapp.ts+app.zod.ts,grep -vE '^[+-][[:space:]]*(\*|/\*|//|\*/)'returns nothing. No accepted key moves, no exported symbol moves, no shape changes — clause ② stays no.The retirement message: made true, not restated
The ruling's body offers two branches — "the retirement message is made true again (or restated to say the renderer no longer reads it)". This PR takes the first, and that is a deliberate reading worth reviewing.
handlerKeyRefusal(key, 'retired', …)generates, from a template inzod/tombstone.zod.tsshared by 22 other retired handler keys:Before this PR that sentence was false —
LayoutRendererread the key. After the deletion it is true, so the text is unchanged and is instead pinned, on both sides. Softening it (say, to "no renderer is intended to read this key") would have bought nothing and cost the property that makes it worth having: it is falsifiable, and a gate can hold it. It would also have rewritten 22 other keys' published messages and their pins to fix one key's claim.What is corrected in
packages/typesis a different sentence, one the deletion does not make true. Both rationale comments on this key said:That is false in the other direction:
LayoutRendererrenders both the'button'and the'user'arm ofactions[]. They now say what was measured —actions[]is read,onClickis not, on the action or onitems[].Pins, and the ablation that shows they can fail
Both were run mutation-then-restore, from the committed tree; each restore is proven by blob hash and an empty
git diff HEAD, not by an exit code, and both scripts carry atrap … EXIT INT TERM. No build is involved: the root vitest config aliases@object-ui/*tosrc, sodistis not on the resolution path for either.A —
packages/runner/src/__tests__/LayoutRenderer.appActionItems-6854.test.tsxdrives a real user menu with both undeclared keys authored, exactly as a validator-bypassing host would, and requires neither to be read.Re-inject the two reads →
blob 7e45da19 → 3b99c550, and:Only the two negatives fail; the two positive controls in the same file (the declared
labelrenders, the declaredtype: 'separator'still renders a separator) stay green — so the pin discriminates rather than just detecting a broken render. Restored: blob back to7e45da19,git diff HEADempty,4 passed (4).B —
packages/types/src/__tests__/app-action-onclick-refusal-6854.test.tsasserts the measured clause survives, that the refusal is addressed toonClickwithcode: 'custom', and that the parse message and the.describe()metadata are still one string. It also pins the premise: the legacy mirror stripsonClickandshortcutfrom an item rather than refusing them, which is why no author could ever have fed the cast.Soften the shared clause in
tombstone.zod.ts→blob 8eaed046 → 4ff4ba1f, and:Tests 2 failed | 4 passed (6)— redhandler-keys-json-refusal-6124.test.ts(pre-existing)Tests 272 passed (272)— greenThat second row is the point: the clause this card measured had no guard at all before this PR. Restored: blob back to
8eaed046,git diff HEADempty, both pins278 passed (278).Gates
Exit codes captured before any pipe (
cmd > log 2>&1; EXIT=$?), all at HEAD96fcb35, heavy runs through the container's shared verify lock (VERDICT command-exitread, never a bare$?).pnpm exec vitest run packages/types/Test Files 112 passed (112)·Tests 1931 passed (1931)pnpm exec vitest run packages/runner/Test Files 4 passed (4)·Tests 17 passed (17)pnpm --filter @object-ui/types type-checktsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.jsonpnpm --filter @object-ui/runner type-checktsc --noEmit && tsc -p tsconfig.test.jsonpnpm --filter @object-ui/types build.d.tswas readnode scripts/check-changeset-presence.mjs✅ 5 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-fixed.mjs✅ All workspace packages are in the changeset fixed group.node scripts/check-changeset-no-major.mjsnode scripts/check-control-bytes.mjs✅ check-control-bytes: OK (scanned 6294 tracked text file(s); skipped 85 binary)node scripts/check-vi-mock-specifiers.mjs·check-vi-mock-inherit.mjs·check-unreferenced-sources.mjspnpm exec eslint . --format jsonNavItem'sanyprops, twoset-state-in-effect,MenuItemSchematyped as ZodType-of-ANY), and the deletion removed threeno-explicit-anywarnings rather than adding anyBoth new test files were confirmed to be inside their package's type-check project by
tsc -p tsconfig.test.json --listFiles, rather than assumed to be.@object-ui/typesis a dependency of all 43 workspace packages, soturbo ls --affectedlists every one of them. Their suites were not run locally and are left to CI; the narrowing is declared, and the ground for it is the comment-only proof above — no behaviour, type shape or zod shape moved inpackages/types, so no consumer's verdict can move.Deviations, declared
handlerKeyRefusaltemplate is unedited. The ruling's title asks for the message to be corrected "so it no longer claims no renderer reads this key"; its body and the dispatch both allow the other branch — make the claim true — which is what deleting the cast does. Reviewer's call if the title was meant literally; ablation B shows the sentence is now guarded either way.handler-keys-json-refusal-6124.test.ts. That census is scoped to the nine#6124mirror files andapp.zod.tsis not one of them; addingAppActionSchema.onClickwould move its asserted22retired /66total counts, which belong to that card. The pre-existing file is re-run here unchanged and stays green.packages/typesdoc comments were corrected beyond the literal instruction, because they carried a claim the deletion does not make true (above). Comment-only; no shape moves.Out of scope, filed and reported
shortcutbecome authorable onAppAction.items? — the one capability question left after #6854's B2 ruling #7719 — the ruling's required follow-up: shouldshortcutbecome authorable onAppAction.items? Unassigned, unlabelled, with the consumer reading and both instrument controls attached. Dedup: MCPsearch_issuesreturned 0 for the question; a control query in the same session returned contract: shouldAppAction.itemsbe re-typed to the overlayMenuItem? — the runner reads two overlay fields throughas anytoday #6854, The menu renderers read an undeclareditem.onSelect, while the declaredMenuItem.onClickis read by nothing #6346 and finding(types,examples): every toast demo hangs an action object offonClick, which is declared as a function and read by no dispatcher #6250 (open and closed), so the zero is a reading.AppComponentSchema.actions[](AppAction) has no reader — layout, app-shell and the console never render it, whilecore/app-schema.mdx"Global Actions" teaches it as the app's global toolbar buttons #7469 (open,pm:queue, p3) rests on a premise this work falsifies: it records thatAppComponentSchema.actions[]has no reader, and triage endorsed retiring the whole surface as the default direction on that basis. The runner renders it. Both censuses asked for the type NAMEAppAction, whichLayoutRenderernever spells — it types its propAppComponentSchemaand readsapp.actionsstructurally. The measurement is attached there as comment 5551453088; no label, state or assignee touched..changeset/7344-handler-string-any-mirrors.md:32still carries the same false parenthetical — "AppAction.onClick(nothing readsAppComponentSchema.actions[])" — and will otherwise ship as published CHANGELOG copy. It belongs to another card's pending changeset, so it is reported rather than edited here.Drafted by the
domain:spec@ objectui dev dispatch, sessionhttps://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s— recorded here in prose as well, because a markdown-link footer is not measured to survive an edit of this body.🤖 Generated with Claude Code
https://claude.ai/code/session_01BAZFhALsQsGqxui8sNqM8s
Generated by Claude Code