Skip to content

fix(app-shell,plugin-designer): standard nav entries are stored label-less, never with the target's machine name (objectui#11201, stage 1) - #11225

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11201-stop-machine-name-labels
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11201-stop-machine-name-labels

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #11201
Clause-②: no

Stage 1 of ruling B on objectui#11201 (triage record 5910404448, execution note 5910537969). The card stays open: stage 2 is routed by triage on the census below. This PR does not touch the renderer's isCustomized guard in resolveNavItemLabel, and it does not touch /meta's translateApp or the inline locale map.

What changes

Ruling B keeps one rule, the spec's: a present label renders verbatim, and an absent one inherits its target's current label at render time, which is the text that localises. Step 1 asks every objectui producer to write a standard entry with no label, never the target's machine name. On main, the console's navigation sync already did (objectui#9868). Three other producers did not. They wrote the target's own text, which is its machine name for a draft or unlabelled object:

Producer (symbol, file) Wrote before this PR Writes now
Console navigation sync: NavigationSyncEffect calling syncPageCreated / syncDashboardCreated (newPageNavigationItem / newDashboardNavigationItem, packages/app-shell/src/hooks/useNavigationSync.ts) no label since objectui#9868 (PR #11186, a8198de22). From the hook's first commit (7c788f5de, 2026-02-23) until then: label: label || pageName / label || dashboardName unchanged. New pin: the stored entry renders its target's localized label under en and zh-CN
Public syncPageCreated / syncDashboardCreated / …AllApps with an explicit label the author label verbatim. No in-tree caller passes one (the effect above is the only caller) unchanged
Studio Create app with "add objects": buildAppSkeleton (studio-design/skeletons.ts) fed by loadPackageObjects (StudioDesignSurface.tsx) label = the object's label, or its machine name for a draft object (d.name) or an unlabelled one (o.label ?? o.name) no label key. The seed is names only
Studio nav-item panel, binding an object to a placeholder entry: StudioNavItemInspector (StudioDesignSurface.tsx) replaced the placeholder with the object's label (o.label ?? o.name, so the machine name for a draft or unlabelled object) removes the placeholder label key. A typed label and a locale map are kept as authored
App wizard: generateNavFromObjects (plugin-designer/src/AppCreationWizard.tsx), fed by CreateAppPage / EditAppPage with label: obj.label || obj.name label: pluralLabel || label, so the machine name for an unlabelled object a label only for a declared plural label that differs from the object's label and from its machine name (trimmed, case-insensitive): inheritance cannot produce a plural. Otherwise no label key
Navigation Designer quick add (newNavItem), Studio canvas "Add nav item" (AppNavCanvas) with AppNavInspector's target picker a placeholder (New TYPE / New item), never the target's name untouched: not a machine-name producer, and objectui#11196 / PR #11207 hold these files
DashboardDesignPage's label: dashboardName ?? '' (named in the dispatch as a lead) not a nav entry: it is the dashboard document's own seed, on a not-found branch that returns before anything is saved n/a

Display: the sidebar renders each new label-less entry with the text the old stored copy showed, and additionally in the viewer's language and after a rename. Pinned under en and zh-CN below.

Serial constraint: land after objectui#11196

objectui#11196 (this seat, branch pushed, no PR yet) makes the designer surfaces draw a label-less entry through the runtime's rule: the app preview, the Studio nav canvas and inspector, the Studio Interfaces rail, the Navigation Designer and the app wizard's Navigation step. Until it lands, such an entry shows as Item N on the Studio nav canvas and as a blank row in the wizard's Navigation step. This PR makes Create app, the bind and the wizard write such entries. Both branches edit StudioDesignSurface.tsx, in different hunks: objectui#11196 edits the rail's NavTree, this PR edits loadPackageObjects and StudioNavItemInspector. git merge-tree --write-tree reports no conflict against objectui#11196's head 2a6f0c5fb or against main e6bc087a3. No write site sits inside objectui#11196's hunks.

Pins

  • useNavigationSync.standardEntryLocalizes-11201.test.tsx: the real NavigationSyncEffect stores a new dashboard's entry with no label key. That stored entry is then rendered by NavigationRenderer, wired as UnifiedSidebar wires it (the useObjectLabel resolvers plus useNavTargetLabel), under a real I18nProvider carrying a zh-CN bundle loaded the console's way. It shows Pipeline Health under en and 管道健康 under zh-CN.
  • StudioDesignSurface.standardNavEntries-11201.test.tsx: seeded entries carry no label, the app parses through the spec's AppSchema, and the stored entries render Account under en, 客户 under zh-CN, and draft_thing for the unlabelled object. Binding an object to a New item placeholder leaves no own label key, and the entry parses through NavigationItemSchema. Controls: a typed label is kept (a locale map is already pinned by the objectui#11148 test).
  • AppWizard.standardNavEntries-11201.test.tsx: drives the real CreateAppPage. The unlabelled, singular-labelled and plural-equals-name objects' entries carry no label. Control: a distinct declared plural (Accounts) is written.
  • createConformance.test.ts: the nav-seeded skeleton case now passes name-only seeds.

Ablation (one-shot, from committed 384d81e73, through ablation-replace.mjs: anchor hit, blob changed, restored to HEAD with git diff HEAD empty)

Leg Mutation Result
A sync: newDashboardNavigationItem writes label: label || dashboardName again 3 red, 2 green: stored-label pin red, en render red (shows pipeline_health), the objectui#9868 stored pin red. zh-CN stays green, because the guard translates a machine-name label, which is why the guard stays in stage 1
B scaffold: buildAppSkeleton writes label: o.name 2 red, 6 green: seeded-label pin red, en render red. zh-CN green (the guard again)
C wizard: const label = o.pluralLabel || o.label restored 3 red, 1 green: the three label-less cases red, the plural control green
D bind: the placeholder removal deleted 2 red: both placeholder-bind cases

Verification

All on 384d81e73, the head of this branch, from the worktree root:

Gate Result
closure build, turbo run build for the dependencies of @object-ui/app-shell and @object-ui/plugin-designer 28 of 28 tasks successful, lock verdict command-exit 0
pnpm --filter @object-ui/plugin-designer type-check (tsc --noEmit and tsconfig.test.json, which lists the new wizard pin) exit 0
pnpm --filter @object-ui/app-shell type-check (both projects; the test project lists both new app-shell pins and createConformance.test.ts) exit 0
vitest run packages/app-shell/src/hooks/ 53 files, 457 tests passed
vitest run packages/app-shell/src/views/studio-design/ 77 files, 439 tests passed
vitest run packages/plugin-designer/ 28 files, 203 tests passed
vitest run of the two other skeletons importers (createConformance.test.ts, flow-node-producers.label.test.tsx) 2 files, 20 tests passed
vitest run scripts/__tests__/ (this PR adds a changeset .md) 178 of 179 files reported, 5343 tests passed, 0 failed. The run hit its 300 s cap on date-only-zone-pins-10183.test.ts, which spawns child vitests. NOT MEASURED for that one file: it was queued for the lock again and not acquired within the 540 s budget. It reads date-only zone behaviour and nothing this PR touches
node scripts/markdown-test-inputs.mjs --audit exit 0
pnpm exec eslint --no-inline-config on the 7 changed source and test files 1 error and 21 warnings, all present on the base as well. Linting the base blobs of the two files that report any gives 1 error and 19 warnings for StudioDesignSurface.tsx and 0 and 2 for AppCreationWizard.tsx: the same rules at shifted lines, so this diff adds none. The config enables no type-aware linting, so this diff cannot change a verdict on an untouched file
node scripts/check-changeset-presence.mjs exit 0, 1 changeset declared for 2 released packages
pnpm check:control-bytes exit 0
pnpm check:new-line-citations 0 new citation(s), exit 0
pnpm check:i18n-keys exit 0 (no string changed)

Declared narrowing: the dispatch's lead named all of packages/app-shell/src/views/metadata-admin/. This PR changes no source there. The only test there that imports a changed module is createConformance.test.ts (and flow-node-producers.label.test.tsx imports skeletons for buildFlowSkeleton), found by a git grep of importers, and no module under metadata-admin/ imports skeletons or StudioDesignSurface. The rest of that directory, and the whole-package runs, are CI's.

Acceptance notes

Census (step 2), reported before any conversion

Scanner: a TypeScript-AST walk of every navigation: [...] literal and its children, applying the guard's own test (plain-string label, trimmed and lower-cased, equal to viewName for an object entry naming a view, else objectName; dashboardName for a dashboard entry). The population was enumerated from the same ref it reads (git grep at objectstack origin/main 1741c5dcb), and cross-checked by a second enumeration (App.create( and : App =), which found the same seven files. The matcher's own control is the one hit it reports.

Population Nav entries Entries the guard reads Label equals the target's machine name Writer
objectstack examples: crm, multi-package, showcase, todo 76 33 1: todo nav_overdue, label Overdue, viewName: 'overdue' author
objectstack platform apps: account, setup, studio 49 6 0 author (platform source)
objectstack scaffolds: create-objectstack blank template (no app), objectstack generate app (writes Title + s for the object) 0 / 1 per app 1 per app 0 (the generated plural never spells the snake-case name) author (a file the author owns)
objectui console preview fixture (preview-samples.ts, not stored) 7 3 0 fixture
page / report entries in the above, label against pageName / reportName (the guard does not read these) 29 not read 0 author
Stored app navigation in production NOT MEASURED: needs production data
A seeded console NOT MEASURED: not booted. A fresh seed stores the packaged apps counted above, and the sync writes nothing without user CRUD

Split by writer, what is readable here: platform sync 0 (it writes only into stored rows at runtime; 0 sync-shaped ids in any source), apply_blueprint NOT READABLE (the objectstack-ai/cloud repo answers 403 through the proxy), author 1. apply_blueprint's declared contract is readable: BlueprintNavItemSchema.label in @objectstack/spec says "Nav entry label (defaults to the target label/name)", so the applier is declared to store the target's label or name when a blueprint omits one.

Can a stored row tell the writers apart? Not from any recorded provenance. Partly from signatures and history:

  • Per entry: nothing. A nav item is a strict object with no room for a mark, and the spec's describe says "No stored inherited flag".
  • Per row: sys_metadata's source, managed_by, created_by, updated_by and version describe the whole app document's last write, not an entry.
  • Sync signature (strong, but a heuristic): the sync has always written id nav_page_ or nav_dash_ followed by a 13-digit epoch-ms stamp and a counter, with icon FileText or LayoutDashboard, and until objectui#9868 the page's or dashboard's machine name as the label. The shape is the same from 7c788f5de to a8198de22. An author would have to type the timestamp id by hand.
  • Weaker signatures: the Studio scaffold's id is nav_ plus the object name (since 7a6837cfa, 2026-07-05). A Studio-canvas entry's id is nav_item_N. The wizard's id equals the object name. Authors write ids like these by hand.
  • History: sys_metadata_history keeps a full snapshot per version (operation_type, source, recorded_by, change_note), so diffing versions finds the write that introduced an entry. But the sync, the Studio and the wizard all save through the same saveItem('app', ...) door as the signed-in user, so those columns do not separate them. sys_metadata_commit (ADR-0067) groups AI-turn writes (actor, ai_model, an event range), so blueprint writes are attributable if the cloud applier writes through commits (not readable) and history is retained (not measured).

Twin check (step 3): no

objectstack packages/spec/src/system/i18n-resolver.ts at origin/main 1741c5dcb, translateApp's node walk:

if (typeof node.id === 'string') {
  const translated = lookupNavLabel(bundle, appName, node.id, opts);
  if (translated) next.label = translated;
}

lookupNavLabel reads only apps.APP.navigation.ID.label down the locale chain. Nothing compares a label's text with objectName, viewName or dashboardName, so it is not the equal-to-name rule. One observation for triage: it also gives a label-less node the bundle text when an entry exists for its id, ahead of inheritance, while the execution note orders the id-keyed bundle only for a present label. The CLI's extractor (walkNavigation in packages/cli/src/utils/i18n-extract.ts) emits such keys only for a present label, so only a hand-written bundle entry reaches this path.

Noted, not filed

  • The app wizard's Navigation step: objectui#11196's read (resolveNavItemLabel(item) with no target resolver) shows objectName for a label-less generated entry, though the wizard holds each object's label in availableObjects. This lies in that branch's hunk.
  • StudioNavItemInspector's Label input writes label: '' when emptied (renamedLabel with an empty value), and shows no inherited text as a placeholder. objectui#11196's clearedLabel handles AppNavInspector only. Read from the code, not measured.
  • AppNavInspector's target picker keeps AppNavCanvas's New item placeholder as a present label. Both files are held by objectui#11196.
  • The wizard's declared-plural copy is a present label: it does not localise or follow a rename, because inheritance has no plural rung. No carrier.

Generated by Claude Code

…l-less, never with the target's text

Ruling B (objectui#11201, stage 1): a present navigation label renders
verbatim, and an absent one inherits its target's current label at render
time, which is the text that localises. The objectui producers of standard
entries stop storing the target's text:

- Studio's Create app scaffold (buildAppSkeleton, fed by the pillar's
  package-object list) seeds object entries with no label. It stored the
  object's label, or its machine name for a draft or unlabelled object.
- StudioNavItemInspector removes a placeholder label when an object is
  bound, instead of adopting the object's label. Authored labels and
  locale maps are kept.
- AppCreationWizard writes a generated entry's label only for a declared
  plural label that differs from the object's label and machine name. The
  host pages hand it the machine name for an unlabelled object.

Pins: the console sync's stored entry renders its target's localized
label under en and zh-CN, and each producer's written entry carries no
label key.

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

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 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/6681-declared-lazy-marketplace-routes.md

  • names views/studio-design/StudioDesignSurface.tsx → packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx — edited by this change

    Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first, and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves group in apps/console/vite.config.ts isolates those co-tenants so the three declared-lazy pages chunk by their own dynamic-only reachability. The console eager closure moves 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three per-chunk ceilings unmoved.

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.

Compared the checked-out tree with 1263e405d (merge-base with origin/main): 7 file(s) changed outside .changeset/, read against 1816 pending declaration(s) that publish a body (2426 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3576.6 KB 3607.4 KB
Main entry chunk (gzip) 149.7 KB 350 KB
Entry file index-CJk48ASU.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.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
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.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
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.54KB 3.46KB
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) 568.99KB 136.03KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.91KB 63.54KB
fields (index.js) 261.19KB 66.27KB
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.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 40.88KB 11.46KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.73KB 37.05KB
plugin-designer (index.js) 216.02KB 44.49KB
plugin-detail (index.js) 242.01KB 63.55KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 172.30KB 44.19KB
plugin-gantt (index.js) 172.43KB 42.85KB
plugin-grid (index.js) 230.79KB 63.39KB
plugin-kanban (index.js) 48.44KB 15.11KB
plugin-list (index.js) 116.28KB 28.88KB
plugin-map (index.js) 22.90KB 7.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 32.26KB 9.42KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
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.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
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) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.17KB 2.73KB
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) 22.61KB 7.40KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
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) 19.82KB 7.15KB
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: 384d81e7349a5b5f9ac35c21c3faae72435f0445
Local-runs: none

Inputs: card objectui#11201 (body; comments 5909828367, 5910404448, 5910537969, 5912136282, 5913605784, 5913610243), PR #11225 (body, 8 files, the diff against main at merge-base 0389650f3), and the head's 43 check-runs, read 2026-09-30T14:55Z: 40 success (Test shards 1–8, Test, Test (dist pins), Type Check, Lint, Build & E2E, Spec Main Shape Gate, Changeset Bump Policy / Declaration / Claim Re-read / Fixed Group Check / Overwrite Report, and the rest), 3 skipped (coverage ×2, dependabot), 0 failed, 0 in progress. Base and objectstack context read as blobs at objectui 0389650f3 and objectstack 1741c5dcb; nothing built, run or re-run. Rendered 2026-09-30T15:00Z.

① Derived judgments

Producers (step 1), each against ruling B. Every nav-entry write site at the merge-base (a git grep of objectName: / dashboardName: / pageName: / reportName: / viewName: and of navigation: writers over packages/*/src and apps/*/src, tests excluded):

  1. Console sync, newPageNavigationItem / newDashboardNavigationItem (useNavigationSync.ts:52–72): ...(label ? { label } : {}) at base, label-less since objectui#9868; NavigationSyncEffect is the only in-tree caller and passes no label. Unchanged, newly pinned. Right.
  2. Studio Create app, buildAppSkeleton fed by loadPackageObjects: base label: o.label with o.label ?? o.name and d.name upstream; now { id, type: 'object', objectName }, and AppNavSeed is { name } only. Right.
  3. Studio bind, StudioNavItemInspector: base label: isPlaceholder && obj ? renamedLabel(label, obj.label, locale) : label; now bindObject deletes label when isPlaceholder and the label is absent or a plain string. Right (the bind reading is below).
  4. App wizard, generateNavFromObjects, fed by CreateAppPage.tsx:30 and EditAppPage.tsx:67 (label: obj.label || obj.name, pluralLabel: obj.pluralLabel): base label: o.pluralLabel || o.label; now generatedEntryLabel, a label only for a declared plural that spells neither o.label nor o.name (trimmed, case-insensitive). Both pages reach it through mergeNavWithObjects; the pin drives CreateAppPage, and EditAppPage shares the function. Right.
  5. Correctly left alone, since none writes the target's text: AppNavCanvas.addItem (t('engine.appNav.newItem'), id nav_item_N), NavigationDesigner.newNavItem (label: labelFor(type)), the wizard's own addNavItem (New Group / New Link, group and url only), AppNavInspector's picker (patch({ objectName: v, object: undefined }), no label written), renameNavigationItems (rewrites pageName / dashboardName, never label). DashboardDesignPage.tsx:47 is the dashboard document's own label on the !dashboard branch that early-returns before any save, not a nav entry, as the dev read it. The other objectName: current.name sites in StudioDesignSurface.tsx (3695, 3843) are object-view / object-form schemas. No producer is missed.

The wizard's declared-plural exception, the reading. Stage 1's letter is "an ABSENT label for a standard entry, never the target's machine name". generatedEntryLabel refuses a plural that spells o.name or o.label, so it never writes the machine name and never writes the copy inheritance would show: within the letter. Against the one rule: the plural is a present label, so it renders verbatim, follows no rename of pluralLabel, and localises only through an id-keyed bundle entry (apps.APP.navigation.OBJECT.label, the wizard's id being the object name) or an inline map. That was already true at base: a plural differs from the machine name, so the retired guard's isCustomized was true and rendered it verbatim, and this PR changes nothing about it; what it removes is the singular copy. Inheritance has no plural rung (inheritedNavItemLabel: view label, object label, machine name), so the alternative, never writing it, would change every wizard-created list entry from Projects to Project, which stage 1 was told not to do ("Display does not change"). My reading: within ruling B's stage 1, and no reliance on the retired guard. What remains is a real but pre-existing class: a platform-written present label that is not a machine name, distinguishable by signature (id equals objectName, label equals the object's pluralLabel). It belongs to stage 2's conversation (a plural rung in the spec's inheritance, or the wizard preferring inheritance), not to this diff. The dev carried it as "承接者:无 · Acceptance notes only"; I would give it a pointer in the census / stage-2 routing rather than none. The seat decides.

The bind: does it ever delete an author's label? dropLabel = isPlaceholder && (label === undefined || typeof label === 'string'); isPlaceholder = !curLabel || curLabel === 'New item' || curLabel === t('engine.appNav.newItem', locale). Deleted: a plain-string label that is '', the English sentinel, or the designer-locale sentinel. Kept: any other string (pinned: Clients), and every map, even one whose designer-locale entry is empty or the sentinel; where the base wrote the object's label into that locale, the map is now left as authored, which is strictly more conservative, and the objectui#11148 pin (label: ITEM_LABEL under zh, kept on bind) still passes. The one author text that can be dropped is a label typed as literally New item or its translation, which the base overwrote with the object's label anyway, so nothing is lost that the base kept. '' is a present label that renders empty (the sync's own docblock: "⛔ Never ''"); dropping it on bind is the right outcome. No.

Pins. useNavigationSync.standardEntryLocalizes-11201.test.tsx drives the real NavigationSyncEffect, reads the entry back from saveItem's call (not.toHaveProperty('label')), then renders that stored entry through NavigationRenderer wired as UnifiedSidebar wires it (useObjectLabel resolvers plus useNavTargetLabel) under a real I18nProvider whose zh-CN bundle is loaded through isSpecTranslationData / transformSpecTranslations: Pipeline Health under en, 管道健康 under zh-CN. That is the ruling's pin as written. StudioDesignSurface.standardNavEntries-11201.test.tsx: seeded entries carry no label, AppSchema accepts the skeleton, en renders Account and draft_thing, zh-CN renders 客户; binding a New item placeholder leaves no own label and the entry parses through NavigationItemSchema; a typed label is kept. AppWizard.standardNavEntries-11201.test.tsx drives the real CreateAppPage, parses the saved body with AppSchema, and pins the three label-less cases and the Accounts control. createConformance.test.ts seeds names only. Each producer has its pin. The ablation legs A to D each hit exactly the pins that guard the mutation; leg A's zh-CN staying green is what the guard does: at base resolveNavItemLabel finds a machine-name label not isCustomized and hands it to resolver(item.objectName, base), the bundle, which is precisely why stage 1 keeps the guard, and that green is not counted against the pin. Right.

The census (step 2), as a reading. The method is sound: a TypeScript-AST walk of navigation: [...] literals and their children, applying the guard's own test, over a population enumerated at the ref it reads. The population checks: git grep -l "navigation: \[" at objectstack 1741c5dcb over examples and packages/platform-objects names the same seven files (crm, multi-package core, showcase, todo; account, setup, studio). A regex read over those seven blobs (no AST, nothing run) finds the same 39 object- or dashboard-targeted entries the guard reads (6+1+20+6+6+0+0) and the same single hit: todo nav_overdue, viewName: 'overdue', label: 'Overdue', an author's. The NOT MEASURED rows are honest and correctly reasoned: production needs production data; a seeded console was not booted, and a fresh seed stores the packaged apps already counted while the sync writes nothing without user CRUD; apply_blueprint is NOT READABLE (the cloud repo answers 403), with its declared contract quoted right, BlueprintNavItemSchema.label: 'Nav entry label (defaults to the target label/name)' (packages/spec/src/ai/solution-blueprint.zod.ts:189). "Can the row tell writers apart" answers the ruling's question, and answers it in the shape the ruling's stage-2 fork needs: no recorded provenance per entry (a strict object, no stored inherited flag) or per row (sys_metadata columns describe the document's last write); the sync's nav_page_ / nav_dash_ epoch-ms id is a strong heuristic, the Studio's nav_ plus name and the wizard's id-equals-name are weak ones; history does not separate the sync, the Studio and the wizard from the signed-in user. That is the material triage routes stage 2 on, including its "back to the maintainer" branch. Sound.

The twin check (step 3). Read at objectstack 1741c5dcb, packages/spec/src/system/i18n-resolver.ts. translateApp's walk: if (typeof node.id === 'string') { const translated = lookupNavLabel(bundle, appName, node.id, opts); if (translated) next.label = translated; }. lookupNavLabel: pickData(bundle, code)?.apps?.[appName]?.navigation?.[navId]?.label for each code of localeChain(opts). Nothing reads objectName, viewName or dashboardName; nothing compares a label's text. Id-keyed only: not a twin, and it is one of the two routes the execution note keeps. Right.

② Semver level

Changeset 11201-standard-nav-entries-label-less.md: @object-ui/app-shell: patch, @object-ui/plugin-designer: patch, both in the fixed group; Changeset Bump Policy, Fixed Group Check, Declaration and Claim Re-read are all success. Exported surfaces: the app-shell index exports StudioDesignSurface (props unchanged), useNavigationSync and NavigationSyncEffect (unchanged); skeletons.ts (AppNavSeed, buildAppSkeleton) and StudioNavItemInspector are not on it (the inspector's docblock says so; the skeleton's importers at base are StudioDesignSurface.tsx and two tests). The plugin-designer index exports AppCreationWizard, CreateAppPage, EditAppPage and NavigationDesigner; generateNavFromObjects is module-private and AppCreationWizardProps is unchanged. What moves is the stored document these components write (no label on a standard entry, which the spec accepts), rendering the same text: a fix. Patch is right. Clause-②: no is right: no schema, prop, type or accepted set on an exported surface moves, and stored entries are not converted. No exported surface moves.

③ Boundary flags

open_questions: []. Each deviation and out-of-scope note, answered:

  1. The serial constraint: landing first is a user-visible regression, so the ACCEPT holds landing until objectui#11196 (PR fix(app-shell,plugin-designer): the designer's nav surfaces show a label-less entry's inherited label, the way the console draws it (objectui#11196) #11224) is on main. At base the Studio Interfaces rail (NavTree: labelText || surface?.name) shows a label-less object entry by its MACHINE NAME (account where Account was shown), the Studio canvas (AppNavCanvas) shows the positional Item N row for a truly absent label, and the wizard's Navigation step (resolveKeyedI18nLabel(item.label)) shows a blank row. Before this PR each of those surfaces showed the stored copy, in the very flows this PR changes: Create app with add-objects, the bind, the wizard. The runtime sidebar is unchanged. finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 at d78e28afd (PR fix(app-shell,plugin-designer): the designer's nav surfaces show a label-less entry's inherited label, the way the console draws it (objectui#11196) #11224) makes the rail (navEntryLabelText(node, locale, targetLabel) with useNavTargetLabel), the canvas, the inspectors and the wizard's step (resolveNavItemLabel(item, …, targetLabel) with labelOf built from draft.objects) draw the inherited text. Plainly: merge fix(app-shell,plugin-designer): the designer's nav surfaces show a label-less entry's inherited label, the way the console draws it (objectui#11196) #11224 first.
  2. Hunk overlap. finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 at d78e28afd: StudioDesignSurface.tsx hunks at 116–117 (imports) and 1379–1428 (NavTree); this PR's at 110–116 (line 113), 991–1009 (loadPackageObjects), 1445–1452, 1507–1549 and 1524–1551 (StudioNavItemInspector). The import edits sit on lines 113 and 116–117 with 114–115 unchanged between them, and no other ranges touch. AppCreationWizard.tsx: finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 at 44, 491, 511–531, 570–590 and 990; this PR at 98–138. Disjoint on both files. objectui#11204: its branch claude/issue-11204-draft-autosave-generation (c3df43a42) points at a main commit and carries no commit of its own yet, so there is nothing to overlap; the autosave change already on main (objectui#11189, 0389650f3) is this PR's merge-base. GitHub reports the PR mergeable against current main (behind by 11; main's moves in NavigationRenderer.tsx and NavigationDesigner.tsx are outside this file list).
  3. The narrowed metadata-admin/ run and date-only-zone-pins-10183.test.ts NOT MEASURED locally: answered by the head's check-runs. Test shards 1–8, Test (dist pins), Type Check and Lint are success on 384d81e73.
  4. The carrier notes. (a) The wizard's Navigation step showing objectName: routed to objectui#11196 and already answered there; its commit d78e28afd ("the app wizard's Navigation step hands its object labels to the inheritance rule") passes a targetLabel built from draft.objects. (b) StudioNavItemInspector's Label input writing label: '' when emptied, with no inherited placeholder: pre-existing at base (patch({ label: renamedLabel(label, e.target.value, locale) })), not a machine name, outside stage 1's letter; routed to objectui#11196's clearedLabel work, which the dev reads as covering AppNavInspector only. The seat should confirm finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 takes the Studio inspector too, or give it a card; it is not a finding against this diff. (c) AppNavInspector's picker keeping the New item placeholder: a placeholder, not a machine name, in finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196's file. Right to note, not to file. (d) translateApp giving a label-less node an id-keyed bundle label ahead of inheritance: not a real contradiction, and no card is needed. The execution note's rung 1 / rung 2 split says what B retires (the text match) and what it keeps (the id-keyed routes); the spec's one order, as recorded on this card by objectstack's seat (5913605784, objectstack#20849 / PR #20875, review PASS), puts the id-keyed bundle entry FIRST: "1. The id-keyed bundle entry apps.APP.navigation.ID.label, applied by translateApp at /meta. 2. Else the present label as authored. 3. Else the target's current label." That is what translateApp's code does, present label or not. The CLI's walkNavigation emits such a key only if (id && item.label), so only a hand-written bundle entry reaches that path, as the dev says. The pointer already on the card is enough; at most a one-line triage note that the execution note's rung 2 is subsumed by the spec's rung 1. (e) The wizard's plural copy: see ①; a pointer into stage 2's routing rather than none.
  5. The other deviations: the PR title's fix(app-shell,plugin-designer) scope matches the diff; a seeded console NOT MEASURED is honest and the census is scoped as a source reading; finding(app-shell,plugin-designer): the designer's nav surfaces show a label-less nav entry as blank, "unnamed" or its id — the entries objectui#9868 makes producible inherit their target's label only at runtime #11196 advancing to touch StudioDesignSurface.tsx is handled in item 2. Stage 1's two prohibitions hold: NavigationRenderer.tsx is not in the file list and the isCustomized guard is untouched; translateApp and the inline locale map are untouched.

Implemented-by: claude/issue-11201-stop-machine-name-labels
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 16:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit cb2f6fb Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11201-stop-machine-name-labels branch September 30, 2026 16:39
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…nslate-if-equal-to-name exception, and an inline locale-map label renders its text (objectui#11201) (objectstack-ai#11297)

Fixes objectstack-ai#11201

Clause-②: no

Stage 2 of ruling B: the director's ruling `5921167691`, on the ruling
record `5910404448` and its execution note `5910537969`. Claim
`5921933038`, branch `claude/issue-11201-nav-label-verbatim`. Stage 1
landed as PR objectstack-ai#11225 (`cb2f6fb5b`).

## What changes

`resolveNavItemLabel` in `packages/layout/src/NavigationRenderer.tsx`
now follows the spec's one order and keeps no rule of its own:

1. The id-keyed bundle entry `apps.APP.navigation.ID.label`.
`translateApp` applies it upstream, at `/meta`. Untouched here.
2. A present label: an inline locale map reads its locale's entry, and a
string renders verbatim.
3. An absent label inherits the target's current, localized label
(objectui#9868, unchanged).

- **Deleted:** the `isCustomized` arm and its comment. The arm looked a
present plain-string label up through the object, view or dashboard
resolver when its text equalled the target's machine name (trimmed,
case-insensitive). Nothing on `label` matches text against a name now.
- **Added:** a present map-valued entry label (`{ en: 'Accounts',
'zh-CN': '客户' }`, which the spec accepts) now reads through the spec's
`resolveI18nLabel`. Areas and the designer canvas already use that
resolver. The label used to reach the keyed resolver and render empty.
- **Kept, but not read:** `resolveObjectLabel`, `resolveDashboardLabel`
and `resolveViewLabel` (the props, and the matching positional
arguments). They are marked `@deprecated`, so callers compile and get
the ruled behaviour. Retiring them is open question 2 in the report.
- `packages/i18n/src/useObjectLabel.ts`: the one comment that named the
guard now says the guard is gone. Comment only.
- Changeset: `@object-ui/layout` patch. It states the visible effect and
the one-line fix: clear the entry's label so it inherits, or set the
wanted text. No conversion, no notice run.

## Mechanism finding: which locale a map label reads

The ruling's map pin says "renders the locale's value". This layer is
not given a locale. `NavigationRenderer` has no locale prop, and
`@object-ui/layout` has no i18n dependency by design (see the docblock
of `resolveAreaLabel` in `AppSchemaRenderer.tsx`). Within the ruled
parameters (patch, `Clause-②: no`, no public prop change), the map
resolves at the spec resolver's documented no-locale default: its `en`
entry, else `default`, else any entry. The area switcher in this package
does the same. So in the console, a zh-CN viewer sees the `en` entry of
a map-valued nav label.

Reaching the viewer's locale needs a new optional prop, with the locale
threaded from the console (`UnifiedSidebar` already holds `language`).
That widens the public surface, so it is not built here. It is open
question 1 in the report.

Measured pull: no app navigation in readable sources writes a map-valued
entry label. This is the objectstack pointer `5913605784`, re-read at
objectstack `5f6b63a6f`: in the files that declare `navigation`, the
only object-valued labels are in translation bundles.

## Pins

New file
`packages/layout/src/__tests__/NavigationRenderer.labelVerbatim-11201.test.tsx`:
- **VERBATIM, 4 rows.** A present label equal to its target's machine
name (object `account`, view `board`, dashboard `sales_overview`)
renders verbatim under `en` and under `zh-CN`. The console's convention
resolvers are wired as `UnifiedSidebar` wires them, and none is called.
Case and padding variants (`ACCOUNT`, `SALES_OVERVIEW`) stay as written.
Control in the same tree: an absent label inherits the target's
localized label in each locale, which shows the wiring is live.
- **MAP, 2 rows.** Map-valued object, dashboard, group and action labels
render the map's text. They never render `[object Object]`, empty text
or the target's label. Search finds them by that text.

Restated rows in `NavigationRenderer.navLabelOwnership.test.tsx` (they
pinned the retired convention):
- `still resolves an un-customized object label through
resolveObjectLabel` is now `renders a present object label equal to the
machine name verbatim, resolveObjectLabel unasked (objectui#11201)`.
- `keeps the same guard on the dashboard branch` is now `renders both
present dashboard labels verbatim, resolveDashboardLabel unasked
(objectui#11201)`.
- `still lets an authored object label win over resolveObjectLabel`: the
assertion is unchanged. Its comment no longer names the guard.

Unchanged and green:
`NavigationRenderer.labelInheritsTarget-9868.test.tsx` (absent labels
inherit, and an explicit label equal to the machine name stays verbatim
with echo resolvers), and the stage-1 pin
`useNavigationSync.standardEntryLocalizes-11201.test.tsx`.

## Ablation

Run from committed `54656bd7d` with objectstack's
`scripts/ablation-replace.mjs`. The layout tests import the renderer by
relative path, and the root vitest config aliases `@object-ui/layout` to
`src`, so no `dist/` is in the path.

- **Leg A: restore the `isCustomized` arm.** Over
`packages/layout/src/`: 6 failed, 287 passed. The 6 are exactly the 4
new VERBATIM rows and the 2 restated rows. The MAP rows stay green.
- **Leg B: drop the map resolution,** so every object label goes back
through the keyed resolver. Over `packages/layout/src/`: 2 failed, 291
passed. The 2 are exactly the MAP rows.
- Each leg: the anchor hit once and then zero times, and the blob
changed. The restore was proven: blob equals `HEAD`, and `git diff HEAD`
is empty.

## Verification

All gates ran from the worktree root, on head `54656bd7d`:

- `pnpm exec vitest run --reporter=verbose packages/layout/src/`: 30
files, 293 passed.
- Consumers outside layout (every test file that imports
`NavigationRenderer` or `resolveNavItemLabel`, plus
`packages/app-shell/src/layout/`, `packages/app-shell/src/chrome/`, the
`nav:menu` render tests, the Studio and designer nav-label pins,
`packages/plugin-designer/` and `packages/i18n/src/`): 173 files, 1966
passed, 13 skipped.
- Closure build `turbo run build --filter=@object-ui/layout^...
--filter=@object-ui/i18n^... --concurrency=2`: 8 of 8 successful. Then
`type-check` for `@object-ui/layout` and `@object-ui/i18n`: exit 0. `tsc
-p tsconfig.test.json --listFilesOnly` lists both touched layout test
files.
- `eslint` on the 4 changed source and test files: 0 errors and 28
warnings, all pre-existing. Linting the base blobs gives
`NavigationRenderer.tsx` 25 warnings against 23 at head (two `as any`
casts went with the arm), and the other files the same counts.
- `pnpm exec vitest run scripts/__tests__/` (the whole root suite,
because a changeset `.md` is added): 177 files passed and 2 skipped;
5371 tests passed and 2 skipped; lock VERDICT command-exit 0.
- `check-changeset-presence` exit 0. `check:control-bytes` exit 0.
`check:new-line-citations` reports 0 new citations, exit 0.
`markdown-test-inputs --audit` exit 0. `check-changeset-no-major` exit
0. `check:changeset-claims` is report-only: it names 3 pending
changesets that cite a touched file, and I read each paragraph; none is
falsified by this diff.

## Acceptance notes

- **Fork clause: not triggered.** No objectui producer on `main` writes
a machine-name label. The console sync effect passes no label. The
Studio skeleton and inspector bind and the wizard's
`generatedEntryLabel` were fixed by stage 1. Remaining `label:
d.name`-shaped writes are the Studio rail's object list and a dashboard
document seed, not nav entries.
- **Declared-type gap, measured, reported (not filed):**
`@object-ui/types` declares a nav entry's `label` as `string` (the type
and the zod mirror), narrower than the spec's `I18nLabel`.
`safeValidateSchema`, which `objectui validate` calls, refuses a
map-valued nav label (`navigation[0].label`: expected string, received
object). The spec's `NavigationItemSchema` accepts the same entry. The
new map pin casts its fixtures for that reason, and says so.
- The header comment of
`packages/app-shell/src/views/nav-menu-renderer.tsx` still says its
labels get "the same convention-based object / view / dashboard i18n
resolution the sidebar gets". That is no longer true for any label.
`UnifiedSidebar` and `nav-menu-renderer` still build the three unread
resolvers. Both files are outside this claim's file surface; they belong
with open question 2.
- `AppSchemaRenderer.tsx`'s `resolveAreaLabel` docblock quotes the old
`resolveObjectLabel` prop doc ("enables convention-based i18n
auto-resolution …"), which this PR rewrote. The docblock's point,
injection over an i18n dependency, still stands.
- `resolveNavItemLabel` still resolves objectui's keyed `{ key,
defaultValue }` reference through `t`, unchanged. The spec's nav label
refuses that shape and objectui's type does not declare it. The new code
tells it apart from a locale map by the two member names a map can never
carry.

---

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

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

Development

Successfully merging this pull request may close these issues.

2 participants