Skip to content

fix(plugin-designer): editing an app keeps its stored navigation (objectui#10894) - #10912

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10894-app-edit-keeps-navigation
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10894-app-edit-keeps-navigation

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10894
Clause-②: no — an edit stops destroying stored navigation. No declared type, accepted key or published surface moves.

Dispatched by the domain:ui seat 2 PM loop, claim comment 5864678970, session https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN. Base 9f0c84a448, head 5b2b8568bb.

What changed

  • AppCreationWizard: leaving the Objects step (handleNext, and handleStepClick on the step 1 to step 2 jump) no longer sets navigation: generateNavFromObjects(prev.objects). Both call mergeNavWithObjects(prev.navigation, prev.objects):
    • an EMPTY navigation is filled with one object entry per selected object, exactly what generateNavFromObjects returns (the create path, unchanged);
    • a non-empty navigation is merged, never replaced (rule below).
  • EditAppPage: an object counts as selected when the stored navigation has an object entry for it ANYWHERE, a group's children included (navObjectNames, exported from the wizard module). It counted top-level entries only.
  • One changeset, .changeset/10894-app-edit-keeps-navigation.md: '@object-ui/plugin-designer': patch.
  • No second navigation model, and nothing regenerates on a step change.

H1 — reproduced on main (9f0c84a448)

A throwaway probe (deleted, never committed) drove the real EditAppPage and wizard to saveItem, renaming the app only. The stored navigation [nav_accounts (object "Customer Accounts", icon Briefcase), nav_rule (separator), nav_sales (group with the nav_pipeline dashboard child), nav_docs (url)] saved as:

[{"id":"account","type":"object","label":"Accounts","icon":"Building","objectName":"account"}]

The Navigation step listed ["account"]. Even the kept object entry lost its authored id, label and icon. Create-path control on the same base: an empty draft with account and contact selected saves the two generated object entries (the control row below, green on base).

H2 — every writer of draft.navigation

All writers live in AppCreationWizard:

Writer What it writes Read point
initial state DEFAULT_DRAFT.navigation ([]), then the initialDraft spread: EditAppPage passes the stored navigation; CreateAppPage passes a localStorage draft, which can carry one Navigation step list
addNavItem appends a group, url or separator Navigation step list
removeNavItem filters a top-level entry by id Navigation step list
reorderNavItem swaps two top-level entries Navigation step list
handleNext (leaving step 1) was generateNavFromObjects (replace); now mergeNavWithObjects Navigation step, then save
handleStepClick (step 1 to 2) same as handleNext Navigation step, then save

The draft's navigation is read by NavigationBuilderStep (items), by handleComplete (onComplete, then wizardDraftToAppSchema, then saveItem in CreateAppPage / EditAppPage) and by handleSaveDraft (localStorage). EditAppPage also reads the STORED navigation for the Objects step's selected.

NavigationDesigner is a controlled component (items / onChange); the wizard does not render it and it writes nothing to the draft. apps/console has no writer: app-shell's AppContent lazy-loads CreateAppPage / EditAppPage onto routes, and the console tests mock EditAppPage.

The merge rule, and the group-child edge

Leaving the Objects step with a non-empty navigation:

  • the object entries of an object the Objects step lists as DESELECTED are dropped, at the top level or inside a group;
  • a SELECTED object with no object entry anywhere in the tree gets one, appended at the end;
  • everything else is kept as stored, in its position: separators, groups and their children, url / dashboard / page / report / component / action entries, each object entry's authored label, icon and order, and an object entry for an object the Objects step does not list (nothing deselected it).

When no entry is dropped the tree is returned as the same array, so an untouched edit saves the stored bytes.

Group-child edge, measured and pinned. An object entry the author placed inside a group (nav_people, object contact, inside nav_sales beside the nav_pipeline dashboard):

  • untouched, it round-trips byte-equal: EditAppPage now lists contact as selected;
  • deselecting contact drops the entry from inside the group. The group keeps its place, its label and icon, and its other children;
  • a group whose only child is dropped stays, with children: []. The spec AppSchema parses that save green.

Without the EditAppPage change the merge would drop a grouped object entry on an edit that never touched it: ablation B below reddens exactly those rows.

Pins, red on base and green on head

packages/plugin-designer/src/__tests__/EditAppPage.keepsNavigation-10894.test.tsx, 9 rows, each through the real EditAppPage or CreateAppPage save:

  1. the stored tree round-trips byte-equal apart from the edited title, leaving the Objects step by Next (whole body compared as JSON, plus the Navigation step lists the stored entries in order, plus spec AppSchema green);
  2. the same, leaving the Objects step by the step indicator;
  3. selecting a new object appends exactly its entry and keeps everything else;
  4. deselecting an object drops only its entry;
  5. a grouped object entry round-trips untouched;
  6. deselecting its object drops it from inside the group; the group and its other children stay;
  7. a group whose only child is dropped stays, with an empty children;
  8. an object entry whose object the Objects step does not list is kept;
  9. CONTROL: the create path still generates one object entry per selected object for an empty draft.
  • Base (the test-only commit 3b93e01369 on base code): Tests 8 failed | 1 passed (9). The one pass is row 9.
  • Head 5b2b8568bb: Tests 9 passed (9).

Ablation A: restore the unconditional regeneration. scripts/ablation-replace.mjs (objectstack) swapped both mergeNavWithObjects(prev.navigation, prev.objects) calls for generateNavFromObjects(prev.objects): anchor x2 to x0, blob 1669c2e50c50 to adac4d10b62c. Result Tests 8 failed | 1 passed (9): rows 1 to 8 red, only the create-path control green. Restored: blob equals HEAD (1669c2e50c50), git diff HEAD empty.

Ablation B: EditAppPage selection back to top-level only. Anchor x1 to x0, blob 31e1ae7eb312 to 6744ceabdad7. Result Tests 3 failed | 6 passed (9): exactly rows 5, 6 and 7. Restored: blob equals HEAD, git diff HEAD empty.

The pins import EditAppPage and CreateAppPage by relative path into src, so no dist sits in the resolution path and neither ablation needed a build leg.

Consumer sweep

Population: 27 test files under packages/plugin-designer/src (git ls-files and find agree). The five that touch the wizard, CreateAppPage, EditAppPage or NavigationDesigner (git grep for those names and their registry keys), at head 5b2b8568bb:

File Verdict
AppCreationWizard.requiredMarker-10367.test.tsx passed 2/2
__tests__/AppWizard.specDocument-10867.test.tsx (the objectui#10867 pins) passed 9/9
__tests__/EditAppPage.declaredKeys-10842.test.tsx passed 5/5
__tests__/EditAppPage.keepsNavigation-10894.test.tsx passed 9/9
__tests__/NavigationDesigner.specNavTypes.test.tsx passed 10/10

Gates (head 5b2b8568bb)

Heavy runs went through the shared verify lock (os-verify-lock.sh, VERDICT command-exit lines quoted).

  • pnpm exec vitest run packages/plugin-designer/: Test Files 27 passed (27), Tests 198 passed (198), VERDICT command-exit 0.
  • pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-designer^...' build (16 packages): VERDICT command-exit 0. Then pnpm --filter @object-ui/plugin-designer type-check, which echoed tsc --noEmit && tsc -p tsconfig.test.json: VERDICT command-exit 0. tsc -p tsconfig.test.json --listFiles includes the new pin file.
  • check:new-line-citations: 0 new citation(s), exit 0. check:control-bytes: OK, exit 0.
  • check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots: OK, exit 0.
  • check:changeset-claims: "No pending changeset names a file this change touches", exit 0. check:pending-changeset-literals: exit 0.
  • check:metadata-write-doors, check:unreferenced-sources, check:shell-escape-residue: OK, exit 0.
  • scripts/check-changeset-presence.mjs: 1 released package, 1 changeset declared, exit 0. scripts/check-changeset-no-major.mjs: exit 0.
  • scripts/check-governed-queue-guard.mjs --test on the four changed paths: NOT GOVERNED.
  • ESLint on the three changed source and test files (--format json, 3 files): 0 errors. The repo lint (pnpm lint) is left to CI.

Pending changesets that name AppCreationWizard or EditAppPage (objectui#10367, objectui#10827, objectui#10842, objectui#10867) were read. None has a sentence this change makes false, so none gets a dated note.

Serial

The claim read the open-PR file lists at 9f0c84a44: none touches packages/plugin-designer/src. Re-read before this PR opened: 10 open PRs, none touches packages/plugin-designer/src. The release PR's file list was read as one capped page of 100; it versions packages. PR objectui#10889, the file's previous holder, is merged.

Acceptance notes

  • "Newly selected" is read as a state, not a delta: a selected object with no object entry anywhere in the tree gets one. So if an author removes an object's entry on the Navigation step but leaves the object selected, passing through the Objects step again brings the entry back. That matches today's behaviour for that entry, and it keeps the Objects step's ticks meaning "this object is in the navigation". The delta reading ("only objects ticked during this visit") would need the wizard to remember a selection snapshot.
  • The wizard's Navigation step still lists and edits top-level entries only. A group's children are kept and saved, but not shown there. This PR leaves that unchanged.
  • The create path merges too once the author has shaped the navigation, a localStorage draft included. Going back to the Objects step keeps added separators, groups and links, and their order. The changeset says so.
  • An appended entry's id is the object's name, as on the create path. A stored non-object entry already using that exact id would be duplicated. Not measured; no fixture or stored tree read here does it.
  • Exporting navObjectNames from the wizard module keeps the change inside the claimed file surface. It costs one react-refresh/only-export-components warning there (warnings are not a gate). The EditAppPage edit removes one no-explicit-any warning.

Generated by Claude Code

…on (objectui#10894)

Drives the real EditAppPage and wizard to saveItem. Red on the base: leaving
the Objects step replaces the stored navigation with generated object entries.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…ectui#10894)

Leaving the Objects step no longer replaces the draft's navigation with one
generated object entry per selected object. An empty navigation is filled as
before (the create path); a non-empty one is merged: a newly selected object's
entry is appended, a deselected object's entries are dropped wherever they sit,
and every other entry is kept as stored, in its position.

EditAppPage counts an object as selected when the stored tree has an object
entry for it anywhere, a group's children included, so an untouched edit drops
nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
…avigation (objectui#10894)

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3096.1 KB 3104.5 KB
Main entry chunk (gzip) 148.6 KB 350 KB
Entry file index-Cb9h59WM.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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: 5b2b8568bb952524f8dc4fa0d21e6441241fc4ac

① Derived judgments

Merge semantics (read against spec NavigationItemSchema at objectstack 5581d3000f, @object-ui/types NavigationItem, layout NavigationRenderer, NavigationDesigner)

  • Entry types. The spec union has ten branches: object (with optional children), dashboard, page, url, report, action, component, doc, separator, and group (with required children). doc landed in spec after the 17.4.0 that objectui pins. mergeNavWithObjects only acts on type: 'object' entries that carry an objectName, and it recurses only into group.children. Every other entry passes through by reference, so its label, icon, order and position are preserved. Groups are handled at any depth, because dropDeselectedObjectEntries and navObjectNames both recurse.
  • "Deselected" is measured from the Objects step's list. The code uses objects.filter(o =] !o.selected) over prev.objects, not absence from the tree. Holds.
  • Unlisted object entry kept. This is right. The Objects step cannot deselect what it does not list, and dropping the entry would destroy entries for objects the metadata list simply does not show. A dangling entry for a deleted object is hidden at runtime by the per-item capability guard in NavigationRenderer (passesNavItemGuards). The Navigation step can still remove it at the top level.
  • Empty group. The spec's group children is z.array(...) with no minimum, and the wizard's own "Add group" writes children: []. NavigationRenderer returns null for a group with no visible children (objectui#10119). The pin also checks the saved body parses against SpecAppSchema. Accepted, and it renders as nothing.
  • Duplicates. Deselecting an object drops every entry for it. Reselecting appends one generated entry at the end. The authored copies are not restored, which is reasonable and disclosed as "appended at the end".
  • Gap, non-blocking. Both walkers skip an object entry's own children, which the spec allows ("e.g. specific views"). The newer repo walkers (navSurface.ts, hasVisibleNavigationItems) walk any children. The two new helpers skip the same shape, so nothing is lost. A child object entry under another object entry reads as unselected, and it is not dropped when that object is deselected. Selecting it appends a duplicate top-level entry. The runtime renderer does not render object children either, so this is not a blocker.
  • Edge, non-blocking. An appended entry takes id: o.name. Previously the whole list was regenerated, so ids were unique. Now an appended id can collide with a stored non-object entry whose id equals that object name. The spec does not refuse duplicate nav ids, and removeNavItem filters by id. This is unlikely in practice.

Create path

  • An empty navigation is filled exactly as generateNavFromObjects did (added over all objects, kept empty).
  • Templates do not pre-fill navigation. There is no duplicate-app flow; AppCreationWizard has only two consumers, CreateAppPage and EditAppPage.
  • Two create-path cases now reach step 2 with a non-empty navigation: going back and forward, and resuming a localStorage draft in CreateAppPage. Both now keep what the author shaped instead of regenerating it.
  • A newly selected object is appended rather than placed in Objects-step order. The changeset discloses this.
  • No stale-entry regression: deselected objects are still dropped.

EditAppPage selection

  • The spec retires AppSchema.objects (retiredKey), so navigation is the only reading.
  • Walking groups is needed. Without it, the merge would drop a grouped entry on an untouched edit, and ablation B shows exactly that.
  • Objects that appear only under areas[].navigation are not counted. This predates the PR and loses nothing, because areas is preserved through pickDeclared.

Pins (read, not run)

  • All nine rows drive the real EditAppPage or CreateAppPage through the wizard to the body saveItem receives. Only router, adapter, metadata and toast are mocked.
  • By construction, eight rows are red on base, because base regenerates [account] or []. The create-path control is green. This matches the dev's quoted "8 failed | 1 passed" and "9 passed".
  • Ablation A (regeneration restored at both call sites) reddens the eight edit rows. That follows by reading.
  • Ablation B (top-level-only selection) reddens exactly the three group-child rows. On the grouped round trip, contact reads unselected and gets dropped. In the other two rows the toggle now selects contact, so it is kept. The unlisted-task row is unaffected. Consistent with the claim.
  • Coverage gap, non-blocking. The step-indicator path is pinned only by a round trip, and that row would also pass if handleStepClick did nothing to navigation. No pin covers select or deselect through the step indicator. The code is symmetric with handleNext.
  • The wizard-step-navigation testid is the step button (wizard-step-${step.id}), so that row does exercise handleStepClick.

Export and lint

  • react-refresh/only-export-components is warn, and lint.yml deliberately sets no --max-warnings.
  • There is precedent for helper exports beside a component: 180 non-hook lowercase function exports from .tsx files on main, for example resolveSetupAppPath in ConsoleShell.tsx.
  • AGENTS.md has no rule on this. A sibling .ts module would be cleaner and would silence the warning, but it is not required.

Changeset. Every sentence is true against the code except one overstatement.

  • "counts an object as selected when the stored navigation has an object entry for it anywhere, a group's children included" is false for an object entry nested under another object entry's children.
  • Recommended replacement, non-blocking: "EditAppPage counts an object as selected when the stored navigation has an object entry for it at the top level or inside a group, at any depth."
  • The four pending changesets that name these files (10367, 10827, 10842, 10867) contain no sentence the change makes false.

② Semver level

patch holds, and Clause-②: no holds.

  • navObjectNames is exported from AppCreationWizard.tsx only.
  • src/index.tsx imports AppCreationWizard and the type AppCreationWizardProps by name, with no export *.
  • The package exports map has only ".".
  • No declared type, accepted key or published surface moves. The behaviour change for a non-empty initialDraft.navigation is the bug fix. No major.

③ Boundary flags

  • CI on head 5b2b8568bb. 42 check runs: 31 success, 3 skipped (dependabot, both coverage jobs), 8 in progress. Lint, Type Check, Build & E2E, Test shard 5/8, all changeset gates and Governed Surface Queue Guard are success. Not final: Test shards 1, 2, 3, 4, 6, 7 and 8 of 8, and Spec Main Shape Gate, are still in progress. This PASS assumes they finish green.
  • Merge. git merge-tree --write-tree against main 733fd5ac65 is clean (tree f9d6379c4b). Main has not touched either source file since base 9f0c84a448.
  • Open PRs on the same files. None. The only other open plugin-designer PR, objectui#10914, touches MetadataFieldsPage.tsx only.
  • check-governed-merges. NOT governed: 0 of 4 paths hit a governed surface, and 323 changed lines is under the 5000-line human-merge threshold.
  • Draft and assignee. The PR is still a draft, as the order asked, and must be marked ready before landing. Assignee is os-elon-musk.

Implemented-by: claude/issue-10894-app-edit-keeps-navigation
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS

…el and groups (objectui#10894)

The selection walk recurses into group children only, so "anywhere" overstated
it for an object entry nested under another object entry's children.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3096.1 KB 3104.5 KB
Main entry chunk (gzip) 148.6 KB 350 KB
Entry file index-Cb9h59WM.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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: e121ec7c115ad12ae42b31742e6c6e54e2a387f3

This is an in-seat delta review of the changeset follow-up (5b2b8568bb to e121ec7c11, one commit), against record 5865065243 (PASS, with one non-blocking changeset overstatement the seat chose to take before landing).

① Derived judgments

  • The changeset sentence: fixed, verbatim. It now reads "EditAppPage counts an object as selected when the stored navigation has an object entry for it at the top level or inside a group, at any depth." That is true of navObjectNames, which recurses into group children only, not into an object entry's own children. The next sentence is unchanged.
  • Nothing else moved: one file, 1 insertion and 1 deletion. There is no code or test change, so the pins and ablations measured at 5b2b8568bb stand.
  • Test (shard 7/8) failed at 5b2b8568bb, and the failure is not this PR's.
    • The check-run annotation names packages/plugin-detail/src/__tests__/RelatedList.unmaterializedSort.test.tsx, case "keeps that button in client mode": "Unable to find an accessible element with the role button and name /Total/", with only Previous and Next present.
    • That is objectui#10895's measured race (the sort-button row appears only after the object schema commits).
    • This PR touches only packages/plugin-designer and its changeset, and plugin-detail does not import it.
    • objectui#10895 is dispatched (claim 5865039628) and its fix is in development, so no fix exists yet to port. The push of e121ec7c11 re-runs every check. This PR lands only on a fully green run at that head; if the race recurs, the seat re-runs the shard once or waits for objectui#10895's fix.

② Semver level

patch for @object-ui/plugin-designer, Clause-②: no. ⛔ Not major.

③ Boundary flags

  • CI: the landing waits for every check-run on e121ec7c11 to be final and green, and re-runs git merge-tree against current main first. The dev's merge-tree against 733fd5ac65 is clean (tree 12a064d0a0).
  • No open PR shares either source file.
  • NOT GOVERNED.
  • Draft, assigned to os-elon-musk.

Implemented-by: claude/issue-10894-app-edit-keeps-navigation
Reviewed-by: session_014mXUNuFomfj24w7s1pZzhN

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: landing paused on Spec Main Shape Gate, which is not this PR's failure

domain:ui seat 2, session_014mXUNuFomfj24w7s1pZzhN.

  • What failed: at head e121ec7c11, Spec Main Shape Gate (check run 108822152446). packages/types/src/__tests__/report-chart-query-spec-parity.test.ts fails with "TS1360: Type 'true' does not satisfy the expected type 'false'", compiled against spec main df3ba164a588. Every other check is green.
  • Why it is not this PR's: this PR touches only packages/plugin-designer and its changeset. objectstack#20369 (681868ca7c) typed JoinedReportBlockSchema upstream, and the pin trips as its own docblock intends. The same line fails in the merge queue for PR objectui#10906.
  • Fix: none exists yet. It is filed as objectui#10916. The ACCEPT and auto-merge wait until the gate is green here, after objectui#10916's fix lands on main (a base merge if the gate needs it).

…0894-app-edit-keeps-navigation

Claude-Session: https://claude.ai/code/session_014mXUNuFomfj24w7s1pZzhN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3096.1 KB 3104.5 KB
Main entry chunk (gzip) 148.6 KB 350 KB
Entry file index-Cb9h59WM.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.58KB 6.17KB
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.22KB 10.61KB
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.70KB 2.23KB
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.70KB 10.94KB
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) 557.59KB 133.60KB
core (index.js) 9.93KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 226.44KB 63.00KB
fields (index.js) 261.01KB 66.28KB
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.32KB 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.96KB 14.83KB
plugin-charts (index.js) 83.99KB 22.86KB
plugin-chatbot (index.js) 197.67KB 46.90KB
plugin-dashboard (index.js) 136.93KB 36.48KB
plugin-designer (index.js) 215.78KB 44.42KB
plugin-detail (index.js) 233.48KB 61.79KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 161.21KB 41.41KB
plugin-gantt (index.js) 170.35KB 42.19KB
plugin-grid (index.js) 228.33KB 62.59KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.86KB 28.64KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.17KB 12.20KB
plugin-timeline (index.js) 31.00KB 9.09KB
plugin-tree (index.js) 11.21KB 3.89KB
plugin-view (index.js) 88.55KB 22.21KB
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.81KB 3.58KB
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) 119.16KB 39.05KB
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) 7.50KB 3.05KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.16KB 2.71KB
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.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.22KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 3.83KB 1.49KB
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.26KB
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.27KB 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
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 09:04
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit 44b67da Sep 28, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10894-app-edit-keeps-navigation branch September 28, 2026 09:34
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
… family D input slice) (objectstack-ai#10907)

Part of objectstack-ai#9256
Clause-②: yes

**Clause-② `yes`, as the claim declared:** `input` accepted an authored
`children` on both published faces and rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it. It now refuses it
by name. A published accept set narrows, so a contract review is owed
before landing.

This is the family-D slice that release `5864147075` on objectui#9256
lists, and nothing else: `input`, the family-D test header's superseded
remarks, and `PartialSchema`'s docblock claim about generic `Omit`
re-spellings. `br`, `hr` and `img` are not touched. objectui#9256 stays
open.

## What changed

- **`input` narrowed** in the family-D shape PRs objectui#9589 and
objectui#10897 used:
- TypeScript face (`InputSchema` in `packages/types/src/form.ts`):
`body?: never` and `children?: never`, each with a docblock saying what
the renderer reads and what it renders instead.
- zod mirror (`InputSchema` in `zod/form.zod.ts`): two
`retirementTombstone` members fed one string, `INPUT_NEITHER_CHANNEL`,
built by `neitherContentChannelGuidance`. Both stay MEMBERS, so
`zod-mirror-parity`'s key sets stay equal.
- `body` was already refused on both faces by `BaseSchema`
(objectui#6771). It is restated because that refusal names `children` as
the remedy, and `input` does not read `children` either.
- **`email` / `password` (`InputShorthandSchema`)**: no type or
accept-set change. See "The shorthand's own pair" below.
- **Texts (comments only):**
- The family-D test header: 「family E, frozen」 (the `InputSchema`
bullet), 「unattributable」 (the `DetailViewSchema` bullet) and the two
`ui:calendar` remarks that still said plain `Omit` (the header bullet on
the six bare-name hold-outs, and the comment above the slice-2 rows) now
say what is true. No assertion in that file moved.
- `PartialSchema`'s docblock in `packages/types/src/index.ts`: the
sentence reading every generic re-spelling as collapsing is marked
false, with `OmitDeclared` named as the counter-example.
- **Bounded in-place fix, declared:**
`partial-schema-collapse-pin.test.ts`'s header carried the same false
claim, and even named the key-remapping spelling as one that collapses.
It is corrected the same way, comment only. See "Texts" below for the
four conditions and the evidence.
- **New pin file**
`packages/types/src/__tests__/content-channel-input-9256.test.ts`.
- **One changeset** `.changeset/9256-input-content-channels.md`:
`@object-ui/types` `minor` with an explicit BREAKING note and a
migration line, the spelling PRs objectui#9589 and objectui#10897 used
under this repo's no-major rule.
- **Four pending changesets are corrected, prose only.**
- Round 2 added a dated note to `6397-partial-schema-collapse-pin.md`,
`9256-content-channel-family-d.md` and
`8499-node-slot-registered-arms.md`.
- Round 3 added a second note to the family-D entry and reworded the E3
entry's render claim in place.
- Each carries a sentence a later change in this same release made
false, or an overstatement, and each ships verbatim into the CHANGELOG.
Every frontmatter block is byte-identical. See "Round 2" and "Round 3"
below.

## Measurement, taken on `origin/main` `244d516df` before any edit

**Instrument.** The TypeScript compiler API (6.0.3), ⛔ not grep. It
builds one program per `tsconfig.json`: the 40 workspace packages,
`apps/console` and the three examples, 44 programs over 2015 non-test
source files. It ran on a BUILT tree (`turbo run build` 43/43, 0 cached)
with 0 unresolved-module diagnostics. It files every `.body` /
`.children` read (property access, string element access, object
destructuring) under the declared type of its receiver: 520 channel
reads were filed.

- **`InputSchema`: 0 `body` / 0 `children` reads.**
`InputShorthandSchema` is never a receiver: the `email` / `password`
wrappers are `any`-typed and hand the node to the `input` renderer,
which reads it as `InputSchema`.
- **Lit controls fire in the same run:** `ButtonSchema` 1, `DivSchema`
1, `CardSchema` 1, `ContainerSchema` 1 (each `children`).
- **Receiver reachability:** the same programs see `InputSchema` as the
receiver of 23 reads over 17 keys: `defaultValue`, `description`,
`error`, `id`, `inputType`, `label`, `max`, `maxLength`, `min`, `name`,
`pattern`, `placeholder`, `readOnly`, `required`, `step`, `value`,
`wrapperClass`. Neither channel is among them, so the zero is a reading,
not blindness.

**Every registration that claims `input`, bare and namespaced**
(`check:registry-bare-names --json`, 1657 files, 424 claims, 0
contested):

- `input`: sole claimant `ui:input`, a literal
`ComponentRegistry.register('input', InputRenderer, …)` in
`renderers/form/input.tsx`. The hop is typed (`InputRenderer` takes
`schema: InputSchema`), not `any`. No `field:input` or other claimant
exists; `ai:input` is a different key, a guarded protocol placeholder.
- `email` / `password`: `ui:email` / `ui:password` own the bare keys;
`field:email` / `field:password` stand down with `skipFallback: true`.

**Does any path pass the node's content through? No.**

- `SchemaRenderer` destructures `children` and `body` out of the props
bag before the spread.
- The only pass-through `InputRenderer` gives the native element is
`toFormControlDomProps(inputProps)`, a whitelist pick of
`SDUI_DOM_PASS_THROUGH_KEYS` plus `name` / `disabled`; `children` is not
on it.
- The `email` / `password` wrappers only spread `props` and
`props.schema` into `InputRenderer`.
- The registration declares no `children` slot input (objectui#9910).
- No in-source host builds an `input` node by spreading another node.
The literal `input` nodes in `form.tsx`, `cli`'s `init` scaffold and the
dashboard / designer config panels are assembled key by key, and none
carries a channel.
- **The 75 `any`-typed channel reads were attributed file by file.**
None can receive an `input` node's channel:
- app-shell nav / search trees, and HTTP request, error or email `body`
payloads;
- React `children` destructured off component props (`view:simple`'s
renderer, `react-page`'s wrapper), which `SchemaRenderer` never fills
from a node;
- the metadata-admin preview canvases, which read a page block's
`properties.children` or a page draft's root `children` to list and
append blocks for selection, not to render a node's channel;
- the page-container and sectioning registrations in `containers.tsx`,
`page.tsx` and `semantic.tsx`, and the html-elements factory, whose tag
list excludes `input` by name (its own comment lists `input` among the
tags it never registers);
- `core`'s schema builder (card / grid / flex builders; `InputBuilder`
has no children setter);
- `record:alert`'s own `props.body`, the runner's nav items, and the
vscode validator.
- **Generic traversers** (`validateChildren` in core, `sdui-parser`'s
parse / validate, `cli validate`) walk children to validate. They render
nothing and are node-agnostic, as for every family-D row already landed.

**Producers.** `pnpm census:body-dialect --keys
input,email,password,ui:input,ui:email,ui:password,div,card,page,button`
read 9016 files. The key population is printed by the tool itself, so
objectui#9545's blindness does not apply.

- Nodes on the six input keys: `input` 321, `email` 75, `password` 60,
`ui:email` 1, `ui:password` 17, `ui:input` 0. **0 author `body`, 0
author `children`.**
- The controls fire in the same pass: `children` on `div` 172, `card`
183, `page` 51, `button` 5.
- Real producers: 0. Nothing was migrated.

## The shorthand's own pair: type-redundant now, kept on purpose

`InputShorthandSchema` inherits `InputSchema` through `OmitDeclared` on
the TypeScript face and `.omit()` on the mirror, so it now inherits the
new pair. It already declared its own (PR objectui#10897).

- **Redundant as a type:** on the built d.ts both are the same `?:
never`, and the new `Eq` pin makes `tsc` hold that.
- **Not redundant as text.** The repo's rule for a restated tombstone is
the one `neitherContentChannelGuidance`'s docblock gives for restating
`body` over `BaseSchema`: restate when the inherited message misdirects
this node's author. The helper's contract spells the node and its route
into the message. The inherited string names `input` and the `ui:input`
route; the shorthand's names `email` / `password` and the wrapper route.
- **Decision:** keep both members and rewrite their docblocks (TS) and
comment (zod) to say they are type-redundant and why they stay. The
shorthand's old `body` docblock gave a reason that became false with
this change (that the inherited refusal names `children` as the remedy),
and it is replaced.
- **Pinned:** a CONTROL row asserts that an author of `email` /
`password` reads the message naming those two nodes and not the `input`
one, so deleting the restatement turns it red.
- Counter-precedents, noted: `UiCalendarSchema` restates nothing and
inherits `calendar`'s message, which names it. objectui#8072 deleted the
shorthand's `wrapperClass` restatement once `InputSchema` carried the
key; that was a live key with one description, not a refusal message
naming a node.

## The `OmitDeclared` propagation, measured on the BUILT d.ts

A compiler-API probe over `packages/types/dist/form.d.ts` reads the
heritage of `InputShorthandSchema` on its own: the base type the checker
gives for `OmitDeclared` of `InputSchema` minus `type` / `inputType`,
before the interface's own members.

| built d.ts | `InputSchema.children` | heritage `.children` | shorthand
face `.children` |
|---|---|---|---|
| base `244d516df` | `SchemaNode \| SchemaNode[]` | `SchemaNode \|
SchemaNode[]` | `undefined` (its own restated tombstone) |
| this branch | `undefined` | `undefined` | `undefined` |

A consumer-side probe resolves `@object-ui/types` the way
`packages/components` does (`--traceResolution`:
`packages/types/dist/index.d.ts`). There, `input` + `children`, `input`
+ `body` and `email` + `children` each answer their `@ts-expect-error`,
and a lit control (`const n: number = 'x'`) fires.

## Red on base, then green — one-shot, the restore proven by state

The predictions were written to a file BEFORE the mutation. The mutation
was `git checkout 244d516` of the two declaration files (`form.ts`,
`zod/form.zod.ts`), with every test at HEAD. It was proven by both blob
hashes equal to BASE and by anchor counts: `INPUT_NEITHER_CHANNEL` 0,
the new TS docblock marker 0. The run sat under `trap … EXIT INT TERM`
with absolute paths.

| reader | mutated (BASE declarations) | restored (HEAD `72dba894a`) |
|---|---|---|
| vitest, `content-channel-input-9256.test.ts` | **RED** exit 1: 6
failed / 13 passed | **GREEN** 19 / 19 |
| `tsc -p packages/types/tsconfig.test.json` | **RED** exit 2: TS2578 on
the `inputChildren` pin, TS2322 on the `Eq` pin for `children`; 0
elsewhere (`zod-mirror-parity` included) | **GREEN** exit 0 |
| built d.ts probe (types rebuilt; dist markers 0 then 2) |
`InputSchema` and heritage `children` accepted | refused |

- **The 6 red rows:** `input.children` refused at its path, its message,
`.describe()` equality, every-value, and the root + nested
`AnyComponentSchema` row; plus the `input.body` message row. The base
message is `BaseSchema`'s `Did you mean` pointer.
- **Green on base, by design:** `input.body` refused at its path,
every-value and `AnyComponentSchema` (`BaseSchema` already refused
`body`), and the shorthand message CONTROL (its own pair is unchanged).
- **One prediction was recorded as uncertain and read, not assumed:**
`input.body` `.describe()` equality is GREEN on base, because
`BaseSchema`'s alias refusal also describes with its own message.
- ⚠️ `?: never` is erased before vitest runs: the TypeScript half is
read by `tsc` alone. `tsc --listFiles` counts the new pin file 1 under
`tsconfig.test.json` and 0 under `tsconfig.json`, so it is a real
assertion under the `type-check` script.
- **Restore:** proven by both blob hashes equal to HEAD, `git diff HEAD`
empty and `git status` clean.

## Texts

- **Family-D test header.** The `InputSchema` bullet now records the
plain-`Omit` hold-out ground in the past tense, the E3 slice's
`OmitDeclared` repair, and this slice's narrowing, pinned in the new
file. The `DetailViewSchema` bullet records that the E3 slice attributed
the `any` hop directly and narrowed it there. The two `ui:calendar`
remarks name `OmitDeclared` for the TS face, `.extend()` for the mirror,
and say which file pins which half. The third `ui:calendar` remark (the
old TRIPWIRE note) was already in the past tense and is unchanged.
- **`PartialSchema`'s docblock.** The triage's reading is kept as the
triage's. The re-spelling claim is marked FALSE, with `OmitDeclared` as
the counter-example. The docblock now says that re-spelling the alias
would narrow a published type, a contract change not made in a comment
correction, and that objectui#6397 is closed. The alias itself is
unchanged.
- **Bounded in-place fix: `partial-schema-collapse-pin.test.ts` header**
(comment only; its assertions are about the alias as written and do not
move). All four conditions hold:
1. It is the same defect as the carried item: the same sentence, one hop
away. The corrected docblock points readers at this file.
2. The fix is mechanical, and its shape is pinned by the carried
correction.
  3. No open PR touches the file.
4. Same gate family: types `tsc` and vitest, no new verification
surface.

**Evidence, a one-shot probe against the built d.ts (not shipped):** the
exact spelling the header named as collapsing, `{ type } & { [K in keyof
T as K extends 'type' ? never : K]?: T[K] }`, declares 62 properties at
`ObjectGridSchema` and 28 at `ButtonSchema`, where the shipped
`PartialSchema` declares 1 at each. It refuses `label: 42` on
`ButtonSchema` and `objectName: 42` on `ObjectGridSchema` (both
`@ts-expect-error` used; tsc exit 0).

## Gates at HEAD `72dba894a` (round 1; round 2's are above) (exit codes
captured by redirect-then-capture; heavy runs through the shared verify
lock)

| gate | result |
|---|---|
| `@object-ui/types` build (tsc + vite + dist completeness) | exit 0,
134 emitted files verified |
| `@object-ui/types` `type-check` (`tsc --noEmit` + examples +
`tsconfig.test.json`) | exit 0 |
| `pnpm exec vitest run packages/types/` (parity, the family-D / E3 /
input pins, the 8072 and 8762 shorthand pins) | 265 files / 5873 tests,
exit 0 |
| `pnpm exec vitest run scripts/` (every `scripts/__tests__` gate suite,
a superset of those whose corpus holds a touched file) | 177 passed + 2
skipped of 179 files / 5322 tests, exit 0 |
| downstream consumer type-check: the packages naming `InputSchema`
(`components`, `core`) plus those authoring `input` nodes or configs
(`cli`, `plugin-dashboard`, `plugin-designer`) and `fields`, `react`,
`types`, against the rebuilt `dist` | 8 × `type-check: Done`, exit 0 |
| `@object-ui/types` lint (`eslint .`, JSON) | 338 files, 0 errors; the
9 `form.ts` warnings sit outside every added hunk |
| `check:handler-key-reads` | exit 0; 128 arms, 50 / 50 reads judged, 0
unjudged |
| `check:control-bytes` · `check:new-line-citations` (0 new) ·
`changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` | exit 0
each |
| `check:spec-symbols` · `check:component-surface-parity` ·
`check:readme-exports` · `check:registry-bare-names` ·
`check:prompt-keys` · `check:doc-types` · `check:test-path-roots` ·
`check:esm-specifiers` · `check:vi-mock-*` · `type-check:coverage` |
exit 0 each |
| `check:doc-snippets` (678 / 678) · `check:doc-examples` ·
`check:skill-examples` · `check:doc-fences` · `check:doc-example-ids` ·
`check:sdui-registration-pins` · `pnpm check` (CLI self-check) | exit 0
each |
| governed guard `--test` over the 7 paths | NOT GOVERNED (lit control
`AGENTS.md`: exit 3) |

- **The gate list was derived by hand.** objectstack's
`dispatch-gates.mjs` refuses an objectui answer by design (exit 2), so
the list comes from objectui's `package.json` and `.github/workflows/`.
- `check:changeset-claims` is report-only. It names 18 pending
changesets that mention a touched file; each was read, and none is
falsified as a record of its own change. The two that carry a claim this
slice bears on are in the Acceptance notes.
- **NOT MEASURED, left to CI:** the 8-way `pnpm test` shards beyond
`packages/types` and `scripts/`, `test:dist`, and E2E.
`check:sdui-registration-pins` read the console built at base; this diff
touches no registration and no `sideEffects` array.

## Round 2: four prose edits, head `6eb3397b4`

These come from contract review `5865087439` ③. The seat took them into
this PR because each ships verbatim into the CHANGELOG. They are prose
only: no code, no test logic, and every frontmatter block is
byte-identical (the `name: bump` lines at base and head compare equal).

1. **`.changeset/9256-input-content-channels.md`, and the Clause-②
sentence above.**
- Old: "an authored child list on it rendered nothing: no error, no
warning, no element."
- New: "an authored child list on it rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it."
- Measured with a one-shot probe, not committed. It built the manifest
from the live registry the way `container-declaration-ratchet.test.tsx`
does, then ran `validateTree` and `compile`.
- `input`, `ui:input`, `email` and `password` carrying `children`: each
draws exactly `not-a-container`.
     - The same four without `children`: none.
     - `div` carrying `children` (control): none.
- The JSX `input` tag with a child: `not-a-container`, severity
`warning`.
2. **`.changeset/6397-partial-schema-collapse-pin.md`.** A dated note
follows the "Why a pin and not a repair or a retirement" paragraph: a
generic re-spelling of this `Omit` does not collapse, objectui#9256.
- Spelling: `OmitDeclared` is spelled exactly as quoted, in
`packages/types/src/form.ts`.
- Counts: the re-spelled alias declares 28 properties at `ButtonSchema`
and 62 at `ObjectGridSchema` on this head and on `main` `733fd5ac6`. The
shipped alias declares 1 at each.
- "Not true when written": at that entry's own commit (`c8ea8af9c`),
under the same TypeScript 6.0.3 its lockfile pins, the same spelling
declares 27 and 61. Those are the source counts in that entry's own
table. Both re-spellings refuse `label: 42` (the `@ts-expect-error` is
used), and a lit control on the shipped alias admits it.
- "Later in this same release": the E3 entry and the 6397 entry are both
pending on `main` (`git cat-file -e` exit 0 for each; control on an
absent path: exit 128).
3. **`.changeset/9256-content-channel-family-d.md`.** A dated note
follows the AMENDED paragraph, in that file's own convention, naming the
hold-outs this card later narrowed in this release.
   - Six of the nine names, in `nine-holdouts-six-narrow.md`.
- `list` and `timeline`, in `9256-list-timeline-content-channels.md`.
This is one more than the reviewer's draft, measured.
   - `DetailViewSchema` and the shorthand faces, in the E3 entry.
   - `InputSchema`, in this PR.
- A compiler-API probe over the source reads `body` / `children` as
`undefined` with their own `?: never` members on `TextSchema`,
`ImageSchema`, `IconSchema`, `TabsSchema`, `AccordionSchema`,
`CalendarSchema`, `ListSchema`, `TimelineSchema`, `DetailViewSchema`,
`InputShorthandSchema` and `InputSchema`. The zod faces are pinned in
the family-D, E3 and input pin files.
- `ButtonSchema` and `AppComponentSchema` still resolve `children` to
`SchemaNode | SchemaNode[]` (`ButtonSchema`'s zod mirror declares its
own `children` union; `AppComponentSchema`'s extends `BaseSchema`, which
declares one). That is the note's "still accept `children`" sentence.
4. **`.changeset/8499-node-slot-registered-arms.md`.** "`inputType` is
deliberately NOT declared on this arm" is false on `main`.
- `form.zod.ts` declares `inputType:
retirementTombstone(SHORTHAND_INPUT_TYPE_REFUSAL)` on
`InputShorthandSchema`, and `form.ts` declares `inputType?: never`. Both
files are identical on `main` and at the base.
- A runtime probe: `inputType` is a member of the mirror shape. `{ type:
'password', inputType: 'text' }` is refused with `invalid_type` at path
`inputType`, the message opening with the objectui#8762 guidance. Bare
`{ type: 'password' }` (control) parses.
- objectui#8762's entry landed a day after 8499's (2026-09-10 against
2026-09-09), and both are pending on `main`.
- A dated note in the file's own form follows the `InputShorthandSchema`
bullet, the same place its objectui#10756 note sits under the
`HtmlElementSchema` bullet.

| gate at `6eb3397b4` | result |
|---|---|
| `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s)
added, 3 modified, 0 deleted". Each of the three modified entries prints
the same declaration at base and now (`@object-ui/types: patch`,
`minor`, `minor`), so no declaration is lost: its case 2, a correction
on purpose. |
| `changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` ·
`check:control-bytes` · `check:new-line-citations` | exit 0 each |
| the 24 `scripts/__tests__` suites that name `.changeset` (the
changeset gates' own suites, the polarity census, control bytes, pending
literals, docs and workflow suites) | 24 files / 913 tests, exit 0 |

## Round 3: two more prose edits, head `876f6d7ea`

These come from delta review `5865466392` ③ flags 2 and 3. They are
prose only, and both frontmatter blocks are byte-identical.

1. **`.changeset/9256-content-channel-family-d.md`: a second dated note,
directly after the AMENDED paragraph** and before round 2's note. The
headline is "the two twins no longer take `body` — objectui#6771".
- Old, still in the AMENDED paragraph: "The two twins are unchanged and
go on inheriting `body` as the content slot".
- New note: objectui#6771 retired `body` on `BaseSchema` itself, so both
twins refuse `body` on both faces. The TypeScript face does it through
`BaseSchema`'s `body?: never`; each mirror does it by name, pointing at
`requestBody`.
- Measured, TypeScript: a compiler-API probe over the source gives
`BaseSchema` its own `body?: never`. `ChatbotEnhancedSchema` and
`ChatbotFloatingSchema` resolve `body` to `undefined` and declare only
`children?: never` themselves.
- Measured, zod, with a one-shot runtime probe (deleted): each twin's
mirror refuses `body` with `invalid_type` at path `body`, and the
message names `requestBody`. `requestBody` parses (control).
- Provenance, from `git log -S`: `BaseSchema`'s `body?: never` and both
twins' mirror tombstones were introduced by `2acd8e109` (objectui#6771,
2026-09-19). The AMENDED paragraph was written on 2026-09-17
(`c42554e94`), and `6771-retire-body-child-list-dialect.md` is pending
on `main`. So "later in this same release" holds.
- The reviewer's draft also cited objectui#9659. That card did not
introduce the twin refusals, so the note cites objectui#6771 alone.
- Round 2's note opens "most of the hold-outs above have since been
narrowed". It is still true beside the new one: it names what this card
narrowed, and it ends with `button` and `AppComponentSchema`, which
still accept `children`.
2. **`.changeset/9256-e3-residual-content-channels.md`, in place.**
- Old: "an authored child list on them rendered nothing: no error, no
warning, no element."
- New: "an authored child list on them rendered nothing, with no
render-time error or warning and no element; only the parser tier's
`not-a-container` warning (objectui#9910) noticed it."
- Measured for all twelve keys, not just the two in round 2. A one-shot
runtime probe (deleted) loaded every plugin registration (`plugin-grid`,
`-form`, `-kanban`, `-map`, `-tree`, `-view`, `-gantt`, `-calendar`,
`-charts`, `-detail`, and `components`). It built the manifest from the
live registry and ran `validateTree`.
- Each of the twelve is known to the registry, and none declares a
`children` input.
- Carrying `children`, each draws `not-a-container` (severity
`warning`). Without it, none does. `div` carrying `children` (control)
draws nothing.
- `object-chart`, `email` and `password` also draw one `unknown-prop` in
both legs, from the probe's own `objectName` fixture key. It is not part
of the difference.

| gate at `876f6d7ea` | result |
|---|---|
| `check-changeset-overwrite` | exit 0 (report-only), "1 changeset(s)
added, 4 modified, 0 deleted". Each modified entry prints the same
declaration at base and now, so no declaration is lost. |
| `changeset:check` · `check-changeset-presence` ·
`check:changeset-claims` · `check:pending-changeset-literals` ·
`check:control-bytes` · `check:new-line-citations` | exit 0 each |
| the 24 `scripts/__tests__` suites that name `.changeset` | 24 files /
913 tests, exit 0 |

## Serial constraints

- **Re-mapped at branch time (`244d516df`), before each round's push,
and again before round 3's edits.** At round 3 the open PRs are
objectui#10921, objectstack-ai#10915, objectstack-ai#10914, objectstack-ai#10912, objectstack-ai#10911, objectstack-ai#10910, objectstack-ai#10908, objectstack-ai#10906,
objectstack-ai#10901, objectstack-ai#10777 and objectstack-ai#10278. objectstack-ai#10904, objectstack-ai#10902 and objectstack-ai#10891 have merged since
the first map.
  - None touches any of the five changesets this PR adds or edits.
- One touches a source path here: PR objectui#10914 edits
`packages/types/src/zod/form.zod.ts`, in two comment / `.describe()`
hunks on the `wrapperClass` key (in `InputSchema` and in the
`InputShorthandSchema` restatement note). Neither overlaps or touches
this PR's hunks, and `git merge-tree` of this head (`876f6d7ea`) against
objectstack-ai#10914's head is clean.
- PR objectui#10908 edits `zod-mirror-parity.test.ts` and
`zod/index.zod.ts`, neither of which this PR touches.
- **The release PR objectui#5400** (1732 files, enumerated to the end,
last updated 2026-09-18) touches none of the source paths. As release
consumption it deletes pending changesets, three of the four edited here
among them (not the E3 entry); that is not a hunk overlap.
- **`origin/main` moved four commits since the base** (`9f0c84a44`,
`7ea8118f7`, `8522396c0`, `733fd5ac6`). None touches `packages/types` or
the changesets edited here, and `git merge-tree` of this head
(`876f6d7ea`) against `733fd5ac6` is clean. `main` has not moved since.

## Acceptance notes: out of scope, not fixed here

- **The `PartialSchema` docblock and its pin file still sequence the
alias's repair on objectui#5155.** That card is closed as a duplicate of
objectui#5250, whose ruling keeps the tolerant face on renderer props.
objectui#8347 (open) is the card on removing the index signature from
the authoring face. "Once objectui#5155 removes the root index
signature" therefore names a card that will not do it. Noted, not
edited: rewriting the sequencing is a judgement about objectui#5250 /
objectstack-ai#8347, not a comment correction. Carrier: none.
- **The same "no error, no warning, no element" sentence lives in
published refusal messages and docblocks,** not only in changesets.
Examples: the family-D `retirementTombstone` strings on the zod mirrors
(the chatbot twins' included) and the matching TypeScript docblocks.
Wherever the node's registration declares no `children` input, the
parser tier's `not-a-container` warning did fire. Correcting those
strings is a published-message change, not prose, and it is not this
PR's. Noted, not edited. Carrier: none.

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

---------

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

Projects

None yet

2 participants