Skip to content

feat(types,layout,app-shell): a navigation entry with no label renders its target's current label at render time (objectui#9868) - #11186

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-9868-nav-label-inherits-target
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-9868-nav-label-inherits-target

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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/spec 17.5.0, which main resolves since PR objectui#11086). A navigation entry with NO label now shows the CURRENT label of what it opens, resolved at render time; a present label renders verbatim. Nothing is stored, no sentinel, no inherited flag, no new metadata member.

  • @object-ui/types (widening, minor): NavigationEntryItem.label is optional. The zod mirror's superRefine (the for (const key of ['id', 'label'] as const) loop) now requires id only; a label-less entry of every entry type passes and parses with NO label key (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). menuItemToNavigationItem stops writing label: '' for a legacy item with no label.
  • @object-ui/layout: resolveNavItemLabel gains an absent arm, reached before resolveLabel (which would read label.key off undefined). It asks a new resolveTargetLabel resolver (new NavigationRenderer prop; exported types NavLabelTarget / 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 and AppSchemaRenderer's mobile tab bar now go through the same function (all three read item.label raw and would throw or mislabel on an absent label).
  • @object-ui/app-shell: new useNavTargetLabel hook, wired into both surfaces (UnifiedSidebar's two NavigationRenderer mounts and the nav:menu block). Navigation sync writes no label key for an unnamed page or dashboard (it used to write label || pageName / label || dashboardName); an author label is still written; stored labels are untouched. The toast text keeps its display fallback.
  • Docs: content/docs/guide/designing-app-navigation.md said label is mandatory; it now says what an entry without one shows.

Premises re-measured on origin/main 0ffc423 (the order's Zone 2)

  • Spec 17.5.0 accept set, per type (installed @objectstack/spec/ui, safeParse with and without label): object, dashboard, page, url, report, action, component, doc and group all accept a missing label; the separator still refuses a label; the parsed entry carries no label key. objectui's type enum has 8 entry types (doc is 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.
  • Do the shell's resolvers read the target's METADATA label? No. useObjectLabel()'s objectLabel / viewLabel / dashboardLabel only 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 the MetadataProvider cache: objects (the object schema, with the eagerly loaded view type merged into listViews, keyed object.key) and dashboards. useNavTargetLabel reads those: the view is matched with resolveViewId from @object-ui/core (the matcher the object page itself uses), an inline locale-map label resolves through the spec's resolveI18nLabel in 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: objects is already read by the sidebar's capability gate; dashboards is read only when a dashboard target is asked about (one list for the type, from the cache; the console's NavigationSyncEffect loads it anyway).
  • useNavigationSync: the only caller of the add helpers is NavigationSyncEffect, and it never passes a label, so every page / dashboard it added was written with its machine name as label.
  • The label: item.label || '' writers in packages/types/src/app.ts: all three are in menuItemToNavigationItem, a RENDER-time conversion of the legacy menu key inside AppSchemaRenderer (not a stored write; exported publicly though). Changed to omit the key.
  • tsc sweep: with label optional, the builds and type-checks found two raw readers outside the claim's list: AppSchemaRenderer's mobile tab bar (resolveLabel(item.label)) and useNavPins (label: item.label into the favorite record, whose nav label is a placeholder the sidebar never renders). Both now go through resolveNavItemLabel. The first dispatch's AppSidebar asymmetry no longer applies: that sidebar was removed (pinned by appSidebarRemoved-5817).

What a label-less entry renders, per type

entry text
object naming a view view label, else object label, else the viewName
object object label, else objectName
dashboard dashboard label, else dashboardName
page / report pageName / reportName (the ruling names inheritance for view, object and dashboard only)
url / component / action the url / componentRef / actionDef.actionName
group, or an entry missing its target its id

Pins (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 no label key after parse; control: a present label (even 'account') kept verbatim. id still required; '' refused with the omit-the-key message; separator unchanged; menuItemToNavigationItem writes no empty label.
  • Inverted, not deleted: 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 missing id as 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's NavigationRenderer wired with useNavTargetLabel, and nav:menu) under one MetadataCtx: 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 real UnifiedSidebar wiring, rename included.
  • packages/app-shell/src/hooks/__tests__/useNavigationSync.labelInherits-9868.test.tsx: NavigationSyncEffect adds a page and a dashboard with no label key; control: syncPageCreated / syncDashboardCreated with an author label store it.

Ablations (one-shot, not kept), at 444bdb9b5

Mutations through ablation-replace.mjs (anchor must hit, disk-verified, restore armed on EXIT/INT/TERM); tests resolve @object-ui/* to packages/*/src through the root vitest.config.mts aliases, so no dist rebuild sits between mutation and measurement. Every leg went red and every restore proved blob == HEAD with an empty git diff HEAD:

leg mutation result
A1 validator re-requires label 13 failed / 26 passed (39); the "still requires an id" pin reddened in the MORE-diagnostics direction (an id-less entry drew an extra label issue)
A2 renderer never asks targetLabel 8 failed / 16 passed (24); backstop and control cases stayed green
A3 UnifiedSidebar drops resolveTargetLabel (both mounts) 1 failed / 1 passed; control green
A4 nav:menu passes no target resolver 2 failed / 2 passed. A first attempt was REFUSED by the tool (the replacement was a substring of the anchor, so it proved no change) and ran no test; re-run with a distinct replacement
A5 hook ignores the view's metadata label 2 failed / 4 passed (both surfaces' rename cases)
A6 sync writes label: label ‖ name again 1 failed / 1 passed; author-label control green

Verification, at 444bdb9b5 (clean tree)

  • Build: turbo run build --filter=@object-ui/app-shell^... (28 tasks) exit 0.
  • type-check exit 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). Each tsconfig.test.json includes src/**/*.test.ts(x); --listFilesOnly lists 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 two nav:menu render files: 125 files / 1036 tests passed.
  • eslint on the 18 changed .ts/.tsx files: 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 / projectService absent), so untouched files' verdicts cannot move. The repo-wide pnpm lint is CI's.
  • Gates exit 0: 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.
  • Not governed: check-governed-queue-guard.mjs --test over the 20 changed paths answers NOT GOVERNED.

Acceptance notes (observations, not filed)

  • One published line this change makes false is NOT edited here: skills/objectui/guides/app-composition.md says every nav item needs an id and a label ("both required by NavigationItemSchema"). 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.
  • Designer surfaces do not show the inherited label for a label-less entry the runtime now names: the metadata-admin App preview (normalizeNav shows unnamed), its nav canvas (navLabel shows the positional item row), the Studio nav surface (resolveSurface reads String(node.label ?? ''), so empty text; fenced out here while PR objectui#11179 lands) and plugin-designer's NavigationDesigner (resolveKeyedI18nLabel(item.label), so empty text). Reported for the seat, not touched.
  • The pre-existing i18n convention path is unchanged: a PRESENT label equal to the target's machine name is still offered to the bundle lookup (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.
  • doc entries: the spec's union has a doc type; objectui's hand-written type enum does not, so the mirror refuses a spec-valid { type: 'doc', book } entry at type (measured on the built dist). Pre-existing and unrelated to the label; objectui#9868 is not where it belongs.
  • Search now matches the text a row shows (resolvers and t included). For a present label this differs from before only when a host passes both searchQuery and 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 (seat domain:ui#2, dispatch round 9).


Generated by Claude Code

…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
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 9 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/10867-app-wizard-saves-spec-app.md

  • names packages/types/src/__tests__/app-wizard-separator-layout-10867.test.ts → packages/types/src/__tests__/app-wizard-separator-layout-10867.test.ts — edited by this change

    Pinned in packages/types/src/__tests__/app-wizard-separator-layout-10867.test.ts and packages/plugin-designer/src/__tests__/AppWizard.specDocument-10867.test.tsx.

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

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

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

.changeset/6335-starred-apps-nav-exclusion.md

  • names UnifiedSidebar.tsx → packages/app-shell/src/layout/UnifiedSidebar.tsx — edited by this change

    StarredApps now filters items to type !== 'nav' before rendering — the same exclusion already applied to the sidebar Favorites list in UnifiedSidebar.tsx — restoring declared-equals-enforced behaviour and making the missing locale key correct by construction. No locale packs were touched, and the FavoriteItem / RecentItem union types are unchanged.

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

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

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

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

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

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

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

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

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

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

  • names packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts → packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts — edited by this change

    No accept/reject behaviour moves — a memoised getter changes schema identity, not what is declared or admitted. The measurement, the eight ReferenceError messages, the identity matrix and an executable reproduction of both the TDZ mechanism and the once-per-process getter are pinned in packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts.

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

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

  • names zod-lazy-getter-identity-7918.test.ts → packages/types/src/__tests__/zod-lazy-getter-identity-7918.test.ts — edited by this change

    Superseded in this release by objectui#9306: FilterGroupSchema is no longer one of the lazies that keep their spelling. That change made a filter group's conditions flat rows (a nested sub-group is retired and refused by name), so the body stopped naming the const being declared and the memoisation became free; it was taken, and FilterGroupSchema.unwrap() is now reference-stable. The ledger in zod-lazy-getter-identity-7918.test.ts lists it with the memoised schemas. The list of eight above describes the tree this entry was written against.

.changeset/8871-page-node-refuses-breadcrumbs.md

  • names layout/src/NavigationRenderer.tsx → packages/layout/src/NavigationRenderer.tsx — edited by this change

    What was measured, on this branch's base 93127bd6f. Zero readers, with a point-access probe rather than a bare word: on that base \.breadcrumbs scores 0 tree-wide (exit 1) against \.breadcrumb\b's 12 files tree-wide (10 under packages/) as the lit control. At head the same two probes read 16 and 13 and \.breadcrumbs is exit 0 over 4 files — every hit one of this branch's own four files (this changeset, the refusal pin, layout.ts, zod/layout.zod.ts) quoting the probe string, and the pin's own exclusions put head back at exit 1. The base reading is the measurement; the head reading is this branch's echo of it. The bare word would have lied — it also names Sentry's own unrelated concept (app-shell/src/observability/sentry.ts) and appears in two comments listing UI surfaces (core/src/utils/record-title.ts, layout/src/NavigationRenderer.tsx), so a bare probe reports five readers that do not exist.

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

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

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

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 37d166280 (merge-base with origin/main): 24 file(s) changed outside .changeset/, read against 1795 pending declaration(s) that publish a body (2402 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3570.7 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-BaxVo5Cj.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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
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.51KB 11.36KB
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.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
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.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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.21KB 3.89KB
plugin-view (index.js) 90.43KB 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: 444bdb9b50c0087cea56de83ccda62215f6078db
Local-runs: none

Inputs: card #9868 (body and all ten comments — ruling 5731297631, dispatch shape 5734583973, unlock 5904248857, claim 5906153963, dev report 5907305054), PR #11186 (body, the 20-file list, the diff against main), the check-runs on the head read with filter=latest, plus context reads through contents/PATH?ref=SHA at the head and at objectstack 0f6dcac5e9 (the spec 17.5.0 tag commit). Judged against the RULING (letter A), not the card body, whose premise 5730387383 falsified.

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

  1. Validator relaxation against the spec 17.5.0 accept set — right, and the one divergence is stated correctly. Read at objectstack 0f6dcac5e9: BaseNavItemSchema.label is I18nLabelSchema.optional() and is spread into all eight entry branches (object, dashboard, page, url, report, action, component, doc, group); the separator spreads nothing and carries no label. I18nLabelSchema is z.union([z.string(), InlineLocaleMapSchema]) with no minimum length, so the spec base does accept ''. The mirror's superRefine now re-imposes id only and refuses label === '' with the omit-the-key message; the divergence is real, is named in code, changeset and PR body, and sits on the safe side (an empty present label would render empty text and silently opt the entry out of inheritance). The new pin nav-label-optional-9868 measures the spec per objectui entry type before asserting the mirror, and asserts the parsed entry carries no label key — the "parse adds nothing" half the upstream pin required. Pre-existing and unchanged: the mirror admits a plain-string label only (z.string().optional()), narrower than the spec's union; not this card.
  2. NavigationEntryItem.label optional, and the two raw readers the tsc sweep found — right as far as the diff reaches. useNavPins now stores resolveNavItemLabel(item); verified at the head that UnifiedSidebar filters type: 'nav' favorites out of its Favorites list ("rendered in the Pinned section above by NavigationRenderer"), so that stored label is a placeholder. The mobile tab bar in AppSchemaRenderer reads resolveNavItemLabel(item) with no t, identical to the old resolveLabel(item.label) for a present label. But the sweep's instrument sees only reads whose parameter type refuses undefined; two guarded runtime readers in the same package escaped it — findings 1 and 2 under ③.
  3. resolveNavItemLabel order — right. At the head: the separator returns ''; then item.label === undefined returns inheritedNavItemLabel(item, targetLabel); only then resolveLabel(item.label, t), whose body reads label.key for a non-string and would throw on undefined. The ladder: an object entry with a viewName asks the view, then the object, then falls to viewName; an object entry asks the object, then objectName; a dashboard asks the dashboard, then dashboardName; every other type shows its target name (pageName, reportName, url, componentRef, actionDef.actionName); a group or an entry missing its target shows its id. Blank answers count as absent. Keyed on absence alone: the isCustomized convention path never consults targetLabel, so an authored label spelled like the machine name is not swapped (route B stays refused) — pinned with a spy resolver that is never called.
  4. Public surface — right, and public. packages/layout/src/index.ts does export * from './NavigationRenderer', so NavLabelTarget, NavTargetLabelResolver, the optional resolveTargetLabel prop, the sixth parameter of resolveNavItemLabel and the defaulted third parameter of filterNavigationItems all publish; all additive. newPageNavigationItem / newDashboardNavigationItem are module exports NOT re-exported by hooks/index.ts or the package index, so app-shell's public surface does not grow. useNavTargetLabel is internal.
  5. useNavTargetLabel — right; the dev's measurement is confirmed. useObjectLabel at the head: resolve() walks the app namespaces' translation bundle and otherwise returns the fallback it was handed; objectLabel falls back to objectDef.label, viewLabel to its fallback, dashboardLabel to label ?? name. On the nav path that fallback was the entry's own label, so nothing read the target's metadata — the assumption was false, as measured. The hook reads MetadataProvider's getters: objects (with the eagerly loaded view type merged into listViews, keyed object-dot-key; UnifiedSidebar already reads objects for its capability gate) and dashboards (a lazy getter touched only inside the dashboard case — one list for the type, never one per entry). resolveViewId accepts the short and the qualified viewName. When a target has no metadata label the bundle fallback is '', nonEmpty answers undefined, and the renderer walks on, so the ladder ends at the machine name as declared. One resolver on both surfaces: both UnifiedSidebar mounts and NavMenuRenderer pass the same hook's value, and the app-shell render pin asserts both surfaces under one MetadataCtx, rename included.
  6. NavigationSyncEffect — right. It calls syncPageCreated(appName, pageName) / syncDashboardCreated(appName, dashName) with no label; the new builders spread label only when truthy, so an unnamed entry carries no key (never '') while an author label is written verbatim; renameNavigationItems touches only the target name; the toast keeps label || pageName. Pinned on the real writer, with the author-label control.
  7. menuItemToNavigationItem — right. All three arms spread the label only when present; a legacy item without one converts to an entry with no label key that validates; pinned with a verbatim control.
  8. Search filter — right, and the change is stated. filterNavigationItems now matches labelOf(item), which NavigationRenderer binds to its own resolvers; for a present label the answer differs from before only when a host passes both searchQuery and resolvers.
  9. Pins — inverted or re-triggered, none deleted. navigation-spec-parity (inverted, with a spec cross-check), navigation-model (inverted), app-wizard-separator-layout-10867 (the ts-expect-error moved to an id-less entry), zod-lazy-getter-identity-7918 (trigger moved to a missing id, positive control added).
  10. Six ablation legs — reported, not re-run (read-only). Each mutation maps onto a pin in the diff and its red/green split is consistent with the test counts in the pinned files (A3 1 of 2, A4 2 of 4, A5 2 of 6, A6 1 of 2); the refused first A4 attempt is disclosed. Accepted as reported.
  11. Guide content/docs/guide/designing-app-navigation.md — its text matches the diff. id and type mandatory, label optional, the ladder in the spec's order, the machine-name fallback for the other types, a group to its id, a written label verbatim, never ''. The same page says its canonical twin is skills/objectui/guides/app-composition.md ("keep the two in sync"), and that twin now contradicts it — ③.
  12. AppSidebar asymmetry from the first dispatch — moot, verified: packages/app-shell/src/layout/AppSidebar.tsx is absent at the head.

② Semver level

Clause-②: yes (widening)

@object-ui/types: minor — right: NavigationItemSchema accepts a label-less entry it refused, and NavigationEntryItem.label widens to optional. @object-ui/app-shell: patch — acceptable: nothing new is exported. @object-ui/layout: patch — wrong in letter, inert in effect: the diff adds two exported types, an optional prop and two optional parameters (additive public surface, minor by semver), but .changeset/config.json places all 39 packages in one fixed group, so the group publishes at the highest declared level (minor) regardless, and the changeset body names every layout addition. No major — right (AGENTS.md: even a breaking change is minor in this repo). Changeset Declaration, Bump Policy, Fixed Group Check and Claim Re-read are green on the head.

③ Boundary flags

  • Governed skills/objectui/guides/app-composition.md ("both required by NavigationItemSchema") left unedited — right, escalated. Editing skills/** would make this PR a governed one; the stale sentence still yields valid metadata; the published guide already carries the truth. The seat owes the one-line skills-only follow-up the dev named a carrier for; it is not yet filed.
  • Designer surfaces (AppPreview unnamed, AppNavCanvas, studio navSurface fenced by fix(app-shell): the Interfaces pillar's nav rail renders a locale-map label instead of crashing, and resolveSurface resolves it once (objectui#11158) #11179, plugin-designer NavigationDesigner) — right to leave out of this PR; escalated as the class-b seam the dev described, with its producers named. The same sweep, though, missed two RUNTIME surfaces inside the claim's own package — findings 1 and 2.
  • NavigationItemTypeSchema missing 'doc' — measured true at the spec (the doc branch spreads the base at type: z.literal('doc')) and false at the mirror (nine-member enum, no doc); pre-existing, unrelated to label; right to leave; needs its own card.
  • page / report entries not inheriting — right. The ruling and the spec's own describe name view, object and dashboard only; a label-less page renders pageName, the same text the old sync materialised.
  • The i18n machine-name convention for a PRESENT label — not a conflict for this contract; a maintainer question, as the dev says. The isCustomized path offers a label equal to the target name to the translation bundle; it never reads metadata, it is the same bundle lookup the inherited text also passes through inside the hook, and route B was refused for metadata substitution, not for translation. Retiring it would change what apps with sync-materialised labels show, which the ruling keeps ("existing apps with stored labels keep them"). The seat carries the question.
  • Harness attribution — right, no deviation. All five commits end with the model-free Co-authored-by: Claude and Claude-Session: pair, which is what AGENTS.md prescribes ("an agent commit ends with the model-free trailer pair"); no model identifier in the PR title, body, changeset or code.

Findings

  1. ⛔ BLOCKING — the console's command palette renders a label-less entry as EMPTY text, and this PR is the producer that makes that reachable today. packages/app-shell/src/chrome/CommandPalette.tsx (untouched; mounted in console/AppContent.tsx beside NavigationSyncEffect) flattens activeApp.navigation and renders resolveKeyedI18nLabel(item.label, t) for every object / dashboard / page / report entry. That helper's first line at the head returns undefined for an absent label, so the row's text is blank and its cmdk value reads page undefined my_page. Since this PR, NavigationSyncEffect writes every page and dashboard created in the console WITHOUT a label, so each new page shows a blank palette row where it showed its pageName before — a regression, not a pre-existing seam. It is a raw reader inside packages/app-shell (the claim's "whatever resolves an entry's display label"), the class the dev fixed in useNavPins and the mobile tab bar; the tsc sweep could not see it because the helper's parameter already admits undefined, and the report does not name that blind spot. Route it through resolveNavItemLabel with the useNavTargetLabel resolver (the palette has t and sits under the metadata provider) and pin a label-less page in the palette.
  2. Same class, non-blocking on its own: packages/app-shell/src/views/SearchResultsPage.tsx builds label: item.label || item.objectName || item.dashboardName || item.pageName || item.reportName || '', so a label-less object / view / dashboard entry is listed and matched by its machine name, never by the inherited label. Fix it with finding 1 or disclose it in the seam list.
  3. Non-blocking: @object-ui/layout declared patch while the diff adds exported types and an optional prop (②) — inert under the fixed group; correct it on the next touch of the changeset.

Implemented-by: claude/issue-9868-nav-label-inherits-target
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3570.9 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-CpNmM-Uu.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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
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.51KB 11.36KB
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.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
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.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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.21KB 3.89KB
plugin-view (index.js) 90.43KB 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

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3570.9 KB 3607.4 KB
Main entry chunk (gzip) 149.6 KB 350 KB
Entry file index-CI6fSTY5.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) 559.46KB 134.18KB
core (index.js) 9.94KB 3.94KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 227.28KB 63.04KB
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.51KB 11.36KB
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.58KB 4.90KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.25KB 2.17KB
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) 215.78KB 44.42KB
plugin-detail (index.js) 234.78KB 62.36KB
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.25KB 63.22KB
plugin-kanban (index.js) 48.43KB 15.11KB
plugin-list (index.js) 115.82KB 28.67KB
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.21KB 3.89KB
plugin-view (index.js) 90.43KB 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: 26d6862a3e81e24e32e11e4164a340a44c967250
Local-runs: none

Round 2 of record 5907554475 (rendered at 444bdb9b5, verdict FAIL). Inputs: card #9868 (body and all eleven comments — ruling 5731297631, dispatch shape 5734583973, unlock 5904248857, claim 5906153963, dev report 5907305054, patch report 5908597262), PR #11186 (body, the 25-file list, the net diff against main), the check-runs on the head read with filter=latest, plus context reads through contents/PATH?ref=SHA at the head. Judged against the RULING (letter A), not the card body. The whole PR is judged at the new head; round-1 judgments are re-cited by number where the diff is unchanged.

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 judgments

The three round-1 findings — each closed at the head.

  1. Finding 1 (BLOCKING, the ⌘K palette) — closed. packages/app-shell/src/chrome/CommandPalette.tsx now binds navLabel(item) = resolveNavItemLabel(item, undefined, t, undefined, undefined, useNavTargetLabel()) and uses it for all eight reads (the value and the visible text of the object, dashboard, page and report rows). For a PRESENT label the answer is identical to the old resolveKeyedI18nLabel(item.label, t): a string returns verbatim (no convention resolver is passed, so the isCustomized branch returns base), and a keyed object goes through the same t(key, { defaultValue, ...params }) / result !== key / defaultValue || key steps in resolveLabel. For an ABSENT label the row shows the inherited label or the machine name, and the cmdk data-value carries that text instead of undefined. The remaining .label reads in the file are recent items and app labels, not nav entries. Pinned through the real CommandPaletteProvider (?palette=1) under a MetadataCtx: label-less page → my_page with value page my_page my_page; label-less dashboard and view → their metadata labels, then the renamed ones on re-render; control: an authored label verbatim. The dev reproduced the blank row on 444bdb9b5 with this pin first (2 failed / 1 passed), which is the right order. useMetadata() (via useNavTargetLabel) falls back to a no-op value outside a provider (AppShellContext.tsx), so the publicly exported CommandPalette gains no new crash path; the console mounts it in console/AppContent.tsx under MetadataProvider.

  2. Finding 2 (full-page search) — closed. SearchResultsPage.tsx builds each nav result's label through the same call with the same hook; t is already in scope above the memo, and the memo's deps now carry t and targetLabel. The second flattenNavigation in the file reads objectName only. Pinned: a label-less view / dashboard entry is listed by its target's current label and found by a query for it (仪表盘, overview), a label-less page by its pageName, the authored label verbatim, and the machine names customer / sales_overview are asserted absent from the titles. The pre-existing exhaustive-deps warning on the edited deps line is disclosed.

  3. Finding 3 (semver) — closed. .changeset/9868-nav-label-inherits-target.md now declares @object-ui/layout: minor; see ②.

The raw-reader sweep — its instrument, control and classification hold; no runtime reader is missed.

  1. The instrument: non-test files under packages/*/src and apps/*/src admitted by three name patterns (the nav types; the app.navigation / areas reads plus flattenNavigation / collectLeaves; the raw-nav aliases), then every .label read in them, plus a second probe for bracket and destructuring reads. 214 hits at the head, classified into 84 EXCLUDED (the six view plugins' schema.navigation, which is the record-open ViewNavigationConfig, not app navigation), 96 OTHER, 34 nav-item reads (3 RESOLVER, 1 VALIDATOR, 5 FIXED, 1 SAFE, 24 SEAM), 0 unfixed runtime readers. The control is lit in the way the round-1 finding asked for: the same instrument at 444bdb9b5 returns the eight palette reads and search line 99, and at 0ffc423b1 the five first-round readers — so the zero at the head is a reading. Its stated blind spot (an untyped prop under none of the three names) is the right one to state. I read the runtime surfaces that could sit in that blind spot at the head: console/AppContent.tsx (findFirstRoute over activeApp.navigation reads no label), ConsoleLayout / AppSwitcher / console/home/* (app labels), AppHeader breadcrumbs (collectNavObjects reads objectName only; segment labels come from the dashboard / page / report / view definitions), HostNavigationBridge, FavoritesProvider (refreshLabel on favorite records), RecentItemsProvider / useTrackRouteAsRecent (object definitions), useHomePath / homePath, PageView / DashboardView / ReportView / ObjectView (their own definitions), the runner's LayoutRenderer (the legacy path / children menu item, as the dev classified it), @object-ui/react's HostNavigationContext, @object-ui/components' sidebar / header-bar / navigation-menu renderers (their own schemas) and apps/console/src. None reads a nav entry's label. The one SAFE predicate, UnifiedSidebar.isOverviewGroup, is typeof-guarded and matches an unlabelled group by its id; the sidebar's remaining item.label reads are Favorite records, whose nav pins now store resolveNavItemLabel(item) text (and nav-type favorites are filtered out of that list — round-1 judgment 2).

  2. The two first-round FIXED readers now carry pins. useNavPins.labelInherits-9868 asserts a label-less page pins as label: 'my_page' (a string, never undefined) with the authored-label control; the layout pin's new last block renders AppSchemaRenderer with mobileNavMode="bottom_nav" and asserts the tab texts ['my_page', 'Accounts'].

  3. Ablation legs A7–A10 — reported, not re-run (read-only); consistent with the pins. A7 (palette back to the keyed read) 2 failed / 1 passed matches the 3-test pin with one control; A8 (search back to the || chain) 3 failed / 0 passed matches its 3 tests, each of which fails on the machine-name text; A9 (useNavPins stores item.label) 1 / 1 matches its 2 tests; A10 (tab bar reads item.label raw) 1 failed / 20 passed matches the 21 tests now in NavigationRenderer.labelInheritsTarget-9868, and A2's split moving from 8 / 16 to 8 / 17 is that same added test, which does not depend on targetLabel. The A1, A3–A6 splits are unchanged from round 1 (judgment 10 there).

Unchanged since round 1 — re-cited from 5907554475: the validator relaxation per entry type against spec 17.5.0 and the stated '' divergence (judgment 1: the round-2 diff touches no packages/types file); NavigationEntryItem.label optional and the tsc sweep's two readers (2); resolveNavItemLabel's absence arm before resolveLabel and the ladder view → object / dashboard → machine name, keyed on absence alone (3 — re-read at the head: the separator returns '', then item.label === undefined returns inheritedNavItemLabel, only then resolveLabel); the public surface NavLabelTarget / NavTargetLabelResolver / resolveTargetLabel / the sixth parameter / the defaulted third parameter of filterNavigationItems, all additive through export * from './NavigationRenderer', while useNavTargetLabel, newPageNavigationItem and newDashboardNavigationItem are not re-exported by hooks/index.ts or the package index (4 — confirmed at the head); useNavTargetLabel reading the MetadataProvider cache with no fetch per entry, the FALSE measurement of useObjectLabel's resolvers, and one resolver on both surfaces (5) — now on four surfaces: the sidebar's two mounts, nav:menu, the palette and the search page all pass the same hook's value; NavigationSyncEffect writing no label key while an author label is still written (6); menuItemToNavigationItem (7); the search filter matching the displayed label (8); the three pins inverted / re-triggered, none deleted (9); the guide content/docs/guide/designing-app-navigation.md matching the diff (11 — the file is unchanged this round); AppSidebar absent (12).

Observation, not a finding: the PR body was not updated for the patch round — its file count, ablation table (A1–A6 at 444bdb9b5) and verification section describe the round-1 head. The published text (the changeset) does name the palette and the search page, and the patch report 5908597262 carries the delta.

② Semver level

Clause-②: yes (widening)

@object-ui/types: minor — right: NavigationItemSchema accepts a label-less entry it refused, and NavigationEntryItem.label widens to optional. @object-ui/layout: minor — now right: two exported types, an optional prop and two optional parameters are additive public surface. @object-ui/app-shell: patch — acceptable: nothing new is exported, and the changeset body names the palette and search behaviour it changes. No major — right. .changeset/config.json at the head keeps all packages in one fixed group, so the group publishes at minor. Changeset Declaration, Bump Policy, Fixed Group Check, Claim Re-read and Overwrite Report are green on the head.

③ Boundary flags

  • Governed skills/objectui/guides/app-composition.md ("both required by NavigationItemSchema") — stays out of this PR by the seat's direction; the seat carries the one-line skills-only follow-up at the ACCEPT. Not a finding here.
  • Designer surfaces (AppPreview unnamed, AppNavCanvas, nav-selection / AppNavInspector, studio navSurface and StudioDesignSurface fenced by fix(app-shell): the Interfaces pillar's nav rail renders a locale-map label instead of crashing, and resolveSurface resolves it once (objectui#11158) #11179, plugin-designer NavigationDesigner and AppCreationWizard) — right to leave out; escalated as the class-b seam with its 24 reads enumerated by line and its producers named (NavigationSyncEffect now, cloud#2021 next). Round 1's objection — that the same sweep missed runtime surfaces — is answered by judgment 4.
  • NavigationItemTypeSchema missing 'doc' — pre-existing, unrelated to label, needs its own card (round-1 ③, unchanged).
  • page / report entries not inheriting — right (round-1 ③); now measured on every runtime surface: a label-less page renders pageName in the sidebar, nav:menu, the palette and the search page.
  • The i18n machine-name convention for a PRESENT label — not a conflict for this contract; a maintainer question the seat carries (round-1 ③, unchanged).
  • Harness attribution — right (round-1 ③): all seven commits, the two patch-round ones included, end with the model-free Co-authored-by: Claude and Claude-Session: pair, and no model identifier appears in the commit messages, the changeset, the PR body or the diff.
  • Patch-round deviations: the surface additions (CommandPalette.tsx, SearchResultsPage.tsx, two pins) were ordered by the seat; the extra sweep buckets (RESOLVER / VALIDATOR / SAFE) are named rather than folded in — right; the round-0 report's silence on the tsc blind spot is acknowledged and answered by the text sweep.
  • open_questions: [] on both reports — nothing to escalate.

Implemented-by: claude/issue-9868-nav-label-inherits-target
Reviewed-by: session_011p7ikEivgXefNDaE5S5Uec

VERDICT: PASS


Generated by Claude Code

akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…-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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment