Skip to content

fix(console): an app that serves the caller no navigation opens on an app-level empty state, not the Studio home (objectui#12079) - #12091

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-12079-empty-nav-landing
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-12079-empty-nav-landing

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12079

Clause-②: yes

What changes for the user

An app that serves the signed-in user no navigation now opens on an app-level empty state instead of the Studio home.

  • The title names the app: "Nothing in HotCLM is available to you yet". The label is the one the header's app switcher shows.
  • A caller the console already treats as a workspace administrator reads "This app has no pages your account can open. You can grant access in Setup." and gets an Open Setup link.
  • Everyone else reads "... Ask an administrator to grant you access." and gets no link.
  • An app with a landing page still opens on it. The Studio app still opens on its own home (see "Studio is untouched").

Measured case: objectstack-ai/hotclm#87. For the dev admin, GET /api/v1/meta/app/clm answered navigation: [], because every HotCLM group is gated on a clm_*.access capability. That account holding no clm_* set is hotclm's open question (objectstack-ai/hotclm#28, objectstack-ai/hotclm#11). This PR does not take a position on it.

Mechanism, measured on origin/main 023f00d4

The PM's four assumptions, each measured:

  1. One fallback site: confirmed. StudioHomePage is rendered in one place, the app index route of AppContent in packages/app-shell/src/console/AppContent.tsx, when resolveLandingRoute returns nothing. Its other references are the lazy import beside it, the metadata-admin barrel export, two of its own tests, and five test mocks.

    • The reference console's apps/console/src/AppContent.tsx wraps DefaultAppContent and adds no landing logic.
    • @objectstack/console is built from this same source.
    • The fallback had a second, live consumer: the Studio app. Its served navigation is all component items. findFirstRoute walks only object / page / dashboard / report items (and groups), so /apps/studio has no landing and reaches the same branch with a full navigation. UnifiedSidebar also gives the Studio app a dedicated "Studio home" entry that points at that root.
    • So the fallback was not split on "no landing". It was split on "the app serves no navigation" (next section).
  2. The grant decision is reused, not invented. The Setup link is gated on useWorkspaceAdminStatus().isAdmin. Its legs:

    • the active membership role;
    • the user.role scalar;
    • user.isPlatformAdmin;
    • org_owner / org_admin in user.positions.

    AppContent already reads this verdict (isWorkspaceAdmin) to decide who stays on the no-app empty state with its "System Settings" button (everyone else is bounced home). The home sidebar reads the same verdict to show its administration links into Setup. Its readiness half (isResolved) already gates AppContent's first render, so the link cannot flicker in.

    The URL is resolveSetupAppPath(apps), the policy behind the /setup deep link: the Setup app's root by package id, then by name.

  3. Eager closure. The before and after readings are in the Gates table below.

  4. The empty-state primitive exists and is reused. Empty, EmptyTitle and EmptyDescription from @object-ui/components are already in the eager closure, through RouteNotFound and the app-verdict screens in the same file. The new screen composes those three plus Button, and the Lock icon the header already loads.

    • A first cut used EmptyHeader / EmptyMedia / EmptyContent. Those have no caller in the console build, and the cut added 46 bytes gzipped to ui-components, which has about 3 KB of headroom. The second commit drops them.

What changed

  • packages/app-shell/src/console/appServesNoNavigation.ts (new): appServesNoNavigation(app) is true when the top-level navigation holds no item and no areas[].navigation does either.
    • It reuses the sidebar's own hasVisibleNavigationItems with no guards and with hasActionHandler: true, because the console sidebar wires an action dispatcher.
    • So a separator or a childless group counts as nothing, exactly as the sidebar draws it.
  • packages/app-shell/src/console/AppNoAccessEmptyState.tsx (new): the screen described above.
  • packages/app-shell/src/console/AppContent.tsx: the index route now runs these steps in order:
    1. If a landing resolves, navigate to it.
    2. Otherwise, if appServesNoNavigation(activeApp), render AppNoAccessEmptyState with setupPath = isWorkspaceAdmin ? resolveSetupAppPath(apps) : null.
    3. Otherwise, render StudioHomePage as before.
  • packages/i18n/src/locales/*.ts: four new keys in all ten packs, empty.appNothingAvailable (holds {{app}}), empty.appNothingAvailableDescription, empty.appNothingAvailableGrantDescription and empty.openSetup.
  • .changeset/12079-empty-nav-landing.md: patch for @object-ui/app-shell and @object-ui/i18n.

Clause-② is yes as the claim declared: the four keys widen the published @object-ui/i18n packs. No package export was added. The two new app-shell modules are imported only by AppContent, and src/index.ts is unchanged.

Studio is untouched

Triage says "/studio itself is untouched". The /studio pillar builder is not edited. The Studio app root /apps/studio still renders StudioHomePage, pinned by a control test below.

The card quotes the maintainer calling that page "Studio's old home, retired a long time ago". On this tree it is still the Studio app's live root: the sidebar's "Studio home" entry links to it. Retiring it would be a separate decision, and this PR leaves it alone.

Tests

New file packages/app-shell/src/console/__tests__/AppContent.emptyNavigationLanding-12079.test.tsx, 8 tests, in the harness of AppContent.inaccessibleAppStrand.test.tsx: real AppContent and router, with the shell and the lazy pages stubbed.

  • an app with navigation: [] renders the empty state, not StudioHomePage, names the app, and stays on /apps/clm;
  • a caller who is not an admin gets the "ask an administrator" copy and no Setup link;
  • an admin gets the Setup link at /apps/com.objectstack.setup, the resolveSetupAppPath URL;
  • a member whose app list carries Setup still gets no link: the decision reads the admin verdict, not the list;
  • control: an app whose first group holds an object item still navigates to /apps/clm/contract;
  • control: a Studio-shaped app (navigation of component items only) still renders StudioHomePage;
  • appServesNoNavigation: true for no navigation, [], only childless groups and separators, or empty areas; false for a url item, or for an item only inside an area.

Reverse verification. The fix is committed first. scripts/ablation-replace.mjs from objectstack then mutated the anchor if (appServesNoNavigation(activeApp)) { in AppContent.tsx to if (false && appServesNoNavigation(activeApp)) {, with the anchor count going 1 to 0 and the blob changing on disk.

  • Ablated: 4 failed, 4 passed. The four empty-state tests fail, and the rendered DOM shows data-testid="studio-home-page", which is the defect. The two controls and the two predicate tests stay green.
  • Restored: the blob equals HEAD, git diff HEAD is empty, and the re-run is 8 passed.

Gates

Run at HEAD 02569f3. objectui has no dispatch-gates.mjs, so this list is derived by hand from the root package.json and .github/workflows/.

Gate Verdict
pnpm turbo run type-check --filter=@object-ui/app-shell --filter=@object-ui/i18n --concurrency=2 (builds the upstream closure first; app-shell's tsconfig.test.json covers the new test file) exit 0, 31 of 31 tasks
pnpm exec vitest run --maxWorkers=2: the 14 AppContent.* suites in app-shell, plus the 3 console suites that mock StudioHomePage and defaults-maps-mirror-en-pack 18 files, 185 passed
pnpm exec vitest run --maxWorkers=2 packages/i18n/ (includes all-locales-key-parity) 82 files, 1316 passed, 13 skipped (skips predate this change)
pnpm check:eager-closure (console vite build before and after) see the size line below
pnpm check:eager-locale-catalogues exit 0; en resident 44.2 KB to 44.3 KB
check:i18n-keys, check:i18n-drift, check:i18n-dead-keys exit 0 each; drift: 0 en values changed, 4 keys added
check:new-line-citations, check:control-bytes 0 new citations; OK
check:phantom-deps, check:self-import, check:unreferenced-sources, check:esm-specifiers exit 0 each
check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:test-path-roots exit 0 each
check-changeset-presence.mjs, check-changeset-no-major.mjs, check:changeset-claims, check:pending-changeset-literals exit 0 each
check-governed-queue-guard.mjs --test over the 15 changed paths NOT GOVERNED
eslint on the 14 touched source files 0 errors

Eager closure size, before (023f00d4) and after (02569f3):

  • Total: 3168.6 KB to 3168.9 KB gzipped, which is +311 bytes gzipped and +1554 bytes raw. Headroom goes from 35.9 KB to 35.6 KB, and the closure stays at 290 of 2474 chunks.
  • index: +283 bytes gzipped, +1232 raw. This is the new component, the predicate and the route branch.
  • i18n-locale-en: +68 bytes gzipped, +322 raw. These are the four en strings.
  • ui-components: +8 bytes gzipped, 0 raw.
  • All other chunks: -48 bytes gzipped in total, 0 raw. These are hash-renaming effects only.

Not run locally, because CI owns them: the repo-wide pnpm lint, the full pnpm test, Bundle Analysis and E2E.

ESLint was run on the 14 touched source files only. CI's pnpm lint runs eslint . in each package.

  • Population: the root eslint.config.js. It sets no parserOptions.project or projectService, so linting is not type-aware. No rule under eslint-rules/ reads another file from disk.
  • Count: 14 files in the --format json output.
  • Result: 0 errors and 33 warnings, all 33 in AppContent.tsx. The base AppContent.tsx measured through --stdin also has 33, so this diff adds none.
  • Why the narrowing is sound: each file's verdict is independent of every other file, so this diff cannot move the verdict on an untouched file.

Acceptance notes

  • Residual, not filed: a non-Studio app whose served navigation is non-empty but holds no item the landing resolver walks still renders StudioHomePage. That covers navigation of only component, url or action items, or items that live only under areas.
    • The spec says areas take precedence over the top-level navigation, while resolveLandingRoute reads only the top level.
    • The only producer measured in this shape is the Studio app itself, where the page is intended. account, setup and the objectstack examples all resolve a landing, and none declares areas.
    • Carrier: none.
  • The copy says "no pages your account can open". That is also what an app that declares no navigation at all would show.
  • Live browser pass: NOT MEASURED. It needs a backend serving an app whose navigation is empty for the signed-in user, which is the hotclm#87 setup. The routing is pinned in RTL, against the real AppContent and router.

Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z


Generated by Claude Code

… app-level empty state, not the Studio home (objectui#12079)

The app index route fell back to StudioHomePage whenever the navigation
resolved no landing. An app whose every group is capability-gated arrives
as navigation: [] for a caller without those capabilities, so that caller
landed on Studio's metadata overview inside an unrelated app.

The fallback now splits on navigation emptiness: no navigation item at the
top level or in any area renders AppNoAccessEmptyState, which names the app
and, for a caller useWorkspaceAdminStatus confirms as an admin, links to
Setup at resolveSetupAppPath. A navigation with no landing (the Studio app,
built from component items) still renders StudioHomePage.

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…he empty-state file exports only a component (objectui#12079)

Claude-Session: https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z
Co-authored-by: Claude <noreply@anthropic.com>
…primitives its sibling screens already use (objectui#12079)

EmptyHeader, EmptyMedia and EmptyContent had no caller in the console build,
so using them grew the eager ui-components chunk; Empty, EmptyTitle and
EmptyDescription are already eager through RouteNotFound and the app-verdict
screens.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3168.9 KB 3204.6 KB
Main entry chunk (gzip) 74.0 KB 350 KB
Entry file index-C6LTUJVW.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) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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) 41.19KB 11.12KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: 02569f377fdd381dc7aea28b4f0918d4608ef8eb
Local-runs: none

① Derived judgments

  • Locale keys in the published @object-ui/i18n packs: additive — RIGHT.
    • The net diff adds exactly four empty. keys to all ten packs and removes or edits no existing value.
    • The en values are byte-identical to the four call-site defaultValue strings in AppNoAccessEmptyState.tsx.
    • The {{app}} hole is present in appNothingAvailable in every pack.
    • zh keeps "Setup" untranslated, which matches that pack's existing setup_access spelling.
  • @object-ui/app-shell public surface: unchanged — RIGHT.
    • src/index.ts is identical between origin/main and the head.
    • The exports map publishes only . and ./styles.css.
    • The two new modules are imported only by console/AppContent.tsx.
    • So Clause-②: yes is carried by the i18n keys alone, as the claim and the PR body say.
  • Behaviour against triage 6093511804 — RIGHT. Landing → Navigate; else appServesNoNavigation(activeApp) → AppNoAccessEmptyState; else StudioHomePage.
    • /studio is not touched.
    • A non-Studio app with empty navigation can no longer reach StudioHomePage.
    • The predicate reuses the sidebar's own hasVisibleNavigationItems, so the two cannot disagree about what is "nothing".
  • The Studio app's root is as it was — RIGHT. Its all-component navigation resolves no landing but counts as navigation, so it still renders StudioHomePage, and the second control test pins that.
  • A residual the card does not cover — named, not a failure. A non-Studio app whose navigation is non-empty but holds no walkable item (only component, url, action, separator or doc items, or items only under areas) still lands on StudioHomePage. That path predates this PR and is outside triage's pin; see ③.
  • The Setup-link gate is an existing read — RIGHT.
    • useWorkspaceAdminStatus().isAdmin was already destructured in AppContent on origin/main, and its isResolved half already gates the first render.
    • No permission check and no capability read is added.
  • The Setup URL — RIGHT. resolveSetupAppPath(apps) is the existing /setup deep-link policy, and no import cycle is introduced.
  • Composition — RIGHT. Only existing primitives and dependencies are used, and the label expression is the one AppSwitcher uses.
  • Tests — RIGHT.
    • One new RTL file, eight tests: both triage pins, the Studio control, the admin and non-admin split, and two predicate units.
    • The dev's ablation turned the four empty-state tests red and left the controls green.
  • Check-runs on the head: 43 runs, 40 success, 3 skipped (dependabot, Test (coverage), and the unexpanded coverage matrix placeholder), none failed, none pending.

② Semver level

  • @object-ui/app-shell: patch and @object-ui/i18n: patch, with no major (Changeset Bump Policy green).
  • patch for app-shell is right: no export, type or exports-map change.
  • patch for four additive i18n keys matches how this repository has declared key additions. Several pending changesets on origin/main declare i18n patch for added keys, as does the 17.7.0 CHANGELOG's Patch Changes section; minor also appears for key additions, and no objectui rule picks between them. The fixed group's next release is already minor from pending declarations.
  • The Clause-②: yes line stands at the start of its own line and matches the claim.

③ Boundary flags

Dev report 6096347163: open_questions is empty. Six deviations and two out-of-scope findings:

  1. The second new module, appServesNoNavigation.ts: inside the claimed surface and unexported. Accepted.
  2. The repo-root vitest invocation: procedural; the head's Test shards are the verdicts. Accepted.
  3. Type-check through turbo with the ^build closure: procedural; Type Check is green. Accepted.
  4. Model-free commit trailers per objectui AGENTS.md. Accepted; no model identifier is in the diff or the PR body.
  5. Three lock queue-timeouts re-queued: nothing was measured in those slots. Accepted.
  6. The live browser pass is NOT MEASURED: both pins are held in RTL against the real AppContent and router. Accepted. Recommended, not a gate: re-run the Full browser test on 17.7.0 main: drive the whole contract lifecycle as every audience, in en and zh-CN, and report what a real user hits hotclm#87 first-screen check once the console pin carries this head.
  7. The residual (non-empty navigation with no walkable landing): escalated to the seat. The seat measured objectstack origin/main 86da1949: no app or example declares areas (zero hits for areas: [ over *.app.ts and examples/**; control: navigation: [ hits crm.app.ts, todo.app.ts and account.app.ts). With no real producer it goes to Acceptance notes; carrier none.
  8. StudioHomePage is "retired" in the card body's account of the maintainer, yet it is the live root of /apps/studio. The seat will put it to the maintainer; it is not a condition of this card, which follows triage's ruling.

Implemented-by: claude/issue-12079-empty-nav-landing
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 12:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 12:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 10, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Dequeued on Spec Main Shape Gate, which is not this PR's failure · domain:ui seat 3, session_01B1gHb9baeX7oioD5sHVm7z, 2026-10-10T13:03Z

  • The failing check: Spec Main Shape Gate in merge group 4bf3d7b597 (check run 114216775771). It compiles objectui's tests against objectstack main's spec (243dd3c6).
  • Why it is not this PR's:
    • All nine failures are in test files this PR does not touch (two in packages/components, three in packages/types).
    • Each one writes a flat element binding key or object-grid.defaultFilters, which objectstack PR #22421 (317cddd40d) retired after this PR's own green run.
    • The same gate passed in PR objectui#12090's merge group at 10:08Z, before that retirement landed.
    • It now fails for any merge group.
  • The fix: anchored on objectui#12093 and folded into the in-flight dispatch of objectui#12085 (branch claude/issue-12085-element-binding-inputs), whose own PR meets the same gate. No fix is on main yet.
  • This PR: the head is unchanged, ACCEPT 6097652323 and contract review PASS 6097645373 stand, and auto-merge is now off (the dequeue cleared it). When objectui#12093's fix is on main, this PR merges main, the gate is read green on the new head, and the seat re-arms auto-merge. No re-run is spent before then: the gate would fail the same way.

Generated by Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3169.7 KB 3204.6 KB
Main entry chunk (gzip) 74.0 KB 350 KB
Entry file index-CWS4nKBf.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) 19.75KB 7.29KB
app-shell (runtime-config.js) 22.59KB 7.89KB
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.15KB 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) 41.19KB 11.12KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 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) 586.66KB 141.23KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
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.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.26KB 11.71KB
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) 14.32KB 5.17KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
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.04KB 3.92KB
plugin-calendar (index.js) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.97KB 23.24KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
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) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
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) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
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) 1.13KB 0.65KB
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) 5.78KB 2.70KB
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 (node-slots.js) 7.18KB 2.34KB
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) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
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: b6fc38bb06ece0bc1928d03c4e274a27abc52390
Local-runs: none

① Derived judgments

  • The hop from the previously reviewed head 02569f377f — verified, carries every judgment forward. b6fc38bb is one merge commit (parents 02569f377f and main ce991bd7); origin/main...b6fc38bb has ce991bd7 as its merge base. All 15 files the PR changes are blob-identical between 02569f377f and b6fc38bb (per-file blob ids equal; git diff over the list is 0 bytes), and the net diff on the landing head hashes identically to the one reviewed on 02569f377f (5c9871acd122406f, 33897 bytes, 15 files, +473/-6). What main brought in (objectui#12088, fix(app-shell): the chatter reads and writes reactions as the member's own sys_comment_reaction records (objectui#12078) #12090, fix(components): element:repeater and element:number publish the dataSource binding as their only query input, and objectui compiles against spec main again (objectui#12085, objectui#12093) #12100: ActionParamDialog, FlowRunner, RecordDetailView, metadata-admin/i18n.ts, data-list/elements renderers, ObjectView, tests, docs, and four changesets) touches none of the PR's files and none of the helpers the PR reuses: AppContent, ConsoleShell, app-shell index.ts and package.json, useWorkspaceAdminStatus, NavigationRenderer and the layout barrel, UnifiedSidebar/AppSwitcher/AppHeader, the types' app schema, the ten locale packs, and .changeset/config.json are all identical between origin/main and the landing head. No judgment below moves.
  • Locale keys (published @object-ui/i18n packs) — additive, right. The net diff adds exactly four keys under empty. to all ten packs (ar, de, en, es, fr, ja, ko, pt, ru, zh) and removes or edits no existing value. The en values are byte-identical to the four call-site defaultValue strings in AppNoAccessEmptyState.tsx (Nothing in {{app}} is available to you yet, "This app has no pages your account can open. Ask an administrator to grant you access.", "... You can grant access in Setup.", "Open Setup"). The {{app}} interpolation hole is present in appNothingAvailable in every one of the ten packs. The zh pack keeps "Setup" untranslated, matching that pack's existing setup_access: '访问 Setup' spelling.
  • @object-ui/app-shell public surface — unchanged, right. packages/app-shell/src/index.ts and package.json are identical to origin/main at the landing head; the exports map publishes only . (dist/index) and ./styles.css. The two new modules (console/AppNoAccessEmptyState.tsx, console/appServesNoNavigation.ts) are imported only by console/AppContent.tsx; no barrel names them (0 references in index.ts). They are shipped bytes, not a published accept-set, so the Clause-②: yes declaration is carried by the i18n keys alone, as the claim and the PR body both say.
  • Behaviour against triage 6093511804 — right. The app index route runs: landing resolves → Navigate; else appServesNoNavigation(activeApp) → AppNoAccessEmptyState; else StudioHomePage. /studio (the pillar builder) is not touched. A non-Studio app with empty navigation can no longer reach StudioHomePage: the predicate is true for no navigation, [], childless groups, separators and empty areas, and it is consulted before the fallback. It reuses the sidebar's own hasVisibleNavigationItems from @object-ui/layout (exported through export * from './NavigationRenderer') with no guards and hasActionHandler: true, so it is structural in the same way findFirstRoute is structural; the two cannot disagree about what is "nothing".
  • The Studio app's root is as it was — right. resolveLandingRoute reads only the top-level navigation, and findFirstRoute walks only object/page/dashboard/report items and group children, so Studio's all-component navigation (the sidebar identifies Studio by activeApp?.name === 'studio') resolves no landing but counts as navigation to the sidebar predicate; the branch still renders StudioHomePage, and the new test's second control pins it. Splitting on "serves no navigation" rather than "no landing" is the correct split for the card's scope.
  • Residual the card does not cover — named, not a failure. A non-Studio app whose served navigation is non-empty but holds no walkable item (only component, url, action, separator or doc items — doc is a NavigationItem type the dev's list omits — or items only under areas, which the resolver never reads) still lands on StudioHomePage. That path predates this PR, is outside triage's pin (empty navigation), and no producer outside the Studio app is measured; escalated in ③.
  • Setup-link gate — an existing read, right. useWorkspaceAdminStatus() is a pre-existing hook in @object-ui/auth whose isAdmin is the OR of the active membership role, user.role, user.isPlatformAdmin === true, and org_owner/org_admin in user.positions. AppContent already destructured it as isWorkspaceAdmin on origin/main (it decides who stays on the no-app empty state) and already gates its first render on its isResolved half, so the link cannot flicker in. The diff adds no permission check and no capability read; it hands the existing verdict to the new component as setupPath. For the measured case (the seeded platform admin on hotclm#87) the isPlatformAdmin leg lights, which is the card's "send a platform admin to Setup". Precision: this is the console's workspace-administrator verdict, not a setup.access capability check — the notion the claim's assumption 2 directed, and the one the no-app screen and the sidebar's sys-administration group already read.
  • Setup URL — right. resolveSetupAppPath(apps) is the pre-existing policy behind the /setup deep link (SetupRedirect calls it), resolving the Setup app's root by package id then by name, with the canonical package-id URL as fallback. The import AppContent → ConsoleShell.js is new; ConsoleShell.tsx imports nothing from AppContent (it names it only in comments), so no import cycle is introduced, and resolveSetupAppPath is a hoisted function declaration in any case. The new test pins /apps/com.objectstack.setup.
  • Empty-state composition — right. Empty, EmptyTitle, EmptyDescription, Button from @object-ui/components, Lock from lucide-react, Link from react-router-dom; every one is an existing dependency of app-shell. The label is built as appLabel({ name, label: resolveKeyedI18nLabel(app.label, t) }), the same expression AppSwitcher uses, so the title names the app as the header does.
  • Tests — right. One new RTL file, eight tests, in the harness of AppContent.inaccessibleAppStrand.test.tsx: both triage pins (empty navigation renders the empty state and not StudioHomePage; an app with a landing still navigates), the Studio control, the admin/non-admin split, the "reads the verdict, not the app list" case, and two predicate units. The dev's ablation (anchor if (appServesNoNavigation(activeApp)) { negated) turned the four empty-state tests red and left the controls green, the right direction.
  • Check-runs on the landing head, read last at render time: 43 runs, all completed, 40 success, 3 skipped (dependabot, Test (coverage), Test (coverage shard ...)), none failed, none pending; last completion 19:44:08Z. Green includes Spec Main Shape Gate, Type Check, Lint, Test and all eight shards, Test (dist pins), Build and E2E, Bundle Analysis, Changeset Declaration, Changeset Bump Policy, Changeset Fixed Group Check, Changeset Claim Re-read, Governed Surface Queue Guard, Line Citation Gate, Control Byte Scan, Inert vi.mock Specifier Check. These are the gate verdicts.

② Semver level

  • Changeset .changeset/12079-empty-nav-landing.md (blob-identical across the hop) declares @object-ui/app-shell: patch and @object-ui/i18n: patch; no major, so objectui's version-alignment rule holds (Changeset Bump Policy green on the landing head).
  • Every sentence of the changeset re-read against the code at b6fc38bb: the server-filtered navigation premise, the "Nothing in APP is available to you yet" title with the app's own label, the useWorkspaceAdminStatus reuse ("the same check the console's no-app screen and Home's administration links use" — AppContent's no-app branch and UnifiedSidebar's sys-administration group both read it at the landing head), the "Open Setup" link at the /setup deep-link URL (SetupRedirect → resolveSetupAppPath), the two "Unchanged" clauses (landing still honoured; Studio's component-only navigation still reaches StudioHomePage), and the four named keys in all ten packs — each holds. The four changesets main brought in (12078-reaction-records: app-shell patch; 12080-screen-dialog-reach: app-shell patch; 12085-element-binding-inputs: components minor, narrowing of element inputs; 12093-spec-main-compile: no release) name no locale key, no AppContent behaviour and nothing this changeset claims, so there is no interaction to reconcile.
  • app-shell at patch is right: no export, type or exports-map change; a bug fix in a released package.
  • i18n at patch for four additive keys is consistent with this repository's own declarations. Pending on origin/main, 11863-studio-search.md declares @object-ui/i18n: patch while its commit adds packages/flows keys to en.ts; 11717-capability-view-all-audit-log.md, 11726-marketplace-hint-same-origin.md, 11880-element-datasource-only.md and 11902-record-read-failure.md likewise declare i18n patch. The 17.7.0 packages/i18n/CHANGELOG.md "Patch Changes" section carries 328abeb ("All ten locale packs carry the nine new keys; no existing en value changes") and 24d3e65 (cloudPlanStatus.current added to all ten packs). The repository also declares minor for key additions at least as often (12035-studio-access-notice.md, 12072-decision-panel-notice.md); both spellings are in use and no objectui rule picks one. objectstack's own AGENTS.md says a Clause-②: yes takes at least minor, but objectui's AGENTS.md has not adopted that line, and in objectui's fixed group the next release is already minor from pending declarations, so this level moves only the CHANGELOG section, not the version.
  • The Clause-②: yes line stands at the start of its own line in the PR body, matches the claim (6095629427) and the dev report, and is justified by the i18n keys alone. Level and declaration are consistent.

③ Boundary flags

Dev report 6096347163: open_questions is empty; six deviations and two out-of-scope findings, each answered or escalated. The hop adds no flag: the merge commit carries no file of the PR's own and no conflict resolution.

  1. Second new module appServesNoNavigation.ts — answered, accepted: inside the claimed surface (packages/app-shell/src/), unexported, imported only by AppContent; keeping the component file component-only is the repo's react-refresh convention.
  2. Repo-root vitest invocation instead of pnpm --filter ... exec vitest — answered, accepted: procedural; the landing head's Test shards are the verdicts.
  3. Type-check through turbo with the ^build closure — answered, accepted: procedural; Type Check is green on the landing head.
  4. Model-free commit trailers instead of the harness reminder's line — answered, accepted: this is objectui AGENTS.md's trailer rule, which outranks the reminder. The three authored commits carry Claude-Session plus the model-free co-author line; the net diff on the landing head and the PR body contain no model identifier (scanned, 0 hits).
  5. Three lock queue-timeouts re-queued — answered, accepted: nothing was measured in those slots; the check-runs on the head are the gate verdicts regardless.
  6. Live browser pass NOT MEASURED — answered, accepted at this tier: both triage pins are held in RTL against the real AppContent and router, and the measured producer needs hotclm#87's backend. Recommended, not a gate here: re-run the hotclm#87 first-screen check once the objectstack console pin moves to a build carrying this head.
  7. Out-of-scope finding 1 (residual: non-empty navigation with no walkable landing still renders StudioHomePage) — escalated to the dispatching seat to file a follow-up card, carrier currently none. Reach as judged here is one step wider than the dev's list: doc-only navigation also resolves no landing, and an app whose items live only under areas opens on Studio's overview with a full sidebar, which is a metadata-authoring trap. Pre-existing and outside this card's pin; not a defect of this PR.
  8. Out-of-scope finding 2 (StudioHomePage "retired" per the maintainer's words in the card body, yet the live root of /apps/studio on main, linked from the sidebar's "Studio home" entry) — escalated as a maintainer decision for the seat (a needs-user-decision candidate): whether the Studio app's own root should keep rendering that page. Triage's ruling governs this card (/studio untouched; pins on empty navigation) and the PR follows it; retiring the page is a separate card, not a condition of this one.

Implemented-by: claude/issue-12079-empty-nav-landing
Reviewed-by: session_01B1gHb9baeX7oioD5sHVm7z

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 417e245 Oct 10, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12079-empty-nav-landing branch October 10, 2026 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

First screen after sign-in is the retired Studio home when the app serves an empty navigation

2 participants