Repository navigation
fix(app-shell,plugin-designer): standard nav entries are stored label-less, never with the target's machine name (objectui#11201, stage 1) - #11225
Conversation
…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>
|
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 objectui#11201 (body; comments ① Derived judgmentsProducers (step 1), each against ruling B. Every nav-entry write site at the merge-base (a
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". The bind: does it ever delete an author's label? Pins. The census (step 2), as a reading. The method is sound: a TypeScript-AST walk of The twin check (step 3). Read at objectstack ② Semver levelChangeset ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…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>
Part of #11201
Clause-②: no
Stage 1 of ruling B on objectui#11201 (triage record
5910404448, execution note5910537969). The card stays open: stage 2 is routed by triage on the census below. This PR does not touch the renderer'sisCustomizedguard inresolveNavItemLabel, and it does not touch/meta'stranslateAppor the inline locale map.What changes
Ruling B keeps one rule, the spec's: a present
labelrenders 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. Onmain, 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:NavigationSyncEffectcallingsyncPageCreated/syncDashboardCreated(newPageNavigationItem/newDashboardNavigationItem,packages/app-shell/src/hooks/useNavigationSync.ts)a8198de22). From the hook's first commit (7c788f5de, 2026-02-23) until then:label: label || pageName/label || dashboardNamesyncPageCreated/syncDashboardCreated/…AllAppswith an explicit labelbuildAppSkeleton(studio-design/skeletons.ts) fed byloadPackageObjects(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)labelkey. The seed is names onlyStudioNavItemInspector(StudioDesignSurface.tsx)o.label ?? o.name, so the machine name for a draft or unlabelled object)labelkey. A typed label and a locale map are kept as authoredgenerateNavFromObjects(plugin-designer/src/AppCreationWizard.tsx), fed byCreateAppPage/EditAppPagewithlabel: obj.label || obj.namelabel: pluralLabel || label, so the machine name for an unlabelled objectlabelonly 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 nolabelkeynewNavItem), Studio canvas "Add nav item" (AppNavCanvas) withAppNavInspector's target pickerNew TYPE/New item), never the target's nameDashboardDesignPage'slabel: dashboardName ?? ''(named in the dispatch as a lead)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 Non 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 editStudioDesignSurface.tsx, in different hunks: objectui#11196 edits the rail'sNavTree, this PR editsloadPackageObjectsandStudioNavItemInspector.git merge-tree --write-treereports no conflict against objectui#11196's head2a6f0c5fbor againstmaine6bc087a3. No write site sits inside objectui#11196's hunks.Pins
useNavigationSync.standardEntryLocalizes-11201.test.tsx: the realNavigationSyncEffectstores a new dashboard's entry with nolabelkey. That stored entry is then rendered byNavigationRenderer, wired asUnifiedSidebarwires it (theuseObjectLabelresolvers plususeNavTargetLabel), under a realI18nProvidercarrying a zh-CN bundle loaded the console's way. It showsPipeline Healthunder en and管道健康under zh-CN.StudioDesignSurface.standardNavEntries-11201.test.tsx: seeded entries carry nolabel, the app parses through the spec'sAppSchema, and the stored entries renderAccountunder en,客户under zh-CN, anddraft_thingfor the unlabelled object. Binding an object to aNew itemplaceholder leaves no ownlabelkey, and the entry parses throughNavigationItemSchema. Controls: a typed label is kept (a locale map is already pinned by the objectui#11148 test).AppWizard.standardNavEntries-11201.test.tsx: drives the realCreateAppPage. The unlabelled, singular-labelled and plural-equals-name objects' entries carry nolabel. 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, throughablation-replace.mjs: anchor hit, blob changed, restored to HEAD withgit diff HEADempty)newDashboardNavigationItemwriteslabel: label || dashboardNameagainpipeline_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 1buildAppSkeletonwriteslabel: o.nameconst label = o.pluralLabel || o.labelrestoredVerification
All on
384d81e73, the head of this branch, from the worktree root:turbo run buildfor the dependencies of@object-ui/app-shelland@object-ui/plugin-designercommand-exit 0pnpm --filter @object-ui/plugin-designer type-check(tsc --noEmitandtsconfig.test.json, which lists the new wizard pin)pnpm --filter @object-ui/app-shell type-check(both projects; the test project lists both new app-shell pins andcreateConformance.test.ts)vitest run packages/app-shell/src/hooks/vitest run packages/app-shell/src/views/studio-design/vitest run packages/plugin-designer/vitest runof the two otherskeletonsimporters (createConformance.test.ts,flow-node-producers.label.test.tsx)vitest run scripts/__tests__/(this PR adds a changeset.md)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 touchesnode scripts/markdown-test-inputs.mjs --auditpnpm exec eslint --no-inline-configon the 7 changed source and test filesStudioDesignSurface.tsxand 0 and 2 forAppCreationWizard.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 filenode scripts/check-changeset-presence.mjspnpm check:control-bytespnpm check:new-line-citations0 new citation(s), exit 0pnpm check:i18n-keysDeclared 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 iscreateConformance.test.ts(andflow-node-producers.label.test.tsximportsskeletonsforbuildFlowSkeleton), found by agit grepof importers, and no module undermetadata-admin/importsskeletonsorStudioDesignSurface. 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 itschildren, applying the guard's own test (plain-string label, trimmed and lower-cased, equal toviewNamefor an object entry naming a view, elseobjectName;dashboardNamefor a dashboard entry). The population was enumerated from the same ref it reads (git grepat objectstackorigin/main1741c5dcb), 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.nav_overdue, labelOverdue,viewName: 'overdue'create-objectstackblank template (no app),objectstack generate app(writesTitle+sfor the object)preview-samples.ts, not stored)pageName/reportName(the guard does not read these)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_blueprintNOT READABLE (theobjectstack-ai/cloudrepo answers 403 through the proxy), author 1.apply_blueprint's declared contract is readable:BlueprintNavItemSchema.labelin@objectstack/specsays "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:
sys_metadata'ssource,managed_by,created_by,updated_byandversiondescribe the whole app document's last write, not an entry.nav_page_ornav_dash_followed by a 13-digit epoch-ms stamp and a counter, with iconFileTextorLayoutDashboard, and until objectui#9868 the page's or dashboard's machine name as the label. The shape is the same from7c788f5detoa8198de22. An author would have to type the timestamp id by hand.nav_plus the object name (since7a6837cfa, 2026-07-05). A Studio-canvas entry's id isnav_item_N. The wizard's id equals the object name. Authors write ids like these by hand.sys_metadata_historykeeps 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 samesaveItem('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.tsatorigin/main1741c5dcb,translateApp's node walk:lookupNavLabelreads onlyapps.APP.navigation.ID.labeldown the locale chain. Nothing compares a label's text withobjectName,viewNameordashboardName, 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 (walkNavigationinpackages/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
resolveNavItemLabel(item)with no target resolver) showsobjectNamefor a label-less generated entry, though the wizard holds each object's label inavailableObjects. This lies in that branch's hunk.StudioNavItemInspector's Label input writeslabel: ''when emptied (renamedLabelwith an empty value), and shows no inherited text as a placeholder. objectui#11196'sclearedLabelhandlesAppNavInspectoronly. Read from the code, not measured.AppNavInspector's target picker keepsAppNavCanvas'sNew itemplaceholder as a present label. Both files are held by objectui#11196.Generated by Claude Code