Skip to content

feat(types,runner)!: retire the app node's actions array, AppAction and AppActionSchema (objectui#7469) - #10761

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-7469-retire-app-actions
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-7469-retire-app-actions

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #7469
Clause-②: yes

This PR carries out the maintainer's ruling C on objectui#7469 (recorded in comment 5828532177). The maintainer chose 「C 退役 actions[] (Recommended)」: "one channel, one contract". App-level actions are navigation items of type: 'action'. The free-form actions array on the app node, AppAction, AppActionSchema and the runner's rendering of them are retired.

The work was done by the domain:spec#1 dispatch under claim 5852900400, session https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW.

What changed

@object-ui/types (contract face).

  • AppComponentSchema.actions is now actions?: never on the TS face and retirementTombstone(APP_ACTIONS_REFUSAL) on the zod mirror. An authored array is refused with ONE invalid_type issue at actions. The message names the remedy in the spec's own spelling: navigation: [{ id, type: 'action', label, actionDef: { actionName } }].
  • AppAction (TS) and AppActionSchema (zod) are deleted, together with their exports from . and ./zod. The unexported AppActionSchemaType is deleted too.
  • AppMenuItem / MenuItemSchema stay, because the legacy menu still reads them (H3). The objectui#7719 shortcut refusal on them is unchanged in behaviour. Its guidance text and docblock named AppAction.items / AppAction.shortcut, and this change would have made that text false, so it now names the menu item instead.

@object-ui/runner. LayoutRenderer no longer reads the app's actions. The 'button' arm, the 'user' avatar-menu arm with its items, and the Bell's actions gate are gone. The chrome now has one rule: the Bell is always drawn, which is what an app with no 'button' action got before. The now-unused dropdown and avatar imports are removed.

Docs.

  • content/docs/core/app-schema.mdx "Global Actions" is rewritten to teach a navigation item of type: 'action'. The example is spec-valid. The section says the console sidebar dispatches the item by action name and that the runner renders no navigation items.
  • Both actions: examples on that page are removed, along with the "keyboard shortcuts" best practice (that key retired with AppAction).
  • content/docs/guide/schema-overview.md loses its two actions examples and the user-menu bullet.

Changeset. .changeset/7469-app-actions-retired.md declares minor for @object-ui/types and @object-ui/runner. The runner is published ("private": false) and is in the fixed group. There is no major.

Mechanism hypotheses, measured

  • H1 was confirmed on BASE 01700dcd7 (the declarations and the three runner arms were where the dispatch said), and it was re-read before editing.
  • H2: AppComponentSchema extends .passthrough() BaseSchema, so a deleted member would be KEPT in silence. The tombstone route follows from that. Block (d) of the new pin reads it: AppMirror.omit({ actions: true }) parses the array green and keeps it.
  • H3: AppMenuItem has another reader (AppComponentSchema.menu, menuItemToNavigationItem, the runner's sidebar), so it stays. No menu-item type was used only by AppAction.
  • H4: the runner's LayoutRenderer.appActionItems-6854.test.tsx was retired with the arms. The new rule is pinned in LayoutRenderer.chrome-7469.test.tsx.
  • H5: in objectstack origin/main e2c4e12, 0 of the 4 app-bearing files that carry navigation: write actions:. The control is that the same actions: [ matcher does hit one file under examples/. In objectui, the only actions: on an app node were two docs pages, the runner pin and one types fixture. All are removed or re-pointed; the new retirement pin keeps one on purpose, under @ts-expect-error.
  • H6: objectstack origin/main e2c4e12 AppSchema is a strictObject whose body has no actions (0 hits; the control navigation has 26). On the installed spec 17.4.0, AppSchema.safeParse of an app with actions gives unrecognized_keys [actions], and the same app with a navigation action item parses green. Pin block (c) covers this. There is no spec change.

Verification (final head 52e5d79278, merge of main at 704e05b09)

  • pnpm exec vitest run --maxWorkers=2 packages/types/ packages/runner/ passed: 250 files, 5351 tests, lock VERDICT command-exit 0.
  • The type-checks passed, lock VERDICT command-exit 0, each against a freshly rebuilt dependency closure:
    • pnpm --filter @object-ui/types type-check (three programs);
    • pnpm --filter @object-ui/runner type-check;
    • pnpm --filter @object-ui/app-shell type-check.
  • Consumer direction (downstream): @object-ui/app-shell was type-checked green. Its one file naming AppComponentSchema (useNavigationSync.ts) passed its test (1 file, 12 tests). The nav-action dispatch and layout suites also passed (useNavActionDispatch.test.tsx plus layout/__tests__/: 36 files, 250 tests).
    • The census found no importer of AppAction / AppActionSchema outside packages/types. The census test asserts this.
    • The full app-shell vitest run is left to CI.
  • Cross-package reverse check: a temporary runner test that authors actions on an AppComponentSchema literal failed tsc -p tsconfig.test.json with TS2322 ("not assignable to type 'undefined'"). That proves the runner compiles against the rebuilt .d.ts. The file was removed afterwards and the tree was clean.
  • Ablation (committed first, reverted to the BASE blobs verified by git hash-object, restored from HEAD, and git diff HEAD read empty after each leg):
    • Types leg: the new pin went 6 failed / 8 passed. tsc -p tsconfig.test.json raised TS2578 twice (on the AppAction import directive and the actions directive) and TS2344 on the Equal assertion.
    • Runner leg: the chrome pin went 4 failed / 2 passed, and both controls stayed green.
  • Gates, each exit 0 on 52e5d79278:
    • Changesets: check-changeset-presence, check-changeset-no-major, check-changeset-fixed and check-changeset-overwrite. The report-only gates check:changeset-claims and check:pending-changeset-literals ran too.
    • Tree-wide checks: check:new-line-citations (0 new), check:control-bytes, check:spec-symbols, check:test-path-roots, check:handler-key-reads, check:readme-exports, check:phantom-deps, check:unreferenced-sources, check-type-check-coverage and check:prompt-keys.
    • Docs: check:doc-types, check:doc-snippets (676 of 676 blocks), check:doc-examples, check:doc-fences, check:doc-example-ids, check-doc-expression-carriage, check-doc-links and check:doc-example-readers.
    • check-governed-queue-guard --test over the 18 diff paths: NOT GOVERNED.
  • ESLint --no-inline-config on the 13 touched TS/TSX/MJS files (the file count is read from --format json): 4 errors and 9 warnings. The per-file, per-rule tally is identical to the same files at the merge base, so there are 0 differences. The 4 errors are pre-existing sites that carry inline disables. The config is not type-aware (no parserOptions.project or projectService), so this diff cannot change the verdict on any untouched file.
  • NOT MEASURED: Spec Main Shape Gate. Reason: it needs a spec tarball built from objectstack main and injected into the virtual store, which is CI's job on this PR. The one coordinate this diff moves on a spec-derived type is actions, and objectstack main e2c4e12 does not declare it (H6), so the gate compiles this change the same way against main as against the pin.

Acceptance notes

  • Surface beyond the claim's file list, needed by the retirement. These edits are forced by deleting the types:
    • app-action-onclick-refusal-6854.test.ts is deleted. It pinned AppActionSchema.onClick only.
    • app-menu-item-shortcut-refusal-7719.test.ts is re-pointed. Its control on AppAction.shortcut is dropped, its message assertion now names AppMenuItem, and its authoring-position block runs through AppComponentSchema.menu.
    • handler-keys-string-any-mirrors-7344.test.ts is updated:
      • the RETIRED ledger goes from 4 to 3 sites (9 in total). The whole schema left, so no key was reclassified in place.
      • the objectui#7721 reader census now asserts ZERO readers, with the former reader as the positive control.
      • the plain-z.object deletion probe went with its only instance. The same strip is still measured on MenuItemSchema in the 7719 pin.
    • component-docs-retired-handler-keys-7340.test.ts records the census move from 27 to 26, and app.ts leaves it.
    • zod-mirror-parity.test.ts loses only this pair's rows and imports. EXPECTED_MIRROR_PAIRS goes from 160 to 159, per its own instruction.
    • phase2-schemas.test.ts swaps its actions fixture for a navigation action item.
    • scripts/check-doc-component-types.mjs gets one DOC_TYPE_EXEMPTIONS entry, action on core/app-schema.mdx, because it is a NavigationItem kind and not a node. The script's 18 suites passed: 689 tests, exit 0.
  • Pending changesets that now describe AppAction as it was. check:changeset-claims (report-only) lists .changeset/7760-lazy-mirror-input-type-args.md, which names the deleted 6854 test file. The 6854, 7344, 7719 and 7721 entries also describe AppAction. They were not edited, because check-changeset-overwrite guards other cards' changesets. This PR's changeset says in its own body that they predate the retirement.
  • Runner scope after the ruling. The standalone runner draws the legacy menu only and renders no navigation item. After this change it has no way to show app-level actions. That is the ruling's stated cost (「独立运行器失去头部按钮和用户菜单」), and the docs say it in plain words.
  • Observation, not filed. The runner's sidebar NavItem reads a menu item's path, label, icon and children. It does not read the declared AppMenuItem.href ("External Link"). app-shell honours href through menuItemToNavigationItem. This comes from reading the source only; no producer or public entry point was measured. Carrier: none.
  • The stale branch claude/issue-7469-app-shell-global-actions was not touched.

Generated by Claude Code

…nd AppActionSchema

Maintainer ruling C: app-level actions are navigation items of type
'action'. The app node's actions member becomes a named refusal on both
faces (?: never + retirementTombstone, since BaseSchema's passthrough
would otherwise keep an authored array), AppAction and AppActionSchema
leave the published exports, and the standalone runner's header stops
reading the array: no per-action buttons, no avatar menu, and the Bell
is unconditional.

Pins that read the retired surface are retired or re-pointed, the
AppActionSchema pair leaves the mirror-parity ledger, and the docs teach
navigation action items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
…a page

The rewritten "Global Actions" section teaches a navigation item of
type 'action'. That value is a NavigationItem kind, not a rendered node
type, so check:doc-types declares it beside the page's existing `item` /
`group` menu-kind exemptions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
Brings in the queue's landings before the pull request opens, including
the other zod-mirror-parity ledger edit (objectui#7759 group A).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014fWVhLzhxR8qrFsJ5o8TYW
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 32 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7760-lazy-mirror-input-type-args.md

  • names packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts → packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts — ⚠️ this change leaves no such file

    This PR hit the second vector inside the repo. AppActionSchema.items is z.array(MenuItemSchema), so its element type went from any to AppMenuItem, and packages/types/src/__tests__/app-action-onclick-refusal-6854.test.ts — which asserted result.data as { items: RecordANGLE-BRACKETS(string, unknown)[] } on a parse result — stopped compiling with exactly that TS2352, and was repaired to route the assertion through unknown. That is the one site in this workspace that needed a repair; every other package was rebuilt and type-checked against the change with nothing further to fix. Consumers of @object-ui/types/zod outside this repo should expect repairs of both shapes, not only the assignment one.

.changeset/4895-retire-block-schema-family.md

  • names phase2-schemas.test.ts → packages/types/src/__tests__/phase2-schemas.test.ts — edited by this change

    The zod half is the one that mattered. AnyComponentSchema.safeParse({ type: 'block-library' }) returned success on 17.6.0 for a node no page can render, so an author who copied the documented shape was told green by the shipped validator and then got the registry's OBJUI-001 "Unknown component type" panel. Validated-then-broken is worse than never-validated, because the green light is what the author trusted. All five discriminants — block, block-library, block-editor, block-instance, component — are now refused, pinned in phase2-schemas.test.ts alongside the theme refusals retired the same way.

  • names zod/app.zod.ts → packages/types/src/zod/app.zod.ts — edited by this change

    ⚠️ Not this family, and not touched. The live slotted record-page vocabulary — PageNodeSchema.kind === 'slotted' with slots?: PageSlotMap (packages/types/src/layout.ts), rendered by usePageAssignment / PageBlockCanvas / PageBlockInspector in @object-ui/app-shell — shares the words "block" and "slot" with the retired family and shares no declaration, type or file with it. Neither is the type: 'component' NAVIGATION item kind ({ type: 'component', componentRef }, NavigationItemSchema in zod/app.zod.ts), a different declaration in a different module.

.changeset/5903-objectgantt-declared-keys.md

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743); the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair — but at that time the ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction, not that nothing was there. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger requires every registered pair's mirrored-but-undeclared key set to equal that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/5928-classname-style-props-rename.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Where the non-pair is recorded now. zod-mirror-parity.test.ts keys its existing EXCLUSIONS entry — the mechanism that accounts for every exported const with no TypeScript declaration to mirror, each with its stated reason — to ClassNameStylePropsSchema. Named for its own two keys, the const leaves no like-named declaration for a name-derived pairing to reach for.

.changeset/6051-gantt-flat-config-declared-keys.md

  • names packages/types/src/index.ts → packages/types/src/index.ts — edited by this change

    GanttConfig itself gains nine members and is a published type, exported by name from packages/types/src/index.ts: lockField, objectField, summaryExtent, defaultCollapsedDepth, borderColorField, dependencyTypes, timeZone, exportFileName, interactions. The entry file's diff is empty only because the export list already named the type — the widening happened at the declaration.

  • names packages/types/src/__tests__/zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⚠️ The JUSTIFICATION for that parity clause was retired (objectui#9743) — the same retirement objectui#5903's entry carries, for the same clause and the same reason; the clause itself stands and did not move. As first written it credited the zod-mirror-parity ratchet with a zero-drift reading for this pair, at a time when that ratchet measured three directions and was structurally blind to the MIRRORED-but-undeclared one (objectui#9711), so a zero from it recorded that it had not looked in that direction. objectui#9725 landed the fourth direction, and it covers this pair BY NAME: packages/types/src/__tests__/zod-mirror-parity.test.ts registers objectql.zod.ts#ObjectGanttSchema in both its mirror map and its declaration map; assertionMirroredUndeclaredMatchesLedger reconciles every registered pair's mirrored-but-undeclared key set against that pair's MirroredUndeclared ledger entry — never for a pair the ledger does not name — and assertionNoVacuousMirroredUndeclaredMeasurement refuses a measurement that has degenerated to any. ⛔ Read this pair's verdict off that reconciliation, which re-derives it on every run, rather than off any figure written here; when this paragraph was authored, on 2026-09-18, it required no MirroredUndeclared entry for the pair.

.changeset/6150-undeclared-but-consumed-keys.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ AMENDED, and the amendment ships in this same release. objectui#7804's TreeViewSchema slice gave the key a zod arm after all — a NAMED REFUSAL (handlerKeyRefusal(key, 'runtime-slot', label)), never a shape — because "no mirror entry" is not neutral under BaseSchema.passthrough(): it meant an authored { "type": "tree-view", "onNodeClick": { "action": "toast" } } parsed GREEN, survived the parse, and reached a call site that expects a function. ⇒ the three clauses this bullet used to carry are no longer true of the code shipping beside it. The key is now a MEMBER of TreeViewSchema.shape and an authored value is refused BY NAME at path onNodeClick; it has LEFT zod-mirror-parity.test.ts's RuntimeOnlyDeclared for that file's KnownDrift; and it is no longer "the first pair to sit there without also sitting in UnmirroredDeclared" — draining it emptied that difference, so RuntimeOnlyDeclared is now a SUBSET of UnmirroredDeclared and the union of the two equals UnmirroredDeclared itself. ⛔ objectui#6152's ruling is untouched by any of this: what the key still does not have, and never will, is a z.function() shape — no serialized document could satisfy one.

.changeset/6175-column-state-persistence.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Nothing is retired. Both spellings remain declared on DataTableSchema; onColumnReorder stays declared and stays unwired, exactly as the RuntimeOnlyDeclared ledger in zod-mirror-parity.test.ts records it. Which of the two survives is a declared-surface ruling that stays open and is deliberately not settled here.

.changeset/6349-types-internal-name-collisions-batch-2.md

  • names app.ts → packages/types/src/app.ts — edited by this change

    MenuItem — renamed to AppMenuItem in app.ts. app.ts declared a flat, all-optional interface (type?: 'item' | 'group' | 'separator', label, icon, path, href, children, badge, hidden) — the @deprecated legacy navigation item that AppComponentSchema.menu, AppAction.items and menuItemToNavigationItem read. overlay.ts declared a discriminated union, MenuCommandItem | MenuDividerItem, whose command arm requires label and whose both arms tombstone type as type?: never (objectui#6523) — precisely the key app.ts declares as a three-value enum. Re-pointing either at the other would have made an authored type: 'separator' legal on one side and a type error on the other, so the two names had to part. @object-ui/types continues to publish overlay's union as MenuItem and app's interface as AppMenuItem, exactly as before.

.changeset/6639-objectgrid-title-mirrored.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The gain is the typed refusal: the mirror's .passthrough() base was already admitting any title unexamined, and it now enforces the declared string. zod-mirror-parity.test.ts's UnmirroredDeclared ledger records the key as worked off — the ledger's first shrink by repair (97 + 1 mirrored + 23 reclassified is what the seeded "121" now means).

.changeset/7113-chart-data-model.md

  • names app.zod.ts → packages/types/src/zod/app.zod.ts — edited by this change

    .extend() with a NEW key still works and preserves the fold and the refinement; .optional(), z.discriminatedUnion, z.toJSONSchema and safeValidateSchema are all unaffected. Nothing in this repository calls the throwing combinators on either const, and the published surface already ships refined mirrors (objectql.zod.ts, complex.zod.ts, form.zod.ts, app.zod.ts), so the class is not new — but it is a real behaviour change on a published export and it belongs in the release note rather than in a reviewer's file.

.changeset/7344-handler-string-any-mirrors.md

.changeset/7654-floating-chatbot-trigger-icon-tombstone.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Every other tombstone in this package pairs ?: never with a retirementTombstone() refusal on the Zod twin. There is no twin here to carry one: FloatingChatbotConfig has no Zod mirror at all, and floatingConfig sits in the UnmirroredDeclared ledger (zod-mirror-parity.test.ts, complex.zod.ts#ChatbotSchema). BaseSchema is .passthrough(), so the whole floatingConfig object rides through unvalidated — before this change and after it. Minting a mirror to host a refusal would be the declared-but-UNMIRRORED axis (objectui#6152), a different defect: a key can be mirrored and inert, or unmirrored and live, and fixing one says nothing about the other. This change does not widen into it.

.changeset/7694-chart-series-chart-type-alias-refusal.md

  • names zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    • Before: series: [{ name: 'revenue', chartType: 'line' }] validated green through @object-ui/types/zod (safeValidateSchema, objectui check / objectui validate, any pipeline that keeps parse()'s output) — and the key was gone from the output, so a consumer of the parse result drew that series in the chart's own family, precisely what the author was overriding. On the TypeScript face the key was merely an excess property on a fresh literal; a widened object carrying it assigned structurally. - After: the same document REFUSES at series[i].chartType (issue code invalid_type) with one message on both channels — the parse-time issue and the .describe() metadata: Unrecognized key(s) on this chart series: \chartType`. Did you mean `chartType` → `type`? …followed by the reason and the remedy. Writetype: 'bar' | 'line' | 'area'. On the TypeScript face ChartDataSeries.chartTypeis a?: nevertombstone, so both the fresh literal and the widened assignment aretsc errors. - **Both written** ({ type: 'bar', chartType: 'line' }) is refused at chartTypealone — the key is not folded ontotypeand no precedence is minted between the two spellings. - **Which documents to scan.** The narrowing does not stop atChartDataSeriesSchema; it reaches every document through the parents that embed it — ChartSchema.series (zod/data-display.zod.ts, z.array(ChartDataSeriesSchema)) and, one level further out, ReportSectionSchema.chart (zod/reports.zod.ts, ChartSchema.optional()). Authors meet it through safeValidateSchema() (zod/index.zod.ts, which parses AnyComponentSchema) and through the CLI's objectui validate command (packages/cli/src/cli.ts). In practice: every chartnode'sseries[], and every report section whose chart` carries one.

.changeset/7719-app-menu-item-shortcut-named-refusal.md

  • names LayoutRenderer.tsx → packages/runner/src/LayoutRenderer.tsx — edited by this change

    ⛔ LayoutRenderer 未恢复任何读点,objectui#6854 的那枚 pin 断言原样保留;本次只修了它与 LayoutRenderer.tsx 里已经过期的散文(两处都在把一个已裁决的问题描述成悬而未决)。 @object-ui/runner 的改动仅为注释,无任何已发布行为变化。

.changeset/7735-zod-mirrors-stop-authoring-defaults.md

  • names app.zod.ts → packages/types/src/zod/app.zod.ts — edited by this change

    What changed. All 41 .default() call sites under packages/types/src/zod/ are removed — layout.zod.ts 22, crud.zod.ts 11, form.zod.ts 5, views.zod.ts 2, app.zod.ts 1. @object-ui/components reconciles the third face objectui#8229 found: flex's registration defaultProps.align seeded 'center', the value its own renderer never applies, so a designer-made node laid out differently from a hand-authored one; it now seeds 'start'.

.changeset/7804-tree-view-handler-slot.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The pair moves from zod-mirror-parity.test.ts's RuntimeOnlyDeclared to its KnownDrift, which empties the former of the one entry the latter did not also hold — so the two unmirrored ledgers are now in a containment relation, and the cross-ledger figure that recorded their difference states the containment instead.

.changeset/7917-export-breadcrumb-object-tree-zod-schemas.md

  • names index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    AnyComponentSchema declares 107 node component types. 105 of them could be named on the ./zod barrel — ButtonSchema.safeParse(node), which is what a designer, a form builder or a targeted test needs. The arms declaring type: 'breadcrumb' (navigation.zod.ts) and type: 'object-tree' (objectql.zod.ts) could not: both were already export const in their own module, but index.zod.ts — the package's only zod entry point — did not re-export them, so the schemas existed, were maintained, and were applied by the union while no consumer could name them.

.changeset/7918-zod-lazy-getter-identity.md

  • names app.zod.ts → packages/types/src/zod/app.zod.ts — edited by this change

    ⚠️ Also settled while locating the ten: AppMenuItemSchema has no declaration of its own — it is the barrel alias of app.zod.ts's MenuItemSchema, while the barrel's own MenuItemSchema is overlay.zod.ts's. Two different schemas, so the list really is ten entries and not nine.

.changeset/8114-detail-tab-activity-timeline.md

  • names scripts/check-doc-component-types.mjs → scripts/check-doc-component-types.mjs — edited by this change

    Which type replaces it was read off the registry rather than guessed, using the repository's own derivation — deriveRegistryKeys() from scripts/check-doc-component-types.mjs, the 649-key universe that gate judges against:

.changeset/8338-retire-toast-action.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    The parity ledgers drain with it, every figure re-derived by zod-mirror-parity.test.ts's own AST and mirror instruments rather than stepped by hand: KnownDrift 42 entries / 64 keys → 41 / 63 (the entry's whole content, so the entry went too — the ledger's first loss by RETIRING a key rather than by moving either face toward the other), and WiderThanDeclared 23 / 36 / 47 arms, split 6 / 30 / 0 / 11 → 22 / 35 / 45, split 6 / 29 / 0 / 10. The pair itself stays registered, so EXPECTED_MIRROR_PAIRS does not move.

.changeset/8344-node-recursion-point-redirect.md

  • names zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    Two mechanical notes for anyone editing the wiring. AnyComponentSchema is built in zod/index.zod.ts from all 13 category modules and 14 modules import zod/base.zod.ts, so the arm cannot be an import — z.lazy defers evaluation, not the module graph. It is a written option slot that index.zod.ts fills inside AnyComponentSchema's own initializer, and it is a z.union option rather than a z.lazy holder because z.lazy memoises its getter: a holder would let whichever module graph parsed first decide the accept set for the whole process. Both constraints are measured, and the reasoning lives on defineNodeComponentUnion in zod/base.zod.ts.

  • names index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    Two mechanical notes for anyone editing the wiring. AnyComponentSchema is built in zod/index.zod.ts from all 13 category modules and 14 modules import zod/base.zod.ts, so the arm cannot be an import — z.lazy defers evaluation, not the module graph. It is a written option slot that index.zod.ts fills inside AnyComponentSchema's own initializer, and it is a z.union option rather than a z.lazy holder because z.lazy memoises its getter: a holder would let whichever module graph parsed first decide the accept set for the whole process. Both constraints are measured, and the reasoning lives on defineNodeComponentUnion in zod/base.zod.ts.

.changeset/8499-node-slot-registered-arms.md

  • names scripts/check-doc-component-types.mjs → scripts/check-doc-component-types.mjs — edited by this change

    The defect, and the direction it ran. Nine type spellings sat at DECLARED node slots in this repository's own corpora and resolved in no arm of the component union. Eight were registered renderers with fixtures proving they draw; the ninth (my-component) is the reader's own plugin component and carries a written exemption in scripts/check-doc-component-types.mjs. A reader following content/docs/utilities/runner.mdx's own instruction — "copy one, wrap it in a page document … and save it as src/app-data/pages/index.json" — got a document that renders correctly in the browser and is refused by objectui validate. That is the expensive direction: the likely reaction is to stop trusting the validator.

  • names zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    Why 49 is acceptable here where 2 was a defect: the 2 were data blocks a consumer had a standing reason to validate on their own, and objectui#7917 (PR feat(types): re-export BreadcrumbSchema and ObjectTreeSchema from the zod barrel (objectui#7917) #8777, open at the time of writing, and the holder of zod/index.zod.ts) exists to export exactly those. The 47 added here are HTML primitives and two input aliases — they have no per-tag consumer to serve, and exporting a SemanticElementSchema / HtmlElementSchema pair would publish a NAMED authoring surface (z.enum families, not per-tag schemas) that this card's ruling does not cover: the ruling is "arm the registered renderers", not "add public exports to @object-ui/types". So the metric is left to move and said out loud instead. ⇒ Whoever next runs that measurement should expect 49, and whoever wants the number back down should treat naming these families as its own decision. If PR feat(types): re-export BreadcrumbSchema and ObjectTreeSchema from the zod barrel (objectui#7917) #8777 lands first, the same movement reads 0 to 47.

.changeset/8572-chatbot-body-retired.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Why this key and not another. It was the ONE place in this vocabulary where body did not mean "what goes inside this component": zod-mirror-parity.test.ts carried the pair under KnownDrift as "two different meanings of one key", and the same collision was the whole reason chatbot was the single arm of the component union whose output was not assignable to SchemaNode. Both ledger rows move with this change, and the two pins that recorded the old state are INVERTED rather than deleted (see below).

.changeset/8598-zod-subpath-single-bundled-module.md

  • names src/zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    The defect. This package declares "sideEffects": false, and src/zod/index.zod.ts fills the node recursion point as the initializer of its AnyComponentSchema const. tsc emitted that barrel as a module whose only other content is re-exports — so a bundler resolving import { CardSchema } from '@object-ui/types/zod' followed the re-export to dist/zod/layout.zod.js, needed nothing from the barrel's own body, and the flag let it drop that body whole. The fill went with it, and every child slot then validated against the pre-spec(types): redirect the node recursion point from BaseSchemaCore to AnyComponentSchema — measured at 9 newly-refused documents, and it drops 118 phantom strict refusals #8344 BaseSchemaCore arm — the ~21 base keys and nothing type-specific — with no error, no warning and no way for the guard inside the dropped code to notice. objectui#8344 shipped that window DECLARED and pointed here to close it.

.changeset/9067-zod-barrel-named-arms.md

  • names packages/types/src/zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    Two new exported symbols, both on packages/types/src/zod/index.zod.ts:

.changeset/9092-inline-locale-declared-face.md

  • names app.ts → packages/types/src/app.ts — edited by this change

    • AppComponentSchema.label (app.ts) - ObjectGridSchema.label and .description (objectql.ts) - PageNodeSchema.aria.ariaLabel (layout.ts)

.changeset/9187-record-highlights-layout-two-values.md

  • names phase2-schemas.test.ts → packages/types/src/__tests__/phase2-schemas.test.ts — edited by this change

    ⚠️ The census behind this narrowing covers this repository only, and it found no in-repo authoring to migrate: every in-tree layout: 'grid' belongs to a different component (detail-view in content/docs/api/schema-reference.md and phase2-schemas.test.ts, ai-recommendations in packages/plugin-ai/README.md), and the one in-repo consumer of this interface that writes a layout (p1-spec-alignment.test.ts) writes 'horizontal'. So no document in this repository stops type-checking. A TypeScript consumer outside this repo that wrote grid is not observable from here and gets a compile error (TS2322) naming the key — which is why the FROM/TO is spelled out above.

.changeset/9511-record-id-is-a-string.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    ⭐ The declaration alone would not have been enough, and this is the reusable part. A TypeScript declaration does not run at parse time. The hand-written zod mirror is the only face in this repository that can refuse an authored number, so narrowing the declaration without the mirror would have shipped declared !== enforced on a published surface — and zod-mirror-parity.test.ts would have stayed GREEN through it, because that instrument asserts a mirror accepts everything its declaration declares and a mirror left WIDER passes. Both faces moved together for that reason.

.changeset/9533-dashboard-bare-key-converge-plugin-namespace.md

  • names scripts/check-doc-component-types.mjs → scripts/check-doc-component-types.mjs — edited by this change

    What changed. The renderer registers as plugin-dashboard:dashboard, the namespace every sibling plugin, both console stubs and the CLI whitelist already use, so the console stubs are cleared on load and all three claimants of the bare key name ONE full type. The retired view:dashboard key answers RetiredDashboardNodeTombstone — an inline refusal, plus a console.error carrying the same text — registered with skipFallback: true so it claims no bare key. Its spelling is withheld from the derived key universe by declaration in scripts/check-doc-component-types.mjs, the same disposition the RETIRED_FIELD_TYPES tombstones take, so objectui check names it in a file it recognises rather than blessing it; packages/cli/src/utils/known-schema-types.ts regenerates and loses that one entry.

.changeset/9659-node-recursion-point-inert-clause.md

  • names zod/index.zod.ts → packages/types/src/zod/index.zod.ts — edited by this change

    Prose amended where this change falsified it, ⛔ not rewritten. zod/index.zod.ts said defineNodeComponentUnion "wraps it rather than replacing it" — true only while a wrapper existed. zod/base.zod.ts's note on the loose parameter bound still names ChatbotSchema's record body in the present tense; the paragraph is kept for the reason it records and carries an AMENDED note saying the exclusion set that pin reads is now empty.

.changeset/action-callback-retired-7068.md

  • names phase2-schemas.test.ts → packages/types/src/__tests__/phase2-schemas.test.ts — edited by this change

    What was measured, on this branch's base (900f8d99). ActionCallback ({ type: 'toast' | 'message' | 'redirect' | 'reload' | 'custom' | 'ajax' | 'dialog', message?, url?, api?, method?, dialog?, handler? }) was declared in crud.ts, mirrored in zod/crud.zod.ts, re-exported by both barrels, and carried on the legacy ActionSchema as onSuccess? / onFailure?. Producers: the package's own phase2-schemas.test.ts fixture and three ts fences in content/docs/core/enhanced-actions.mdx — nothing else (git grep -l ActionCallback over packages content skills hit the five packages/types files; positive control SchemaNodeSchema hit 22). Runtime readers: none — ActionRunner imports UIActionSchema, never this interface, and its own ActionDef.onFailure is a different (runner-native) meaning. It was the THIRD meaning of one key: objectui#5934 had already retired the runner's callback meaning of onSuccess and converged it on the spec's block.

  • names content/docs/guide/schema-overview.md → content/docs/guide/schema-overview.md — edited by this change

    Docs, same change. content/docs/core/enhanced-actions.mdx — the three onSuccess / onFailure fences author successMessage / errorMessage instead, and the "Callbacks" section is a "Post-success behaviour" note pointing at the spec block (no fence: the legacy type carries no spec-derived block). content/docs/guide/schema-overview.md — the fragment line, the feature bullet and the checklist row are rewritten to the truth (the ✅ claim is now a retirement note).

.changeset/object-view-unmirrored-keys-7779.md

  • names zod-mirror-parity.test.ts → packages/types/src/__tests__/zod-mirror-parity.test.ts — edited by this change

    Who is NOT affected: every correctly typed document, and every document that never wrote these keys — absent stays valid on all nine. No renderer changed. The parity ledger (zod-mirror-parity.test.ts) records the move: UnmirroredDeclared 14 entries / 96 keys to 14 / 87, the ObjectViewSchema entry re-derived into the SPEC-DERIVED half because the mirror now references the spec in code.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 704e05b09 (merge-base with origin/main): 17 file(s) changed outside .changeset/, read against 1574 pending declaration(s) that publish a body (2162 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3066.1 KB 3104.5 KB
Main entry chunk (gzip) 148.3 KB 350 KB
Entry file index-wXwnd1Qs.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 554.92KB 132.66KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.93KB 62.31KB
fields (index.js) 259.29KB 65.64KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.65KB 14.69KB
plugin-charts (index.js) 82.37KB 22.57KB
plugin-chatbot (index.js) 198.40KB 47.22KB
plugin-dashboard (index.js) 134.11KB 35.62KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 233.23KB 61.74KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 157.08KB 40.13KB
plugin-gantt (index.js) 169.75KB 41.96KB
plugin-grid (index.js) 218.12KB 59.72KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 114.70KB 28.42KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.91KB 9.05KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 87.83KB 22.01KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 52e5d7927800343795f483bd31239cdb4bb8afcd

① Derived judgments

Contract face (@object-ui/types). Confirmed on the diff. app.ts turns actions?: AppAction[] into actions?: never; zod/app.zod.ts turns the member into retirementTombstone(APP_ACTIONS_REFUSAL) (z.never({ error }).optional().describe(), the same helper the 7719 shortcut refusal already uses). AppAction and AppActionSchema are deleted, and both barrels (index.ts, zod/index.zod.ts) drop them; AppActionSchemaType is gone too. AppComponentSchema is a member of AnyComponentSchema, so safeValidateSchema reaches the tombstone; the new pin asserts exactly one invalid_type issue at ['actions'] for the button, dropdown, user and empty-array values on both doors. The remedy string navigation: [{ id: 'quick_create', type: 'action', label: 'Quick Create', actionDef: { actionName: 'quick_create' } }] is verified field by field against objectstack packages/spec/src/ui/app.zod.ts: ActionNavItemSchema spreads BaseNavItemSchema.shape (id required SnakeCaseIdentifierSchema, label optional I18nLabelSchema, icon, order, badge, badgeVariant, visible, requiredPermissions, requiresObject) plus type: z.literal('action') and actionDef: strictObject({ actionName: z.string(), params?: record }). quick_create is snake_case; actionDef carries only actionName. The spec's strict AppSchema body has 0 actions hits (control navigation 24) and no retiredKey/alias for it, so the platform refuses the key with unrecognized_keys, which pin block (c) asserts.

AppMenuItem / MenuItemSchema stay. True: app.ts menu?: AppMenuItem[] and zod/app.zod.ts menu: z.array(MenuItemSchema) read them, and packages/runner/src/LayoutRenderer.tsx (app.menu?.map) and packages/layout/src/AppSchemaRenderer.tsx (menuItemToNavigationItem) consume menu. Their 7719 refusal is unchanged in behaviour; only the guidance text stops naming the deleted AppAction.shortcut.

Runner. The LayoutRenderer.tsx diff is confined to the header chrome region and the import block: the 'button' map, the 'user' DropdownMenu/Avatar arm with its items, the Bell's (!app.actions || !app.actions.some(...)) gate, and the ten now-unused DropdownMenu*/Avatar* imports. The Bell is drawn unconditionally. Nothing else in the file changes; it still reads app.menu and never app.navigation. packages/runner/package.json has "private": false, and @object-ui/runner is in the single 40-member fixed group of .changeset/config.json (as is @object-ui/types). minor is right under the repo's version-alignment rule (breaking changes in the fixed group are minor; check-changeset-no-major enforces it).

Docs. The rewritten 「Global Actions」 teaches an AppComponentSchema literal whose navigation item is { id: 'quick_create', type: 'action', label, icon: 'zap', actionDef: { actionName: 'quick_create' } }; that item is spec-valid (pin block (b) submits the same item to SpecAppSchema.safeParse and asserts zero issues). Both actions: examples on the page and the 「keyboard shortcuts」 best practice are removed; schema-overview.md loses its two actions examples and the user-menu bullet, and its doc-snippet note is re-pointed. The prose about the console (useNavActionDispatch warns and toast.errors on a missing actionDef.actionName and on an undefined action) and about the runner (draws menu only, no navigation) is true of the source. Non-blocking nit: the sentence 「so the document validates on both sides」 overclaims, since the surrounding literal carries type: 'app' and title, and the spec's strict AppSchema aliases title to label and declares no type; the item validates on both sides, the whole document does not. Determined from the spec source; no built spec was available for a probe.

Extra test edits. Each is re-pointed at the new truth, and nothing guarded shrinks except the retired thing itself:

  • app-action-onclick-refusal-6854.test.ts (deleted) pinned AppActionSchema.onClick message text plus a MenuItemSchema strip/shortcut block; the MenuItemSchema halves are still measured in the 7719 pin's STRIP and (b) blocks.
  • 7719: the dropped rows were the AppAction.shortcut non-target controls; block (d) now runs through AppComponentSchema.menu[] and asserts the issue path list equals exactly ['menu.0.shortcut'] (stricter than the old some()), with a same-app green control.
  • 7344: the RETIRED ledger drops the whole-schema site (4 to 3, 10 to 9 ids); the objectui#7721 reader census keeps the same population controls and now asserts an empty reader set with LayoutRenderer.tsx retained as the positive control (namesType still contains it, readers does not, source has no app.actions); the mentions row is now file-exact rather than package-level. Its SCAN_ROOTS are packages/apps/examples, so the docs callout naming AppAction is out of scope by design.
  • 7340: census 27 to 26, app.ts leaves the split; the entry counted AppAction.onClick?: never, which left with its type.
  • phase2: the actions fixture becomes a navigation action item; no residual actions assertion remains.
  • zod-mirror-parity: only the app.zod.ts#AppActionSchema row leaves MIRRORS and Declared, plus the two imports; EXPECTED_MIRROR_PAIRS 160 to 159 against the merge base 704e05b09, which already holds the objectui#7759 ledger edit.

Gate edit. DOC_TYPE_EXEMPTIONS is the script's designed route: its header (「the file must DECLARE it in DOC_TYPE_EXEMPTIONS with a written reason」) and its diagnostic (「a field data type, a nav item kind — declare it in DOC_TYPE_EXEMPTIONS with a reason」) say so, the same page already exempts the item and group nav kinds, and a stale-exemption verdict fails an entry whose value leaves the page. The entry is one page, one value, with a reason naming NavigationItemTypeSchema/ActionNavItemSchema; it exempts type: 'action' on core/app-schema.mdx only, matching the existing action exemptions on schema-reference.md and enhanced-actions.mdx. No rule code path changes.

Pins can fail. Types pin: 14 rows; on BASE the four it.each refusal rows, the describe-identity row (issuesOf(r)[0]! on a green parse) and block (e) (AppActionSchema still exported) fail = 6 failed / 8 passed; the two @ts-expect-error directives go unused (TS2578 x2) and Expect<Equal<AppComponentSchema['actions'], undefined>> fails its constraint (TS2344 x1). Runner pin: 6 rows; on BASE the Bell is suppressed, Quick Actions is drawn as a titled button, the AD avatar and forceMount content appear, and the button count differs = 4 failed / 2 passed (control and page-body rows). Both match the claimed ablations.

Residue. At head, AppAction/AppActionSchema survive outside packages/types only as prose in the docs callout and the runner pin header; the only actions: [ on an app-node literal is the pin's deliberate @ts-expect-error row; no JSON fixture carries "actions"; neither package README taught the key.

② Semver level

minor for @object-ui/types and @object-ui/runner, correct: both published surfaces narrow, both sit in the fixed group, and the repo forbids major there. Every sentence of .changeset/7469-app-actions-retired.md checks against the diff and the source: the refusal and compile failure, the removed exports, the retained AppMenuItem/mirror, the spec's strictness and the console's platform-only source, the runner's buttons declaring no behaviour, the Bell previously hidden when a 'button' action was authored (the old gate), and the surviving 7719 shortcut refusal on menu items. Non-blocking nit: the caveat sentence names 6854, 7344, 7719 and 7721 as pending changesets that describe AppAction; 6349-types-internal-name-collisions-batch-2.md and 6692-app-schema-menuitem-import.md also name AppAction.items as a historical reader, and are not listed.

③ Boundary flags

  • (a) All three branch commits carry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> and a Claude-Session: trailer. The main ruleset's merge_queue rule has merge_method: SQUASH, and the repository's squash message source is squash_merge_commit_title: PR_TITLE / squash_merge_commit_message: PR_BODY. The PR body carries no Co-Authored-By line (only the inline session link), so the trailer cannot reach main through the squash commit; the branch commits themselves stay off main. mergeable_state is clean.
  • (b) The 18 diff paths are exactly the claim's list plus the seven paths the addendum 5853384985 names (the five types pins, the gate script, schema-overview.md). The new runner pin LayoutRenderer.chrome-7469.test.tsx is not named in the addendum; it falls under the claim's 「the runner tests that pin them」 clause as the replacement for the deleted 6854 runner pin. No breach.
  • (c) CI ran the full suite: Test (shard 1/8) through (8/8) each execute pnpm test --shard=N/8 (the whole vitest tree, app-shell included) and all eight plus the aggregate Test context are success. All 43 check runs are success or skipped (dependabot and the coverage variants); every required context, Spec Main Shape Gate included, is green.
  • Other, non-blocking: the report-only changeset-claim-re-read comment flags .changeset/7760-lazy-mirror-input-type-args.md for naming the deleted 6854 test file in prose; the PR discloses this and check-changeset-overwrite bars editing another card's changeset. No human or bot review threads exist on the PR.

Implemented-by: claude/issue-7469-retire-app-actions
Reviewed-by: session_014fWVhLzhxR8qrFsJ5o8TYW

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 07:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 25c7d58 Sep 27, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-7469-retire-app-actions branch September 27, 2026 07:42
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 28, 2026
…ding it hides (objectui#10746) (objectstack-ai#10762)

Fixes objectstack-ai#10746
Clause-②: no — a Studio inspector's patch on a type switch; no declared
surface moves

## What changed


`packages/app-shell/src/views/metadata-admin/inspectors/ReportDefaultInspector.tsx`:
the Report type picker's commit is now `commitType`. When the picked
type is `joined`, the patch that commits `type: 'joined'` also carries
`dataset`, `values`, `rows`, `columns`, `chart` and `order` as
`undefined`-valued keys — only those the draft actually holds
(`draft[key] !== undefined`). Any other type commits `{ type }` alone,
as before. The list lives in `JOINED_CONTAINER_CLEARED_KEYS` with the
reasoning beside it; `blocks`, `runtimeFilter` and `drilldown` are never
named.

`order` is a sixth key, beyond the card's five, under the role file's
bounded in-place exemption (same defect class, mechanical, same file,
same gate family, no other claim on the file): the spec's own
`reportForm` hides `order` in the same "Dataset binding" section
(`visibleWhen: "data.type != 'joined'"`), and the INSTALLED spec already
refuses a container `order` on a joined report ("a `joined` report
orders per block — move `order` onto `blocks[]`"), so the same
invisible-key save refusal exists today for `order`. Evidence in H3
below.

New pin file `ReportDefaultInspector.joinedClearsBinding-10746.test.tsx`
(8 tests); changeset `.changeset/10746-joined-report-clears-binding.md`
(`'@object-ui/app-shell': patch`).

## H1 — reproduction on `origin/main` (`1422a920ed`)

The pin file run against the untouched component: `Tests 4 failed | 4
passed (8)`. The defect pin quotes the only `onPatch` argument the type
picker commits today: `AssertionError: expected { type: 'joined' } to
strictly equal { type: 'joined', …(6) }` — the commit is `{ type:
'joined' }` and nothing else. The 4 pins green on base are the
boundary/control pins, which pin what must NOT change.

## H2 — the clear mechanism

`onPatch` is a shallow patch in all three hosts: `ResourceEditPage`
applies it as `handleDraftChange((d) => ({ ...d, ...patch }))`,
`ReportConfigPanel.handlePatch` as `{ ...draftRef.current, ...patch }`,
and `StudioDesignSurface.onPatch` as `setDraft((d) => ({ ...d, ...patch
}))`; each saves through `client.save`, which is `JSON.stringify`. None
offers a delete sentinel, so the one way to drop a key is an
`undefined`-valued key — the spelling this inspector's own `commitChart`
(`chart: next.type ? next : undefined`) and the siblings
(`ActionDefaultInspector`, `ObjectDefaultInspector`,
`DatasetDefaultInspector`'s "Clear all", pinned by its objectui#9372
suite) already use. Measured in the second pin: after the spread the key
is an OWN property holding `undefined` (`Object.hasOwn` true, value
`undefined` — not `null`, not an empty string), and
`JSON.parse(JSON.stringify(committed))` — the shape `client.save` puts
on the wire and the spec parses — has no such property. The spec's
refinement itself skips `undefined` (`if (value === undefined || …)
continue;` in the joined arm), so even the in-memory draft would not be
refused.

- Reach: The clear holds for the patch and for the save that follows it.
The metadata-admin editor rebuilds its draft as the served draft spread
over `layered.effective` (on load, after each save and after publish),
and `effective` is the published layer, so a report whose PUBLISHED
version was bound gets those keys back in the draft after the first
draft save until it is published, and a report already saved `joined`
with stale keys is not repaired on load. Both are the host's
draft-over-baseline merge, objectui#10765.

## H3 — the spec check

Installed `@objectstack/spec` is 17.4.0
(`packages/app-shell/node_modules/@objectstack/spec`). It PREDATES
objectstack-ai/objectstack#20160: `grep 'selects per block'` over its
`dist/` gives 0 hits; the control `grep 'orders per block'` gives 3.
One-off probe with the installed `ReportSchema.safeParse` (not
committed):
- joined + one block: success
- joined + stale `dataset` / `values` / `rows` / `columns` / `chart`:
**success** — 17.4.0 accepts them
- joined + stale `order`: refused, `custom` at `['order']`, "a `joined`
report orders per block — move `order` onto `blocks[]`."
- joined + `blocks: []`: refused at `['blocks']`
- joined + `runtimeFilter` + `drilldown: false`: success
- joined + `dataset: undefined` as an own key: success

So the pins' parse leg measures the `order` half with the installed spec
(before the fix: refused at `['order']`; after: parses), and for the
four selection keys the pins assert ABSENCE and cite the rule read at
objectstack `origin/main` `packages/spec/src/ui/report.zod.ts`:
`JOINED_CONTAINER_SELECTION_KEYS = ['dataset', 'rows', 'columns',
'values']`, one `custom` issue per present key at `path: [key]`, message
"a `joined` report selects per block — move `KEY` onto `blocks[]`, or
delete it; on the container it selects nothing." (`KEY` stands for the
key's own name.) No checkout on this box holds a built spec `dist`
carrying that refusal, and building one was outside this card.

## H4 — edges

- `joined` → non-joined: nothing is restored; the patch is `{ type }`
alone and the author re-binds (pinned).
- non-joined → non-joined: the binding is kept; the patch is `{ type }`
alone (control, pinned).
- `joined` with `blocks[]` present: untouched, same array reference, in
both directions (pinned).
- Undo: there is no undo stack behind the metadata draft.
`ResourceEditPage`'s `Undo2` button is `doDiscardDraft` (ADR-0034:
discard the whole pending draft), not a per-edit undo, and
`handleDraftChange` keeps no history. One undo cannot restore the keys;
switching the type back does not either.
- `runtimeFilter` and `drilldown` survive the switch (pinned): the
joined branch reads both.
- Partially bound report: the patch names only the keys present
(pinned), so `ReportConfigPanel.onFieldChange` sees no phantom clears,
and an unbound report's switch stays `{ type: 'joined' }` (pinned).

## Pins and ablations

Head `3f53c15776`, pin file plus the two existing
`ReportDefaultInspector` suites: `Test Files 3 passed (3) · Tests 37
passed (37)`.

Red on base (`1422a920ed`, component untouched, pin file present): `4
failed | 4 passed` — the defect pin, the own-key/serialised pin, the
parse leg, the partially-bound pin. The other 4 are controls and
boundaries, green on base by construction.

Per-hunk ablations on the committed head, each through objectstack's
`scripts/ablation-replace.mjs` (anchor must hit exactly once; the blob
change is verified on disk; restore proven `blob == HEAD` `a9cb7e9c` and
`git diff HEAD` empty), the prediction written before each run, observed
direction = predicted:
- M1, drop the `joined` guard (`if (nextType === 'joined')` becomes `if
(typeof nextType === 'string')`): predicted CONTROL F red; observed `1
failed | 7 passed` (CONTROL: non-joined → non-joined keeps the binding).
- M2, drop the presence filter (`if (draft[key] !== undefined)
patch[key] = undefined;` becomes unconditional): predicted D and E red;
observed `2 failed | 6 passed` (names only present keys; unbound one-key
patch). The first attempt was a NO-OP the tool refused — the replacement
text was a substring of the anchor, so its on-disk count could not rise
(`1 to 1, a rise of 0`) — and it restored; the leg was re-run with a
distinct replacement.
- M3, add `blocks` to the cleared list: predicted C and G red; observed
`2 failed | 6 passed` (parse leg: the joined report loses its blocks;
CONTROL: blocks untouched).
- M4, drop `order` from the list: predicted A, B and C red; observed `3
failed | 5 passed`.

Assertion spelling: every patch is read with `toStrictEqual`, because
`toHaveBeenCalledWith` / `toEqual` treat an `undefined`-valued key as
absent and would read the defect and the fix alike.

Round 2 (`d4945789f6`, the contract-review wording round: the
changeset's reach sentence and the pin file's three-host comment; no
source line changed — the diff's only non-changeset lines are the
comment lines shown by `git diff -U0`, and `ReportDefaultInspector.tsx`
is untouched): pin file re-run `Tests 8 passed (8)`.

## Gates (local, derived by hand from `package.json` and the workflows —
objectui has no `dispatch-gates.mjs`)

- ① closure build `turbo run build --filter='@object-ui/app-shell^...'
--concurrency=2`: `Tasks: 28 successful, 28 total`, 2m12s, under the
verify lock.
- ② `pnpm --filter @object-ui/app-shell type-check` (`tsc --noEmit &&
tsc -p tsconfig.test.json`): exit 0; `tsc -p tsconfig.test.json
--listFiles` lists the pin file (1 hit among 4673 files).
- ② vitest, the three files above: 37 passed; round 2 pin file: 8
passed.
- `node scripts/check-changeset-presence.mjs`: ✅ (round 2: 2 source
files of 1 released package changed, 1 changeset declared). `node
scripts/check-changeset-no-major.mjs`: ✅. `pnpm check:changeset-claims`:
✅. `pnpm check:pending-changeset-literals`: ✅.
- `pnpm check:control-bytes`: ✅ OK (round 2: 8942 tracked text files).
The role file's control-byte grep over the changed files: no match.
- `pnpm check:new-line-citations`: `0 new citation(s)` (both rounds).
- `pnpm check:test-path-roots`: ✅ OK. `pnpm check:vi-mock-specifiers`: ✅
OK.
- eslint with the package config on the two touched `.tsx` files: exit
0.
- NOT MEASURED locally, CI-owned: `pnpm lint` (repo-wide), the 8 test
shards, `Build & E2E`, `check:i18n-*` (no `t()` key was added or
changed). Round 1 CI on `3f53c157`: 40 success, 3 skipped, 0 failed per
the contract review.

## Serial

Round 2: `origin/main` at `25c7d584e4` (+5 commits over BASE
`1422a920ed`: objectstack-ai#10752, objectstack-ai#10753, objectstack-ai#10708, objectstack-ai#10751, objectstack-ai#10761); none touches the
three files; `git merge-tree --write-tree` exits 0 (clean, tree
`738f2bf534`). No merge commit was needed.

## Acceptance notes

- `chart` is cleared per the triage direction and
objectstack-ai/objectstack#20161 (inert on a joined container); the
installed spec does not refuse it, so its clearing is pinned by absence
only.
- Deviation: the worktree-creating `git fetch origin main` ran against
the shared checkout (the role-file recipe) rather than inside the
worktree as the order asked; `origin/main` did not move (`1422a920ed`
before and after), only the shared checkout's `FETCH_HEAD` was touched.
Every later fetch used a private ref inside the worktree.
- Observation, not filed: `ReportConfigPanel` hosts this inspector too,
merges with the same spread and emits `onFieldChange(key, undefined,
next)` once per cleared key — the documented "field changed" signal,
correct for a live preview.
- The draft-over-baseline merge that re-introduces a published binding
after the first draft save is objectui#10765 (filed by the seat), not
addressed here.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment