Skip to content

fix(auth,app-shell): take the organization chain and the metadata preload off the console splash's critical path (objectui#12809) - #12855

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12809-splash-critical-path
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-12809-splash-critical-path

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12809
Clause-②: yes

The console splash now waits for the session, the membership answer, me/permissions and the metadata the current route reads. It no longer waits for get-full-organization or for metadata the route does not read.

What changed

The organization chain (packages/auth/src/AuthProvider.tsx, types.ts). When the get-session answer carries better-auth's activeOrganizationId, AuthProvider names the active organization from that id and the organization/list answer. It asks for the member row (get-active-member) at the same time as the list, not after the active organization lands. get-full-organization is no longer asked on boot. Every first-paint reader of activeOrganization reads only id, name or slug, and the list row carries all three: actionContextOrg, WorkspaceSwitcher, CurrentOrganizationIndicator, the metadata cache scope and the filter scope. The early member answer is taken once, by the first member read for the same organization, and dropped on a switch, a sign-out or an id the list does not confirm. It is not a cache: nothing time-based was added. A session answer without the key (a sign-in response, or a server without the organization plugin) takes the old path. So do mutation callers of refreshOrganizations. An organization switch, the ADR-0081 single-membership repair, the stale-id clear (objectui#12698), the session-user-change invariant (objectui#5664), sign-in and sign-out are unchanged, and their existing pins pass unchanged.

The splash gate (packages/app-shell/src/console/AppContent.tsx, new appRouteMetadataReads.ts). The scopeMetaReady preload of object, dashboard, report and page is gone. APP_ROUTE_METADATA_READS declares, per route, the buckets its element reads at render. Examples: an object list reads object; a record page reads object and page; a dashboard reads dashboard and object; a redirect reads nothing. The boot splash waits for the current route's buckets only. After the first paint, a route whose buckets have not landed shows a skeleton in the route slot, inside the console chrome, instead of an empty list or a not-found state. A bucket that is refetching keeps the route mounted. A route the host adds through extraRoutes waits for all four buckets, as before.

The route readers. One gate in AppContent replaces a wait in each reader. Without a wait, these readers would draw an empty or not-found state on an empty bucket: ObjectView, ObjectDataPage, RecordFormPage, RecordDetailView (objects), ReportView, DashboardDesignPage, the CreateAppPage and EditAppPage pickers, SearchResultsPage, and AppRootLanding's nav prune. appRouteMetadataReads-12809.test.ts reads the route table out of AppContent.tsx and fails on a route without a row, so a new route cannot skip its wait.

What the splash waited for, measured

Setup: hotcrm objectstack dev --fresh, behind a local proxy that adds 50 ms to every request on both sides. The console was a production build served by vite preview, opened in Chromium via Playwright: sign in, one warm open, then the measured open of /apps/crm_enterprise/crm_account. Times are milliseconds from navigation start; three opens per side.

main 5d69b0e this branch
splash on screen 598-1498, 571-1327, 650-1554 (756-904 ms) 615-1243, 671-1467, 670-1344 (628-796 ms)
get-session 515-587 534-603
organization/list 612-698 629-721
get-full-organization 740-1057 not requested
get-active-member 1068-1171 (after get-full-organization) 629-716 (beside the list)
me/permissions 1068-1192 737-1037
meta/dashboard, meta/report, meta/page 1198-1338 (preload, gating) 1043-1151 (chrome reads, not gating)

The waits after get-session went from four serial steps to two:

  • main: the list, then get-full-organization, then the member row and me/permissions, then the dashboard, report and page preload;
  • this branch: the list and the member row together, then me/permissions.

me/permissions used to wait for get-full-organization because the console mounts it below RequireOrganization, which waits for the organization load to finish. On the branch, slowing one request by 600 ms leaves the splash unchanged for sys_user_preference, me/localization and meta/page; slowing me/permissions moves it, as ruled ('me' is on the critical path).

A Vite dev-server open measured 1744 ms on main and 1726 ms on the branch. That interval is dominated by module loading, so the production builds above are the reading.

Screenshots (light, evidence branch, not merged here)

Tests

  • orgChainParallel-12809.test.tsx (auth): the member row is asked while organization/list is still pending; get-full-organization is never asked; the active organization is the list row the session names. CONTROLs: a StrictMode replay asks once; a switch re-reads the member row for the new organization; a session without the key takes the old path; a stale id names no organization and clears the stored id; one membership is repaired once.
  • AppContent.splashCriticalPath-12809.test.tsx (app-shell): real AuthProvider and useWorkspaceAdminStatus under AppContent. The splash leaves after the session, the member row and the route's object bucket. It does not wait for get-full-organization (whose double never answers) or for the dashboard, report and page buckets. CONTROLs: a later route draws its skeleton inside the chrome, then its content; a refetching bucket keeps the route mounted.
  • appRouteMetadataReads-12809.test.ts (app-shell): the enumeration, in both directions, plus URL-to-row matching with host routes.

Ablation, one per new pin, each mutation confirmed on disk and each restore confirmed against HEAD with an empty git diff HEAD:

  • disable the early member ask: 1 test red;
  • drop the route-reads term from the splash gate: 1 test red;
  • delete the search row: 1 test red.

Evidence, pinned to 82150d0

  • pnpm --filter @object-ui/auth test: exit 0, 34 files, 317 tests.
  • app-shell in 8 shards (pnpm exec vitest run packages/app-shell/ --shard=K/8; the whole suite exceeds the container's foreground cap): every shard exit 0. 1527 files (1526 passed, 1 skipped); 14395 tests passed, 9 skipped.
  • pnpm exec vitest run apps/console/: exit 0, 222 files, 2300 tests.
  • type-check for auth and app-shell: 0.
  • build: auth, app-shell, the console's dependency closure, and the console: all 0.
  • pnpm check:eager-closure 0, check:i18n-keys 0, check:i18n-drift 0, check:spec-symbols 0, check:new-line-citations 0, check:test-path-roots 0, node scripts/check-changeset-presence.mjs 0.
  • eslint on the touched files: 0 errors. It adds 7 react-hooks/refs warnings (render-time ref reads in the first-paint latch and the settled-type set) and removes one set-state-in-effect warning; lint sets no --max-warnings.

Deviations

  • packages/auth/src/types.ts: one new optional member, AuthClientSession.activeOrganizationId, the type of useAuth().session.
  • New packages/app-shell/src/console/appRouteMetadataReads.ts. It is not exported from the package entry. No route component was edited; the wait lives in AppContent, for the reasons under 'The route readers'.
  • useWorkspaceAdminStatus.ts is unchanged. me/permissions is the console host's ConsolePermissionsScope, not this hook, and it stays on the critical path.
  • Some existing values now resolve sooner, on a session that carries the key: activeOrganization, activeMember, isMembershipResolved, and isOrganizationsLoading turning false. activeOrganization is now the list row.

Acceptance notes

  • Out of scope and unchanged: the server-side query count of get-full-organization, and the cloud control plane's own calls. objectui#12808 already landed as 5d69b0e.
  • Observations, not filed: under StrictMode in dev, several boot requests go out twice (production sends one); MetadataProvider loads its eager app and view types one after the other; the recent-items tracker does not record the boot landing when objects are not yet loaded. All three are pre-existing and unchanged here.

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

🤖 Generated with Claude Code

https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK

…load off the splash's serial path (objectui#12809)

The active organization is named from session.activeOrganizationId and the
organization/list answer, and the member row is asked for beside the list.
AppContent waits only for the lazy metadata the current route reads.

Claude-Session: https://claude.ai/code/session_019B3t48EDiMzo9xVuXhracK
Co-authored-by: Claude <noreply@anthropic.com>
…a reads (objectui#12809)

Re-asks a route bucket that drops back to idle, and adds the changeset.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 76 chunks) 3137.2 KB 3744.1 KB
Main entry chunk (gzip) 76.8 KB 350 KB
Entry file index-3FpL8Uxr.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) 21.42KB 7.85KB
app-shell (runtime-config.js) 23.60KB 8.19KB
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) 45.59KB 12.03KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 20.67KB 6.12KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.74KB 3.94KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.27KB 1.86KB
auth (createAuthClient.js) 42.84KB 11.66KB
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.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
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) 606.28KB 146.84KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.83KB 69.44KB
fields (index.js) 291.36KB 74.46KB
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.79KB 11.90KB
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) 15.35KB 5.51KB
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) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 210.60KB 49.94KB
plugin-dashboard (index.js) 146.23KB 39.59KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 263.70KB 69.73KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.22KB 46.59KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 271.78KB 75.39KB
plugin-kanban (index.js) 62.17KB 19.22KB
plugin-list (index.js) 138.78KB 35.68KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 94.68KB 24.36KB
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.23KB 39.46KB
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.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
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-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.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

席位核对 — ACCEPT · objectui#12809 · head 82150d0 · 2026-10-17T04:32Z

Seat domain:ui#4, session session_01SJn1GEkrbbBQkhGC26wEDw. Checked against GitHub and the diff, not against the report (relayed on the card). The seat opened this PR from the dev's drafts (degraded channel). The body is the dev's draft verbatim; the title adds the card number. The seat's TIER_DEFAULT contract review follows this record.

Checklist

  • Form:

    • Draft → main, assignee os-sales.
    • The first line is Fixes #12809, and the second is Clause-②: yes, as the claim declares (6228313857).
    • Two commits, each carrying only the model-free trailer pair.
    • git merge-tree against current main is clean. 8 files, +923 / −30. No PNG, and nothing under packages/components/src/ui/**.
  • Rulings, as bound:

    • The organisation chain is no longer serial. When the get-session answer carries activeOrganizationId, AuthProvider names the active organisation from that id and the organization/list answer. It asks get-active-member at the same time as the list. get-full-organization leaves boot: every first-paint reader of activeOrganization reads only id, name or slug, and the list row carries all three, as measured live.
    • The early member answer is held with its org and user. It is taken once, by the first member read for that same org, and dropped on a switch, a sign-out or an id the list does not confirm. Nothing is time-based.
    • A session answer without the key (a sign-in response, or a server without the organisation plugin) takes the old path. So does every mutation caller of refreshOrganizations.
    • The preload leaves the splash gate. scopeMetaReady is gone. The splash waits for the session, the member row, me/permissions (the ruled "me") and the buckets the current route reads, per a declared per-route table. After the first paint, a route whose buckets have not landed shows a skeleton inside the console chrome. A host route (extraRoutes) waits for all four buckets, as before.
    • Unchanged, as bound: an organisation switch, the ADR-0081 repair, objectui#12698, objectui#5664, sign-in and sign-out. Their existing pins pass unchanged; that is the CONTROL.
  • Mechanism assumptions: Implement visual designer for Object UI schemas #1–Add public roadmap, VitePress documentation site, and GitHub Pages deployment #3 held. Add default props to all components to prevent collapse in designer #4 was falsified, a good result. useWorkspaceAdminStatus issues no request. me/permissions comes from the console's ConsolePermissionsScope, which mounts below RequireOrganization and so used to wait for get-full-organization. It stays on the critical path, and now starts about 330 ms sooner.

  • Live readings (hotcrm objectstack dev --fresh behind +50 ms per request on both sides; production builds via vite preview; three opens per side; ms from navigation start):

    main 5d69b0e this branch
    splash on screen 756–904 ms 628–796 ms
    get-full-organization 740–1057 not requested
    get-active-member 1068–1171, after the full org 629–716, beside the list
    me/permissions 1068–1192 737–1037
    dashboard, report, page metadata 1198–1338, gating 1043–1151, not gating

    The waits after get-session went from four serial steps to two. Isolation on the branch, adding 600 ms to one request at a time:

    • sys_user_preference, me/localization and meta/page leave the splash unchanged;
    • me/permissions moves it.
  • Screenshots, read by the seat (never-merged claude/issue-12809-evidence at 42c8870):

    • the console home after the change (CONTROL): it renders, with the apps, My work and Recently Accessed;
    • Sales Performance opened after the first paint, its metadata slowed to 4 s: the dashboard-shaped skeleton inside the chrome, with the sidebar and the top bar live;
    • the same route once the metadata landed.

    Sent to the maintainer at 2026-10-17T04:32Z.

  • Tests:

    • orgChainParallel-12809 (auth), 6 tests: the member row is asked while the list is pending; the full organisation is never asked; the active org is the list row the session names. CONTROLs:
      • a StrictMode replay asks once;
      • a switch re-reads the member row;
      • a session without the key takes the old path;
      • a stale id clears the stored id;
      • one membership is repaired once.
    • AppContent.splashCriticalPath-12809 (app-shell), 3 tests: the real AuthProvider and useWorkspaceAdminStatus under AppContent. The splash leaves after the route's object bucket, not after a never-answering get-full-organization or the other buckets. CONTROLs: a later route draws its skeleton in the chrome, then its content; a refetching bucket keeps the route mounted.
    • appRouteMetadataReads-12809 (app-shell), 18 tests: reads every Route path out of AppContent.tsx and checks it against the table, in both directions.
    • Three ablations, one per pin. Each went red and was restored to an empty git diff HEAD.
    • Package suites:
      • @object-ui/auth: 317 tests;
      • app-shell in 8 shards: 14395 tests passed;
      • apps/console: 2300 tests, all exit 0.
    • Type-check and build for both packages and the console, check:eager-closure, check:spec-symbols (objectui#12808's red), the i18n checks, changeset presence: all exit 0.
  • Changeset: @object-ui/auth and @object-ui/app-shell, a minor each, for the new optional AuthClientSession.activeOrganizationId; user-facing prose.

  • CI: running at this record. Landing waits for green and for the contract review below.

Rulings

  1. The contract conflict: as dispatched. The relay is refused from the dev's container, so the seat opened the PR and relayed the report.

  2. One gate in AppContent, not a wait in each reader: A, accepted as the in-surface reading of the ruling. The ruling's outcome holds:

    • a route waits for the buckets it reads;
    • it shows a skeleton, never an empty or "not found" state;
    • the enumeration is pinned against the route source, so a new route cannot skip its row.

    Waits in each reader would have landed in views/, layout/, plugin-designer and apps/console, all outside the claimed surface, and a missed reader would fail silently.

  3. A session answer without the key takes the old path: A, as built. Reading the absence as "no active organisation" would be a wrong answer on a sign-in response or a server without the plugin.

  4. packages/auth/src/types.ts: accepted. It is outside the listed files, but the claim's Clause-② text anticipated it: one optional member on the type of useAuth().session, which the ruling's "fewest optional members" allows. It is reported under deviations.

  5. The new appRouteMetadataReads.ts: accepted. It sits beside AppContent under console/, and the package entry does not export it.

  6. The observations stay notes, not filed. All three are pre-existing and unchanged here:

    • StrictMode double requests in dev;
    • MetadataProvider loads app and view serially;
    • the recent-items tracker misses the boot landing.

    The serial app → view load is the next lever on the splash. If it is measured to gate it, it belongs on its own card.

Implemented-by: cloud session session_019B3t48EDiMzo9xVuXhracK, branch claude/issue-12809-splash-critical-path


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 82150d02acaab385279fffb45ebcf1717fce1c86
Local-runs: none

Face: the Clause-②: yes declaration of the claim 6228313857 on objectui#12809 («拿不准 ⇒ 按 yes»: @object-ui/auth exports useAuth's context and useWorkspaceAdminStatus, and when their values resolve may change). The PR's file list carries no packages/spec/src/** path and no governed path.

Inputs read:

  • the card and the claim;
  • the diff at the head above;
  • the changeset;
  • the two package entries (packages/auth/src/index.ts, packages/app-shell/src/index.ts);
  • AuthContext.ts and MetadataProvider.tsx at the head;
  • the PR body.

Nothing was built or run locally.

① Derived judgments

  • AuthClientSession.activeOrganizationId?: string | null: one new optional member on a type the @object-ui/auth entry re-exports (index.ts :99). It is additive, and it describes a field better-auth's organisation plugin already puts on the get-session answer. No existing member changes type or meaning. Acceptable: the fewest optional members, as ruled.

  • useAuth()'s context type: AuthContext.ts :150 still types refreshOrganizations as () => Promise<AuthOrganization[] | undefined>. The new second parameter is on the provider's internal callback only. Every out-of-package caller (SetupPage.tsx :146) calls it with no argument, so it takes the server path, as before. No member moves.

  • When existing values resolve (a published-face change, reported):

    • Earlier, on a session that carries the key: activeOrganization, activeMember, isMembershipResolved, and isOrganizationsLoading turning false. Earlier only: no value is skipped, reordered, or served from a different question.
    • activeOrganization is now the organization/list row. It has the same AuthOrganization type and the same id, name, slug, logo and createdAt. The full-organization answer also carried members and invitations, which the dev measured no first-paint reader to use.
    • Not moved: useWorkspaceAdminStatus is unchanged, and its me/permissions wave is the console host's.
  • The early member answer (bootMemberRef) is asked for the session's own active organisation and taken only by the first member read for that same org id. It is dropped:

    • on a switch;
    • on a sign-out;
    • on an active organisation the list did not confirm, or that the ADR-0081 repair replaced.

    A user change re-asks, because the held entry is keyed on userId. It answers one question once and is never a time-based cache. Correct.

  • @object-ui/app-shell: DefaultAppContent (index.ts :276) keeps its props (extraRoutes, extraRoutesNoApp). appRouteMetadataReads.ts is module-internal. getTypeStatus was already on the metadata context, as a pure read.

    The behaviour change after the first paint, a skeleton in the route slot while that route's buckets load, is in the changeset. A host's extra routes wait for all four buckets, as before.

  • Render-phase refs: two refs are written during render:

    • the first-paint latch, which can only fall from the splash to the in-chrome skeleton;
    • the answered set, a monotone record of which buckets settled.

    A discarded concurrent render can at worst show the skeleton instead of the splash. No wrong data is shown. Acceptable; eslint records them as warnings, and lint sets no --max-warnings.

  • The sidebar on a route that reads no bucket (marketplace, metadata/package/*, the not-found route):

    • useNavMemberGates' capability prune shows every object entry while objects is empty.
    • AppContent's own objects read still kicks that load at once, through MetadataProvider's lazy getter.
    • An entry for an object this runtime lacks can show until the list lands, then leave.

    The default landing and every object, record, dashboard, report and page route read object and still hold the splash for it. Noted for the maintainer, not blocking.

② Semver level

One changeset: @object-ui/auth minor and @object-ui/app-shell minor, in pre mode on tag next.

  • auth: one optional member is added to an exported type, and some existing values resolve earlier.
  • app-shell: a behaviour change in the exported DefaultAppContent, with no prop change.

Nothing is removed. Consistent.

③ Boundary flags

  • None that needs the maintainer's ruling. One behaviour to know, from ①: on a route that reads no metadata bucket, the sidebar can briefly show an object entry the capability prune later removes.

Implemented-by: session_019B3t48EDiMzo9xVuXhracK
Reviewed-by: session_01SJn1GEkrbbBQkhGC26wEDw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing · head 82150d0 · 2026-10-17T04:37Z

  • Head since the ACCEPT: unchanged, 82150d0. The ACCEPT 6229670863 and the TIER_DEFAULT contract review 6229693622 (PASS) were both written at this head.
  • Checks on 82150d0: 36 runs, 33 green and 3 expected skips, none red. That includes Repo Gates, Build & E2E, all test shards, Type Check (with check:spec-symbols), Lint, Changeset Bump Policy and Bundle Analysis. The eager closure is 3,137.2 KB against the 3,744.1 KB ceiling.
  • Merge state: clean against main 4b3d6ed.
  • Commit messages: both commits carry only the model-free trailer pair, so the squash adds no model name to main.
  • Action: mark it ready for review and enable auto-merge through the merge queue. Fixes #12809 closes the card on merge.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 17, 2026 04:38
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 17, 2026 04:38
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit 84b8ffa Oct 17, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12809-splash-critical-path branch October 17, 2026 04:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants