Repository navigation
feat(types,layout,app-shell): a navigation entry with no label renders its target's current label at render time (objectui#9868) - #11186
Conversation
…s its target's current label at render time (objectui#9868) @objectstack/spec 17.5.0 made NavigationItemSchema.label optional with a declared semantic: absent means the entry shows the current label of what it opens (view, else object / dashboard), present means verbatim. - types: NavigationEntryItem.label is optional; the zod mirror accepts a label-less entry for every type the spec does, fills nothing in, still requires id, and still refuses an empty label. menuItemToNavigationItem stops writing ''. - layout: resolveNavItemLabel resolves an absent label through a new resolveTargetLabel resolver (view -> object / dashboard -> machine name); action entries, search and the AppSchemaRenderer tab bar go through it. - app-shell: useNavTargetLabel reads the metadata cache (object schema, merged list views, dashboards); UnifiedSidebar and nav:menu both wire it. Navigation sync no longer materialises label || pageName / dashboardName. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…bel; the navigation guide states the optional label (objectui#9868) navigation-model's "should reject missing required label" is inverted to accept a label-less entry that parses with no label key. The objectui#7918 nested-superRefine pin keeps its subject with a missing id as the trigger, now with a positive control. The app-navigation guide no longer says label is mandatory, and says what an entry without one shows. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…any, drop the now-unused resolveLabel import (objectui#9868) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…rms that carry it, not as a spec member path (objectui#9868) check:spec-symbols reads a dotted spec citation as a key of the symbol's own shape; the nav-item union has none, so the passage now names the entry arms. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
…spec, caught by objectui validate rather than the save door (objectui#9868) Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
|
changeset-claim-re-read
|
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Inputs: card #9868 (body and all ten comments — ruling Check-runs on the head, all completed: 40 success, 3 skipped (dependabot and the two coverage jobs), 0 failure. Type Check, Lint, Build & E2E, all eight Test shards, Test (dist pins), Spec Main Shape Gate, Build Docs, Changeset Declaration / Bump Policy / Fixed Group Check / Claim Re-read / Overwrite Report, Governed Surface Queue Guard, Doc Snippet Type Check and README Export Check are green. ① Derived judgments
② Semver levelClause-②: yes (widening)
③ Boundary flags
Findings
Implemented-by: VERDICT: FAIL Generated by Claude Code |
…less nav entry through the shared resolver; layout's changeset level is minor (objectui#9868) Contract review 5907554475. The palette rendered resolveKeyedI18nLabel( item.label, t), which answers undefined for an absent label, so an entry NavigationSyncEffect now writes without a label drew a blank row whose cmdk value read "page undefined my_page". The search page listed such an entry by its machine name via item.label || item.objectName || ... Both now call resolveNavItemLabel with the useNavTargetLabel resolver the sidebar and nav:menu share; a present label resolves as before. @object-ui/layout goes patch -> minor in the changeset: the diff adds exported types and an optional prop. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
…av pins store text, the mobile tab bar shows the backstop (objectui#9868) The patch-round sweep asks every FIXED reader to carry a pin. useNavPins and AppSchemaRenderer's mobile tab bar were routed through resolveNavItemLabel in the first round without one. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
Contract reviewServed-tier: Round 2 of record Check-runs on the head: 43, all completed — 40 success, 3 skipped (dependabot and the two coverage jobs), 0 failure. Type Check, Lint, Build & E2E, all eight Test shards, Test (dist pins), Spec Main Shape Gate, Build Docs, the five Changeset gates, Governed Surface Queue Guard, Doc Snippet Type Check, Skill Guide Path Check and README Export Check are green. This matches the dev's read. ① Derived judgmentsThe three round-1 findings — each closed at the head.
The raw-reader sweep — its instrument, control and classification hold; no runtime reader is missed.
Unchanged since round 1 — re-cited from Observation, not a finding: the PR body was not updated for the patch round — its file count, ablation table (A1–A6 at ② Semver levelClause-②: yes (widening)
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…-less, never with the target's machine name (objectui#11201, stage 1) (objectstack-ai#11225) Part of objectstack-ai#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 objectstack-ai#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 objectstack-ai#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: ```ts 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](https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #9868
Clause-②: yes (widening)
What this does
Executes ruling letter A on the card (comment 5731297631, the cloud#2021 ruling; spec half landed as
@objectstack/spec17.5.0, whichmainresolves since PR objectui#11086). A navigation entry with NOlabelnow shows the CURRENT label of what it opens, resolved at render time; a present label renders verbatim. Nothing is stored, no sentinel, noinheritedflag, no new metadata member.@object-ui/types(widening,minor):NavigationEntryItem.labelis optional. The zod mirror's superRefine (thefor (const key of ['id', 'label'] as const)loop) now requiresidonly; a label-less entry of every entry type passes and parses with NOlabelkey (nothing filled in at parse time). An EMPTY label stays refused, now with a message that says to omit the key (the spec's base accepts''; this is a stated divergence, kept from before).menuItemToNavigationItemstops writinglabel: ''for a legacy item with no label.@object-ui/layout:resolveNavItemLabelgains an absent arm, reached beforeresolveLabel(which would readlabel.keyoff undefined). It asks a newresolveTargetLabelresolver (newNavigationRendererprop; exported typesNavLabelTarget/NavTargetLabelResolver) the spec's ladder: the view's label, else the object's / dashboard's label, else the machine name. The arm is keyed on ABSENCE only, so an authored label is never swapped for the target's label, including one spelled like the machine name (route B stays refused). Action entries, the search filter andAppSchemaRenderer's mobile tab bar now go through the same function (all three readitem.labelraw and would throw or mislabel on an absent label).@object-ui/app-shell: newuseNavTargetLabelhook, wired into both surfaces (UnifiedSidebar's twoNavigationRenderermounts and thenav:menublock). Navigation sync writes nolabelkey for an unnamed page or dashboard (it used to writelabel || pageName/label || dashboardName); an author label is still written; stored labels are untouched. The toast text keeps its display fallback.content/docs/guide/designing-app-navigation.mdsaidlabelis mandatory; it now says what an entry without one shows.Premises re-measured on
origin/main0ffc423 (the order's Zone 2)@objectstack/spec/ui,safeParsewith and withoutlabel):object,dashboard,page,url,report,action,component,docandgroupall accept a missing label; the separator still refuses alabel; the parsed entry carries nolabelkey. objectui's type enum has 8 entry types (docis not among them, see Acceptance notes); the mirror now accepts a label-less entry for all 8, pinned per type against the spec in the new test file.useObjectLabel()'sobjectLabel/viewLabel/dashboardLabelonly look up the translation bundle and fall back to whatever is passed in, which on the nav path was the nav entry's own label. What does hold the metadata label is theMetadataProvidercache:objects(the object schema, with the eagerly loadedviewtype merged intolistViews, keyedobject.key) anddashboards.useNavTargetLabelreads those: the view is matched withresolveViewIdfrom@object-ui/core(the matcher the object page itself uses), an inline locale-map label resolves through the spec'sresolveI18nLabelin the active language, and the result still passes the same bundle lookup, so the nav shows the name the target's own page shows. No request per nav entry:objectsis already read by the sidebar's capability gate;dashboardsis read only when a dashboard target is asked about (one list for the type, from the cache; the console'sNavigationSyncEffectloads it anyway).useNavigationSync: the only caller of the add helpers isNavigationSyncEffect, and it never passes a label, so every page / dashboard it added was written with its machine name aslabel.label: item.label || ''writers inpackages/types/src/app.ts: all three are inmenuItemToNavigationItem, a RENDER-time conversion of the legacymenukey insideAppSchemaRenderer(not a stored write; exported publicly though). Changed to omit the key.tscsweep: withlabeloptional, the builds and type-checks found two raw readers outside the claim's list:AppSchemaRenderer's mobile tab bar (resolveLabel(item.label)) anduseNavPins(label: item.labelinto the favorite record, whosenavlabel is a placeholder the sidebar never renders). Both now go throughresolveNavItemLabel. The first dispatch'sAppSidebarasymmetry no longer applies: that sidebar was removed (pinned byappSidebarRemoved-5817).What a label-less entry renders, per type
objectnaming a viewviewNameobjectobjectNamedashboarddashboardNamepage/reportpageName/reportName(the ruling names inheritance for view, object and dashboard only)url/component/actionurl/componentRef/actionDef.actionNamegroup, or an entry missing its targetidPins (each with its control)
packages/types/src/__tests__/nav-label-optional-9868.test.ts: per type, spec accepts a label-less entry and so does the mirror, with nolabelkey after parse; control: a present label (even'account') kept verbatim.idstill required;''refused with the omit-the-key message; separator unchanged;menuItemToNavigationItemwrites no empty label.navigation-spec-parity(label-less entry now accepted, id-less still refused),navigation-model(was "should reject missing required label"),app-wizard-separator-layout-10867(the type-level control now expects an id-less entry to be the compile error).zod-lazy-getter-identity-7918's nested-superRefine pin keeps its subject with a missingidas the trigger plus a positive control.packages/layout/src/__tests__/NavigationRenderer.labelInheritsTarget-9868.test.tsx: the ladder, the rename (a new resolver answer over the SAME nav tree), the per-type backstops (and the resolver is never asked for them), the controls (explicit label verbatim; explicit machine-name label verbatim and the target resolver not consulted), an unlabelled action entry renders, and search finds an unlabelled entry by the text it shows.packages/app-shell/src/views/__tests__/nav-label-inherits-target-9868.render.test.tsx: BOTH surfaces (the sidebar'sNavigationRendererwired withuseNavTargetLabel, andnav:menu) under oneMetadataCtx: a view labelled 客户管理仪表盘 renders 客户管理仪表盘; re-rendered with the view renamed, the new name shows on both; object / dashboard inheritance; machine-name backstop; authored labels verbatim.packages/app-shell/src/layout/__tests__/UnifiedSidebar.navLabelInheritsTarget-9868.test.tsx: the realUnifiedSidebarwiring, rename included.packages/app-shell/src/hooks/__tests__/useNavigationSync.labelInherits-9868.test.tsx:NavigationSyncEffectadds a page and a dashboard with nolabelkey; control:syncPageCreated/syncDashboardCreatedwith an author label store it.Ablations (one-shot, not kept), at
444bdb9b5Mutations through
ablation-replace.mjs(anchor must hit, disk-verified, restore armed on EXIT/INT/TERM); tests resolve@object-ui/*topackages/*/srcthrough the rootvitest.config.mtsaliases, so no dist rebuild sits between mutation and measurement. Every leg went red and every restore provedblob == HEADwith an emptygit diff HEAD:labellabelissue)targetLabelUnifiedSidebardropsresolveTargetLabel(both mounts)nav:menupasses no target resolverlabel: label ‖ nameagainVerification, at
444bdb9b5(clean tree)turbo run build --filter=@object-ui/app-shell^...(28 tasks) exit 0.type-checkexit 0 for@object-ui/types,@object-ui/layout,@object-ui/app-shell, and the other packages whose source names the navigation types (@object-ui/components,@object-ui/plugin-designer,@object-ui/runner). Eachtsconfig.test.jsonincludessrc/**/*.test.ts(x);--listFilesOnlylists every changed test file.vitest run packages/types/: 284 files / 6520 tests passed.vitest run packages/layout/src/ packages/app-shell/src/hooks/ packages/app-shell/src/layout/plus the twonav:menurender files: 125 files / 1036 tests passed..ts/.tsxfiles: 0 errors (56 warnings, all in pre-existing lines; 0 in the new files). Measured narrowing: the config's population is**/*.{ts,tsx}, the JSON report counts 18 files, and the config enables no type-aware parsing (parserOptions.project/projectServiceabsent), so untouched files' verdicts cannot move. The repo-widepnpm lintis CI's.check-changeset-presence,check-control-bytes,check:new-line-citations(0 new),check:spec-symbols,check:vi-mock-specifiers,check:vi-mock-inherit,check:vi-mock-override-shape,check:test-path-roots,check:unreferenced-sources,check:changeset-claims,check:pending-changeset-literals,check:doc-types,check-changeset-no-major. NOT MEASURED:check:readme-exports, exit 1 solely on "type entry not on disk" for seven packages outside this build closure (0 fabricated, 0 wrong-path among those it could judge); this diff touches no README.check-governed-queue-guard.mjs --testover the 20 changed paths answers NOT GOVERNED.Acceptance notes (observations, not filed)
skills/objectui/guides/app-composition.mdsays every nav item needs anidand alabel("both required byNavigationItemSchema").skills/**is governed, so editing it would turn this PR into one that waits for a maintainer approval, and it is outside the claim's file surface. It needs a one-line skills-only follow-up. Following the stale line still yields valid metadata (a label is still accepted), so nothing breaks meanwhile.normalizeNavshowsunnamed), its nav canvas (navLabelshows the positional item row), the Studio nav surface (resolveSurfacereadsString(node.label ?? ''), so empty text; fenced out here while PR objectui#11179 lands) andplugin-designer'sNavigationDesigner(resolveKeyedI18nLabel(item.label), so empty text). Reported for the seat, not touched.isCustomized), which can translate it when a bundle carries that key. It never reads metadata, it predates the ruling, and retiring it would change what apps with labels materialised by the old sync show. Left as it was; a maintainer question if the spec's "present ⇒ verbatim" is meant to cover translation too.docentries: the spec's union has adoctype; objectui's hand-written type enum does not, so the mirror refuses a spec-valid{ type: 'doc', book }entry attype(measured on the built dist). Pre-existing and unrelated to the label; objectui#9868 is not where it belongs.tincluded). For a present label this differs from before only when a host passes bothsearchQueryand label resolvers; no current mount does both.objectui#9868 carries the ruling; cloud#2021 is the cloud half that waits on this PR landing. Implemented in session
https://claude.ai/code/session_011p7ikEivgXefNDaE5S5Uec(seatdomain:ui#2, dispatch round 9).Generated by Claude Code