Skip to content

feat(app-shell): the Studio landing lists recent packages, stored by identity and labelled at render (objectui#11863, Q3) - #11996

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11863-studio-recent-packages
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-11863-studio-recent-packages

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #11863 (Q3)

Clause-②: yes. This widens the published RecentItemType union of @object-ui/app-shell by one member, 'package', so RecentNamedItem and RecentItem carry it too. A host's exhaustive switch or Record over the union must handle it. The published useRecentItemLabel also takes one new optional argument, { packages }. No export is added or removed. The changeset is minor for @object-ui/app-shell.

This covers only Q3 of #11863, per the retriage ruling (Q3 → A): recent packages on the Studio landing. Q1 (the /studio header) and Q2 (search) follow in later rounds, so #11863 remains open. Recent objects and flows wait for a later card. #11912 is not addressed here.

What changed

  • Store (RecentItemsProvider.tsx): RecentItemType gains 'package', a RecentNamedItem kind. A package entry is stored by identity, in the objectui#11678 shape: id is package:PKG, name is the package id, and href is the bare /studio/PKG route, which opens the Data pillar. It stores no display text. If a caller hands in a label, the reader drops it.
  • Producer (StudioRoute.tsx, the route owner): the :packageId/:tab leg is now StudioPackageBuilder, which records the visit and then renders the builder exactly as before. It sits behind the entry gate, so a refused principal records nothing. It is keyed on the package, not the pillar, so moving between one package's pillars changes nothing and writes nothing. The package-less scope ~org is skipped. StudioLanding's header is untouched (it belongs to Q1).
  • Label (useRecentItemLabel): a new package case returns the name of the package's row in the packages list the caller hands in. With no row, or no list, it shows the machine name. That is what objectui#11678 draws for a missing object, page or report.
  • Landing (BuilderLanding.tsx): a "Recently viewed" row (the existing engine.home.recent key) sits above the package cards. It shows the last three package entries, labelled from the package list the page already loads, and each links to its /studio/PKG route. It appears only once that list has answered, so no entry is drawn as its id while the list loads, and it is absent when there are none. The existing cards are unchanged.
  • i18n: one home.recentApps.itemType.package row in each of the ten packs. pnpm check:i18n-keys checks this key family against the union exactly.

H2: every consumer of the recent list, and what it does with a package entry

Consumer Before Now
Home's Recently Accessed (HomePage, then HomeRail.HomeContinue) allow-list: object, dashboard, page, record unchanged: the package entry is left out. Home loads no package list, and packages are listed on the Studio landing.
RecentApps (exported, not mounted in this tree) draws what its caller passes unchanged: the caller chooses the kinds (Home filters before it renders).
App sidebar Recent group (UnifiedSidebar) drew every kind leaves package entries out. It loads no package list, and GET /api/v1/packages is a Studio read, so drawing the entry could only show the id.
Command palette recent group type === 'record' only unchanged: left out
metadata-admin app home (StudioHomePage) drew every kind, through an exhaustive Record over the type leaves package entries out. Its own package list (buildPackageScopeOptions) holds writable bases only, so a read-only package would draw as its id.
useRecentItemLabel its default branch returned item.label new package case (above)
BuilderLanding none the new consumer; it labels from its list

No surface draws a live package as its raw id.

Hypotheses, measured

  • H1 holds. I reverse-verified with every non-test source file restored to the base 1473757bf and the new tests run over it. With the builder mounted at /studio/com.acme.crm/data, the real RecentItemsProvider held [] (expected [] to have a length of 1 but got +0). After the change, the same visit stores exactly one entry: {"id":"package:com.acme.crm","type":"package","name":"com.acme.crm","href":"/studio/com.acme.crm","visitedAt":…}. It has no label, and the persisted copy equals it. In the browser (below), two visits and one pillar switch inside the first package left exactly two entries, each in that shape.
  • H2: the table above.
  • H3 holds. A renamed package draws its new name (unit test, and in the browser Acme CRM became Acme Sales). A package the list no longer has draws its id, com.example.showcase, following the missing-object precedent rather than a new rule. A package's manifest name is a plain string in the spec (ManifestSchema.name is z.ZodString), so it has no per-language form. On the landing, a language switch can only change the row's heading, the existing engine.home.recent key. I did not pin a language switch: the name has nothing to switch.
  • H4 holds. In Chromium at 1440×900, against a mocked API and my own vite dev server from this worktree at c55b3f62b (the later commit touches neither the landing nor the route), with three packages (two writable, one read-only):
    • The landing ends at y=630 with no recent package, and at y=735 with two. The rest of the page stays as it was, and the viewport still holds the whole page.
    • The recent links read Showcase | com.example.showcase and Acme CRM | com.acme.crm (newest first), and the first one opened /studio/com.example.showcase/data.
    • There were no page errors.
    • The first dev's earlier y=480 reading came from a different package set, so I compared the two states with the same fixture.

Tests

All readings are at head c39af0271 (git rev-parse --short HEAD) unless a line names another commit. Commands were run from the worktree root, through the shared verify lock.

  • New: StudioRoute.recentPackage-11863.test.tsx (the real studioRoutes inside the real store), BuilderLanding.recentPackages-11863.test.tsx, UnifiedSidebar.recentPackage-11863.test.tsx and StudioHomePage.recentPackage-11863.test.tsx. New cases were also added to useRecentItemLabel.test.tsx and RecentItemsProvider.test.tsx.

  • Reverse verification, run from the committed change c55b3f62b with all six source files at the base content. The swap was checked on disk by blob hashes, and a trap restored from HEAD, proven by blob hashes and an empty git diff HEAD. Result across the six files: Tests 17 failed | 22 passed (39). The 22 that stayed green are the 14 pre-existing provider cases, the 4 pre-existing resolver cases, and 4 negative controls: the landing with no recent package, the sidebar control, and the two route cases that must record nothing. Direction: every case that asserts the new behaviour went red.

  • Test union at c39af0271:

    • every test that imports RecentItemsProvider, useRecentItems, useTrackRouteAsRecent, useRecentItemLabel, BuilderLanding, StudioRoute, UnifiedSidebar or StudioHomePage (68 files, from git grep -l): Test Files 68 passed (68) · Tests 616 passed (616);
    • scripts/__tests__/: Test Files 179 passed | 2 skipped (181) · Tests 5452 passed | 2 skipped (5454);
    • packages/i18n/ at c55b3f62b: Test Files 82 passed (82) · Tests 1316 passed | 13 skipped (1329). The later commit touches no i18n file.
  • Type-check, at c39af0271 and after building the console's dependency closure (turbo run build --filter='@object-ui/console^...', Tasks: 34 successful, 34 total), via pnpm --filter @object-ui/app-shell --filter @object-ui/console --filter @object-ui/i18n run type-check. Each script name was echoed (packages/i18n type-check$, packages/app-shell type-check$, apps/console type-check$) and the exit was 0. The first build, at c55b3f62b, failed with TS2741 in StudioHomePage.tsx: Exclude over the RecentItem union removes no arm, so the icon map still needed a package key. c39af0271 narrows on the type member instead.

  • Gates at c39af0271, all exit 0:

    • pnpm check:i18n-keys: 116 member key(s) checked exactly, with the home.recentApps.itemType. family read against RecentItemType;
    • check:i18n-drift: No en value changed in this range.;
    • check:i18n-dead-keys, report-only: the new key sits under the live home.recentApps.itemType. head;
    • check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s);
    • check:control-bytes: OK;
    • node scripts/check-changeset-presence.mjs: 22 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s);
    • check:changeset-claims and check:pending-changeset-literals: no findings;
    • check:skill-examples: Every marked skill example holds up against the built types.
  • check:readme-exports: exit 1, from an unmet precondition only. 8 bindings were unjudged because @object-ui/cli and @object-ui/plugin-ai are not in the console closure, so they were not built. All 8 are in packages/cli/README.md and packages/plugin-ai/README.md, which this diff does not touch. Of the 579 self-bindings, 571 were judged real, 0 fabricated and 0 wrong-path. Every @object-ui/app-shell (22) and @object-ui/i18n (29) README binding is real, useRecentItems and useRecentItemLabel included.

  • Bundle (eager closure): I ran two vite builds of the console on the same tree and dependency builds. The second had every changed source file and the ten packs at the base content (restored from HEAD by trap, proven by blob hashes). Readings from pnpm check:eager-closure:

    • before: 3311.5 KB gzipped (3390969 B);
    • after: 3311.6 KB gzipped (3391056 B), against a budget of 3312.0 KB, headroom 0.4 KB;
    • delta: +87 B gzipped (+348 B raw). Rolldown folded the small useRecentItemLabel chunk into nav-selection, now that BuilderLanding imports it too.

    The PR's own Bundle Analysis verdict: NOT MEASURED, because it had not posted when this was written.

  • Lint, narrowed: eslint --format json over the 22 changed .ts/.tsx files (the count is from its JSON output) gave 0 errors. Each file's warning count equals the count for its base content, linted through stdin under the same path. The config applies to **/*.{ts,tsx} and is not type-aware (eslint.config.js sets no parserOptions.project or projectService), so this diff cannot move a verdict on an untouched file. The repo-wide pnpm lint is CI's.

  • Not run: a reverse check of check:i18n-keys with the new en row removed. The session's safety classifier refused the scripted edit, so it is NOT MEASURED.

Acceptance notes

  • Docs (AGENTS.md Add automated testing infrastructure and CI/CD workflows #2), outside the claimed surface, so not edited. packages/app-shell/README.md ("A recent object, dashboard, page or report entry stores its identity only…") and content/docs/guide/user-state-persistence.md ("An object, dashboard, page or report entry in the recent list is stored as its type and name…") do not yet name the package kind. Suggested sentence for both: "A Studio package entry (type: 'package') is stored the same way; label it with useRecentItemLabel({ packages }), passing the package list you loaded, or leave it out where you have no list." I left this for the seat to add to the claim or to carry.
  • RecentApps (exported, not mounted in this tree) draws whatever list its caller passes and labels it with useRecentItemLabel() and no package list. A host that hands it an unfiltered list would see a package entry drawn as its id. No caller in this tree does that: Home filters first. Carrier: none. Noted here, not filed.

Dev session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw


Generated by Claude Code

claude added 3 commits October 8, 2026 16:10
…identity and labelled at render (objectui#11863, Q3)

A /studio/PKG/TAB visit is recorded by the console's StudioRoute as a recent
`package` entry: id `package:PKG`, name the package id, href `/studio/PKG`,
no display text. RecentItemType gains `package`; useRecentItemLabel takes an
optional `{ packages }` list that labels it, falling back to the machine name
as objectui#11678 does for a missing item. BuilderLanding lists the last
three above its package cards once its package list has answered. The app
sidebar and the metadata-admin home, which load no full package list, leave
package entries out. One `home.recentApps.itemType.package` row per pack.

Claude-Session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw
Co-authored-by: Claude <noreply@anthropic.com>
…y, and match the sidebar row by its label (objectui#11863)

Claude-Session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw
Co-authored-by: Claude <noreply@anthropic.com>
…er, and drop a test cast (objectui#11863)

`Exclude` over the `RecentItem` union removes no arm, since `RecentNamedItem`'s
`type` is itself a union, so the icon map still demanded a `package` key and
the package build failed (TS2741). The listed kinds are now
`Exclude<RecentItem['type'], 'package'>`.

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

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3311.3 KB 3312.0 KB
Main entry chunk (gzip) 71.5 KB 350 KB
Entry file index-CQtLPrfZ.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) 18.33KB 6.73KB
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.07KB 141.28KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
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) 41.50KB 11.82KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.22KB 68.84KB
plugin-kanban (index.js) 52.22KB 16.38KB
plugin-list (index.js) 117.72KB 29.41KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.09KB 22.94KB
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) 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.25KB
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

…keep the README's RecentList true for it (objectui#11863)

Beside the sentence listing the recent-entry kinds stored by identity, the
package README and the user-state guide now say that a Studio package entry
(`type: 'package'`) is stored the same way and is labelled with
`useRecentItemLabel({ packages })`, or left out where no package list is
loaded. The README's RecentList snippet loads no package list, so it leaves
package entries out.

Claude-Session: https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 1 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/11806-login-server-unreachable.md

  • names packages/app-shell/README.md → packages/app-shell/README.md — edited by this change

    packages/app-shell/README.md documents the two new states in its sign-up table. Its useSignUpOffer recipe for hosts now asks decideSignUpOffer only once the read has answered, and answers unreachable for a failed read.

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

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3311.3 KB 3312.0 KB
Main entry chunk (gzip) 71.5 KB 350 KB
Entry file index-CQtLPrfZ.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) 18.33KB 6.73KB
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.07KB 141.28KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
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) 41.50KB 11.82KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.22KB 68.84KB
plugin-kanban (index.js) 52.22KB 16.38KB
plugin-list (index.js) 117.72KB 29.41KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.09KB 22.94KB
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) 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.25KB
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: 4f42ef8019674d7f9b702921172a12c18252deb9
Local-runs: none

PR #11996 against main, merge base 1473757 (confirmed with git merge-base against origin/main on the shared checkout; the PR API's base.sha names the current main tip, not the base). Inputs: card #11863 body and all nine comments, the PR body and file list (25 files), the net diff, and the 46 check-runs on this head. Isolated subagent; read-only; nothing built, run or re-run.

Check-runs on this head: 43 success, 3 skipped (the two coverage legs and dependabot, path-skipped by design), 0 failures. Among the successes: Type Check, Lint, Test (8 shards, dist pins, aggregate), Build and E2E, Bundle Analysis, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Governed Surface Queue Guard, Line Citation Gate, README Export Check, Doc Snippet Type Check, Skill Example Check. The file list touches no governed surface (.claude/**, skills/**, docs/adr/**, docs/NORTH-STAR.md, AGENTS.md, CLAUDE.md), and the head repo is the base repo.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against the ruling (card comments 6054672824 Q3 to A, 6060520389 lane) and the claim 6063712393.

  1. RecentItemType gains 'package' (packages/app-shell/src/context/RecentItemsProvider.tsx; exported through the hooks/useRecentItems shim and the package index). RIGHT: it is the one widening the ruling ordered, spelled on the single literal-union line the i18n key gate reads as vocabulary: kind 'union', and pnpm check:i18n-keys is a step of ci.yml, green on this head.
  2. RecentNamedItem.type and RecentItemInput admit a package entry (type: Exclude of record and metadata now includes package). RIGHT: the objectui#11678 identity shape, name and no label; the new provider test pins that a handed-in label is dropped on write and that the persisted copy carries none.
  3. NAMED_TYPES gains 'package', so a persisted package entry is now READ by readEntry instead of returned as null. RIGHT: it follows the same prefix rule as the other named kinds (name from id after package: when absent), and a persisted list that predates this head cannot contain the kind, so nothing stored is re-interpreted.
  4. useRecentItemLabel(sources = {}) gains an optional packages argument (a ReadonlyArray of id and name rows, or null); the RecentItemLabelResolver type is unchanged; the new package arm returns the row's name or the machine name. RIGHT: additive and call-compatible; packages enters the useMemo dependency list as the payload the arm reads, which is the keying AGENTS.md [WIP] Enhance every detail of the designer #10 asks for.
  5. Producer: StudioPackageBuilder on the :packageId/:tab leg (apps/console/src/components/StudioRoute.tsx) records { id: package:PKG, type: package, name: PKG, href: /studio/PKG }. RIGHT: the element is a child of StudioRoute (ProtectedRoute, then RequireStudioAccess, then Outlet), so a refused principal never mounts it; ~org is skipped by STUDIO_ORG_SCOPE_SEGMENT; the effect is keyed on packageId alone, with addRecentItem held out exactly as useTrackRouteAsRecent holds it (AGENTS.md [WIP] Enhance every detail of the designer #10). RecentItemsProvider is mounted in ConsoleShellProviders above every console route, so the store the route writes is the real one, not the hook's no-op fallback. useTrackRouteAsRecent is untouched and still reads /apps paths only, so nothing double-records. One edge, named rather than hidden: a URL naming a package that does not exist still records, and the landing then draws its id, the missing-object precedent the ruling accepted.
  6. Consumers, read one by one at this head. BuilderLanding lists at most 3 package entries, only once pkgs is non-null, labelled from that list: RIGHT. UnifiedSidebar and StudioHomePage filter package entries out, and StudioHomePage narrows its exhaustive RECENT_ICONS Record to ListedRecentType (the TS2741 the dev measured): RIGHT, each loads no full package list, so the only alternative was drawing an id. HomePage already allow-lists object, dashboard, page and record at its recentApps filter, and CommandPalette keeps type === 'record': unchanged and correct. RecentApps and HomeRail use partial Record over string icon and tone maps with fallbacks, so a package entry handed to them would render with its id as label; no caller in this tree hands them one, and the README snippet now teaches the filter. Acceptable as noted by the dev; carrier none.
  7. i18n: home.recentApps.itemType.package in all ten packs. RIGHT and REQUIRED: the key gate checks the family against the union as an exact set, so the member cannot land without the row. No surface renders this row today (Home filters packages out before recentItemTypeLabel runs), so the row is gate-required rather than user-visible; it is not dead under the gate's own definition.
  8. Docs (prose face, not a contract face per the review reference; read anyway since they are in the diff): packages/app-shell/README.md and content/docs/guide/user-state-persistence.md each gain one sentence naming the kind and useRecentItemLabel({ packages }); the README RecentList snippet filters type !== 'package'. RIGHT: every claim in both sentences matches the code at this head; Doc Snippet Type Check and README Export Check are green.
  9. Not changed, verified: no export added or removed from the app-shell index; StudioLanding's header untouched (Q1's); no packages/spec change; no narrowing of any published accept set anywhere in the diff.

② Semver level

Clause-②: yes (widening)

  • The PR body's declaration reads Clause-②: yes, and the changeset body says Clause-②: yes (widening). Both match the diff: items 1 to 4 above widen a published type and a published function signature of @object-ui/app-shell; nothing narrows a published accept set (the sidebar and metadata-admin filters narrow what is DRAWN, not what is accepted).
  • .changeset/11863-studio-recent-packages.md declares @object-ui/app-shell: minor (the ruled level, 6054672824), @object-ui/i18n: patch, @object-ui/console: patch. All three are in the fixed group of .changeset/config.json, so the group releases at minor; no major is declared (Changeset Bump Policy green); presence is declared for every changed released source file (Changeset Declaration green). Level MATCHES what the diff publishes.
  • Note, not a finding: a new @object-ui/i18n key is arguably minor on that package alone; pending changesets in the tree spell key additions both ways, and under the fixed group the released version is the same either way.
  • Changeset Claim Re-read named .changeset/11806-login-server-unreachable.md for the README; the dev read the paragraph against the diff and it still holds (the README hunks sit in the recent-items section only). I read the same two hunks: they do not touch the sign-up table or the useSignUpOffer recipe. Agreed.

③ Boundary flags

Each dev deviation and each open_questions entry from reports 6065684736 and 6065785121, answered or escalated.

  • D1 StudioHomePage.tsx and useRecentItemLabel.ts not named in the claim's parenthetical. ANSWERED, inside scope: the claim's bullet "Every consumer that switches on RecentItemType" covers StudioHomePage (its exhaustive Record does not compile without the member), and useRecentItemLabel is named in the claim's first bullet as the label seam.
  • D2 docs not edited in round 1. RESOLVED by patch round 1 (this head): both files carry the sentence the dev proposed; judged in ① item 8.
  • D3 H1 before-reading taken as a reverse verification after the implementation commit. ANSWERED: same mechanism through the real route; the base-source run went red on every new-behaviour case and stayed green on the four negative controls, which is the direction the hypothesis needed.
  • D4 changeset carries i18n and console at patch beside app-shell minor. ANSWERED in ②: consistent with the fixed group; nothing to change.
  • D5 commit trailers follow the model-free pair. ANSWERED: all four branch commits end with Claude-Session: and Co-authored-by: Claude, as objectui AGENTS.md prescribes.
  • D6 stray /suites.pid in the dev container. ESCALATED to the seat as housekeeping: outside the repository, absent from the diff and the file list, no effect on this PR.
  • D7 vitest guard refused mixing a directory with file paths. Informational; no action.
  • D8 (round 1 report) claim-surface amendment for the two docs files arrived in the coordinator's message and is recorded on no card comment. ESCALATED to the seat: write the one-line amendment on the card so the claim's file surface matches the PR's file list. Not a FAIL reason: the two files are a prose face, AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 requires them, and the diff in them is judged right above.
  • D9 worktree re-created from the remote head for the docs round. Informational; the branch is one lineage (four commits on the merge base).
  • Open question (report 1): extend the claim to the two docs files, A/B/C. ANSWERED A by the seat and implemented on this head; the second report lists no open questions.
  • Out-of-scope note: RecentApps labels without a package list. ANSWERED in ① item 6; carrier none is the right disposition, since no caller in this tree reaches the case and the README now teaches the filter.

No FAIL reason found. The landing act (ready, queue) stays the dispatching seat's; this record is the Tier S review of record for this head only.

Implemented-by: claude/issue-11863-studio-recent-packages
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 336 chunks) 3311.5 KB 3312.0 KB
Main entry chunk (gzip) 71.5 KB 350 KB
Entry file index-Ddy2HQbS.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) 18.33KB 6.73KB
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.07KB 141.28KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 268.74KB 68.04KB
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) 41.50KB 11.82KB
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.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.71KB 23.25KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 248.57KB 65.47KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.11KB 45.88KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 248.22KB 68.84KB
plugin-kanban (index.js) 52.22KB 16.38KB
plugin-list (index.js) 117.80KB 29.42KB
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.10KB 11.81KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.09KB 22.94KB
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) 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.25KB
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: b4914d75e14a8713bffacd1dbab4f0d58002b86a
Local-runs: none

PR #11996 against main, merge base b13ea3c67 (from git merge-base origin/main on the read-only clone after git fetch origin main pull/11996/head). Inputs: card #11863 body and all ten comments (rulings 6054672824 and 6060520389, claim 6063712393, dev reports 6065684736, 6065785121 and 6066344660 among them); the PR body, its five comments (the earlier record 6066168690 on head 4f42ef80 among them), its file list (25 files) and the net diff against main (+881 / −38); the 43 check-runs on this head. Isolated subagent; read-only; nothing built, run or re-run.

The head moved: what the move changed, and what it did not. The move is one merge commit, b4914d75, parents 4f42ef80 (the previous head) and b13ea3c67 (then origin/main), with no change of its own. Before leaning on anything in the earlier record I established that the net diff is the same patch:

  • git diff 1473757bf 4f42ef80 and git diff b13ea3c67 b4914d75, each piped to git patch-id --stable, both read cc2bdcabeabb31304c517a7ec44f52a249db5ec4; the merge's diff against its second parent has the same patch-id; both net diffs are 25 files, +881 / −38.
  • A textual diff of the two net diffs (index lines dropped) differs in exactly one hunk header, en.ts -3138 became -3139, because main added one row above the home.recentApps.itemType table. No content line differs.
  • Main's side of the merge (1473757bf..b13ea3c67: 10 commits, 93 files) overlaps the PR's file list only in the ten packages/i18n/src/locales/*.ts packs; the merge was clean and the net diff unchanged, so main's rows and this PR's one row sit side by side. Of the 93 main-side files, the only one that names a recent-items or Studio-route symbol at this head is packages/app-shell/src/layout/AppHeader.tsx (objectui#11912), and its hits are appStudioRoutePath only, nothing of the recent-items store. packages/app-shell/src/index.ts is unchanged between the two bases. The set of non-test useRecentItems( call sites at this head equals the set at the old base plus this PR's two (StudioRoute, BuilderLanding); no consumer that switches on RecentItemType arrived from main.
  • So the earlier record's judgments on the diff carry over to the same patch; they are re-derived below from the diff itself rather than adopted. What the move did change (the base, the check-runs, the bundle reading, and one new dev report, 6066344660) is judged fresh.

Check-runs on this head: 43 in all: 40 success, 3 skipped (Test (coverage), Test (coverage shard …/4), dependabot, path-skipped by design), 0 failure, 0 pending. The name set is identical to the one on 4f42ef80. Among the successes: Type Check (whose job runs pnpm check:i18n-keys, ci.yml line 517), Lint, Test (8 shards, dist pins, aggregate), Build & E2E, Bundle Analysis, Changeset Bump Policy, Changeset Declaration, Changeset Fixed Group Check, Changeset Claim Re-read, Governed Surface Queue Guard, Line Citation Gate, README Export Check, Doc Snippet Type Check, Doc Component Type Check, Skill Example Check, Spec Main Shape Gate. The file list touches no governed surface, and the head repo is the base repo.

Bundle, the reason the head moved: the Console Performance Budget on this head (PR comment 6066301017; run 37822666205, the Bundle Analysis check-run) reads an eager closure of 3311.5 KB against the 3312.0 KB budget, PASS. The gate is green and that is the verdict. Headroom is 0.5 KB, so the next card that grows the eager closure meets the budget constant: noted for the seat, not a finding on this PR.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against the ruling (6054672824 Q3 → A; 6060520389 lane) and the claim 6063712393.

  1. RecentItemType gains 'package' (packages/app-shell/src/context/RecentItemsProvider.tsx line 54; exported through the hooks/useRecentItems shim and the package index). RIGHT: it is the one widening the ruling ordered, spelled on the single literal-union line that scripts/check-i18n-call-site-keys.mjs reads as the vocabulary of the home.recentApps.itemType. family (kind: 'union'), and that gate is a step of the Type Check job, green on this head.
  2. RecentNamedItem.type (the Exclude of record and metadata) now admits package, so RecentItemInput and RecentItem carry it too. RIGHT: the objectui#11678 identity shape, name and no label; the new provider case pins that a handed-in label is dropped on write and absent from the persisted copy, and that a stored entry reads back unchanged.
  3. NAMED_TYPES gains 'package', so a persisted package entry is READ by readEntry instead of returned as null. RIGHT: it follows the same prefix rule as the other named kinds, and a list persisted before this head cannot hold the kind (an unknown type was dropped on read), so nothing stored is re-interpreted; a build older than this head reading a newer list drops the entries, which is the pre-existing disposition of an unknown kind.
  4. useRecentItemLabel(sources = {}) gains an optional packages argument (a ReadonlyArray of id and name rows, or null); RecentItemLabelResolver is unchanged; the new package arm returns the row's name or the machine name. RIGHT: additive and call-compatible (a call with no argument resolves every other kind as before), and packages sits in the useMemo dependency list as the payload the arm reads (AGENTS.md [WIP] Enhance every detail of the designer #10). The new label cases pin the rename on re-render, the missing row and the no-list call.
  5. Producer: StudioPackageBuilder on the :packageId/:tab leg (apps/console/src/components/StudioRoute.tsx) records { id: package:PKG, type: package, name: PKG, href: /studio/PKG }. RIGHT: the element is a child of StudioRoute (ProtectedRoute, then RequireStudioAccess, then Outlet), so a refused principal never mounts it (pinned: a setup.access-only principal lands on /home with nothing persisted); ~org is skipped by STUDIO_ORG_SCOPE_SEGMENT (pinned); the effect is keyed on packageId alone, with addRecentItem held out exactly as useTrackRouteAsRecent holds it at its lines 173 to 174; the bare :packageId leg redirects to data and then records (pinned); a pillar switch inside one package leaves the persisted copy byte-identical (pinned). RecentItemsProvider is mounted in packages/app-shell/src/console/ConsoleShell.tsx line 169, above every console route, so the store the route writes is the real one, not the hook's no-op fallback. useTrackRouteAsRecent is untouched and still reads /apps paths only, so nothing double-records. The route test drives the REAL studioRoutes inside the REAL provider and reads the persisted copy, not a spy. One edge, named rather than hidden: a URL naming a package that does not exist still records, and the landing then draws its id, the missing-object precedent the ruling accepted.
  6. Consumers, read one by one at this head. BuilderLanding lists at most RECENT_PACKAGES_SHOWN (3) package entries, only once pkgs is non-null, labelled from that list, under the existing engine.home.recent heading: RIGHT (pinned: order, labels, hrefs, rename, missing package, the loading window, the empty case, the cards beside the row). UnifiedSidebar filters package entries out and its Recent group is absent when nothing else is listed: RIGHT (pinned), it loads no package list and GET /api/v1/packages is a Studio read. StudioHomePage filters them out with a type predicate and narrows its exhaustive RECENT_ICONS Record to ListedRecentType (the TS2741 the dev measured): RIGHT (pinned), its own list holds writable bases only. HomePage already allow-lists object, dashboard, page and record at its recentApps filter (line 314), and CommandPalette keeps type === 'record' (line 162): unchanged and correct. RecentApps and HomeRail use partial icon and tone maps with fallbacks, so a package entry handed to them would render with its id as label; no caller in this tree hands them one, and the README snippet now teaches the filter. Acceptable as the dev noted; carrier none.
  7. i18n: home.recentApps.itemType.package in all ten packs. RIGHT and REQUIRED: the key gate checks the family against the union as an exact set, so the member cannot land without the row. No surface renders this row today (Home filters packages out before recentItemTypeLabel runs), so the row is gate-required rather than user-visible; it is not dead under the gate's own definition.
  8. Docs (a prose face, read since they are in the diff): packages/app-shell/README.md and content/docs/guide/user-state-persistence.md each gain one sentence naming the kind and useRecentItemLabel({ packages }); the README RecentList snippet filters type !== 'package' with a one-line comment. RIGHT: every claim in both sentences matches the code at this head; Doc Snippet Type Check and README Export Check are green.
  9. Not changed, verified: no export added or removed (packages/app-shell/src/index.ts is not in the diff, and at this head it exports the same Recent names as before); StudioLanding's header untouched (Q1's, and it waited for objectui#11912, which is now on main through this very merge without touching anything here); no packages/spec change; no narrowing of any published accept set anywhere in the diff.

② Semver level

Clause-②: yes (widening)

  • The PR body's declaration reads Clause-②: yes, and the changeset body says Clause-②: yes (widening). Both match the diff: items 1 to 4 above widen a published type and a published function signature of @object-ui/app-shell; nothing narrows a published accept set (the sidebar and metadata-admin filters narrow what is DRAWN, not what is ACCEPTED).
  • .changeset/11863-studio-recent-packages.md declares @object-ui/app-shell: minor (the ruled level, 6054672824 and claim 6063712393), @object-ui/i18n: patch, @object-ui/console: patch. All three are in the fixed group of .changeset/config.json at this head, so the group releases at minor; no major is declared (Changeset Bump Policy green); presence is declared for every changed released source file (Changeset Declaration green); Changeset Fixed Group Check green. Level MATCHES what the diff publishes.
  • The changeset body's own claims, re-read against the diff at this head: the route records a pillar visit; identity and no display text; the landing labels from its list; pillar switches write nothing; ~org records nothing; the refused principal records nothing; sidebar and metadata-admin home leave package entries out; Home and the palette already list other kinds only; useRecentItemLabel takes an optional { packages }; no export added or removed; one key in ten packs. Each is true of the code.
  • Note, not a finding: a new @object-ui/i18n key is arguably minor on that package alone; pending changesets spell key additions both ways, and under the fixed group the released version is the same either way.
  • Changeset Claim Re-read named .changeset/11806-login-server-unreachable.md for the README. The two README hunks of this diff sit in the recent-items section only and touch neither the sign-up table nor the useSignUpOffer recipe, so that claim still holds. Agreed with the dev's reading in 6065785121.

③ Boundary flags

Each dev deviation and each open_questions entry from reports 6065684736 (round 1), 6065785121 (patch round 1) and 6066344660 (patch round 2, this head), answered or escalated.

  • D1 StudioHomePage.tsx and useRecentItemLabel.ts not named in the claim's parenthetical. ANSWERED, inside scope: the claim's bullet "Every consumer that switches on RecentItemType" covers StudioHomePage (its exhaustive Record does not compile without the member), and useRecentItemLabel is named in the claim's first bullet as the label seam.
  • D2 docs not edited in round 1. RESOLVED by 4f42ef80, on this head; judged in ① item 8.
  • D3 H1 before-reading taken as a reverse verification after the implementation commit. ANSWERED: same mechanism through the real route; the base-source run went red on every new-behaviour case and stayed green on the four negative controls, which is the direction the hypothesis needed.
  • D4 changeset carries i18n and console at patch beside app-shell minor. ANSWERED in ②: consistent with the fixed group; nothing to change.
  • D5 commit trailers follow the model-free pair. ANSWERED: the four authored branch commits end with the pair objectui AGENTS.md prescribes.
  • D6 stray /suites.pid in the dev container. ESCALATED to the seat as housekeeping: outside the repository, absent from the diff and the file list, no effect on this PR.
  • D7 vitest guard refused mixing a directory with file paths. Informational; no action.
  • D8 the claim-surface amendment for the two docs files arrived in the coordinator's message and is recorded on no card comment. STILL OPEN, ESCALATED again: the newest Claim: comment on the card is still 6063712393 and no amending comment has followed it, so the claim's file surface and the PR's 25 files still disagree on two files. The seat should write the one-line amendment on the card. Not a FAIL reason: the two files are a prose face, AGENTS.md Add automated testing infrastructure and CI/CD workflows #2 asks for them, and their diff is judged right above.
  • D9 worktree re-created from the remote head for the docs round. Informational; the branch is one lineage.
  • D10 (round 2) the merge commit b4914d75 carries git's default message and no Claude-Session / Co-authored-by trailer pair, because the dispatch prescribed --no-edit. ANSWERED, no action: objectui AGENTS.md names git fetch origin && git merge origin/main as the one compliant way to sync a branch (never a rebase, never a force-push), says the merge commit's cost is that it does not survive the squash landing, and states that nothing in the repo enforces the trailer rule; the four authored commits carry the pair, and the squash keeps Co-authored-by from them.
  • D11 (round 2) worktree re-created at ../objectui-issue-11863-q3 and removed after the report. Informational.
  • Open question (round 1): extend the claim to the two docs files, A/B/C. ANSWERED A by the seat and implemented on 4f42ef80; patch rounds 1 and 2 list no open questions.
  • Out-of-scope note: RecentApps labels without a package list. ANSWERED in ① item 6; carrier none is the right disposition, since no caller in this tree reaches the case and the README now teaches the filter.
  • The round-2 report's CI line read "29 success, 3 skipped, 10 in progress; not waited on". At this reading every one of the 43 has completed: 40 success, 3 skipped, 0 failure.

No FAIL reason found. This record supersedes 6066168690 (head 4f42ef80) as the Tier S review of record; the landing act (ready, queue) stays the dispatching seat's.

Implemented-by: claude/issue-11863-studio-recent-packages
Reviewed-by: session_01DBZ9bntPZ7VKyQNtJeNsgw

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 18:51
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 18:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 054fd84 Oct 8, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11863-studio-recent-packages branch October 8, 2026 19:10
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 9, 2026
…der's new studio variant (objectui#11863, Q1) (objectstack-ai#12004)

Part of objectstack-ai#11863 (Q1)

Clause-②: yes. This widens a published type of `@object-ui/app-shell`:
the exported `AppHeader` component's `variant` prop accepts a fourth
member, `'studio'`, beside `'app'`, `'home'` and `'orgs'`. The union
(`AppHeaderVariant`) is not exported by name, since the package entry
exports `AppHeader` only, so a host meets it as the prop's type, and a
host that switches exhaustively over that type must handle `'studio'`. A
call with no `variant` resolves as before. No export is added or
removed. It also narrows `@object-ui/i18n`: the exported `en` pack and
the `TranslationKeys` type derived from it lose
`console.studio.backToHome`. The changeset is `minor` for
`@object-ui/app-shell` and `@object-ui/i18n`, and `patch` for
`@object-ui/console`.

This covers only Q1 of objectstack-ai#11863, per the retriage
ruling (Q1 → A): the `/studio` header. Q3 landed in objectstack-ai#11996. Q2, search
on `/studio`, follows in a later round, so objectstack-ai#11863 remains open.

## What changed

- `packages/app-shell/src/layout/AppHeader.tsx`: `AppHeaderVariant`
gains `'studio'`, a sibling of `'orgs'`. It draws the brand logo, then a
fixed Studio crumb read from `console.studio.title`, as `'orgs'` draws
`organizations.title`. Everything else is the header's own, unchanged:
the inbox, help and the account menu (profile, workspaces, theme,
language, notification switches, sign-out). The `app`, `home` and `orgs`
drawing is untouched; the only other edits are comments that list the
frames.
- `apps/console/src/components/StudioRoute.tsx`: `StudioLanding` mounts
`AppHeader` with `variant="studio"` in the header `HomeLayout` uses
(same classes), in place of its wordmark link. `StudioPackageBuilder`
and the recent-package record (Q3) are untouched.
- `packages/i18n/src/locales/*.ts`, all ten packs:
`console.studio.backToHome` (the wordmark's tooltip) retires with its
reader, and `console.studio.title` ("Studio") takes its place in the
same `console.studio` block. "Studio" is a product name every pack
already writes in Latin script inside its own Studio strings, so the
value is the same in every pack.
- `packages/app-shell/README.md`: the one place it lists the header's
surfaces ("Platform preview badge") now names `studio`.
- `.changeset/11863-studio-header.md`.

Why `console.studio.title` and not a new family: the `orgs` crumb reads
its own frame's `title` row (`organizations.title`). The Studio front
door's own rows already lived in `console.studio` (`backToHome` was its
only row), so the crumb is that block's `title`. No new key family.

## Hypotheses, measured

**H1 holds.** Browser drive, Chromium at 1440x900 and 390x900, the
console's built `dist` served by `vite preview` (base `054fd8441` on one
port, this branch on another), API mocked with `page.route` (signed-in
admin holding `studio.access`, one org, two packages). Before and after,
at `/studio`:

| | base `054fd8441` | this branch |
|---|---|---|
| visible interactive elements in the header (1440) | `a: Back to home
-> /home` | `a: ObjectOS -> /home`, `button: Open inbox`, `button: Help
& Documentation`, `button: AA` (avatar) |
| header text | `ObjectOS` | `PREVIEW / Studio AA` |
| header height | 40.5 px | 56 px (the `/home` header's) |
| at 390 px | `a: Back to home -> /home` | `a: ObjectOS -> /home`,
inbox, avatar (help is `md` and up, as on `/home`) |
| avatar menu | (none) | Ada Admin, Profile, My Workspaces, Create
workspace, Theme, Language, In-app alerts, Desktop notifications, Log
out |
| page errors | none | none |

CONTROL: `/home` on both trees draws the same three controls at 390 px
and the same menu. The provider tree: `/studio` (`StudioRoute`, then
`ProtectedRoute`, then `ConnectedShell`, inside `ConsoleShell`) is the
tree `/home` (`ProtectedRoute`, then `HomeLayout`) has, so `AppHeader`
mounts with no new provider. The drive is the runtime reading of that:
the real account menu, inbox and help rendered, with no page error.

**H2 holds.** The studio variant's brand is `AppHeader`'s logo link,
`to={homePath}` from `useHomePath()`, the line
`homeAffordancesFollowDeclaration-7256.test.ts` already pins as "top-bar
brand logo". Pinned three ways: the new
`AppHeader.studioVariant-11863.test.tsx` runs the real `useHomePath`
over metadata apps (no declaration: `/home`; `isDefault` on `helpdesk`:
`/apps/helpdesk`); `StudioRoute.test.tsx` "the front door's header brand
walks back to the same home the gate bounces to" drives the real route
tree with a declared landing (`/apps/cloud_control`); and the browser
drive above (`/home`).

**H3 holds.** `/studio` mounts no command palette, so under
objectui#11987's rule the header draws no search trigger there. Pinned
in `AppHeader.searchTriggerNeedsPalette-11912.test.tsx`, which now runs
the `studio` variant in its no-provider case: neither trigger testid and
no `aria-keyshortcuts` element, while the header itself drew. In the
browser: 0 trigger testids, 0 `aria-keyshortcuts` elements, and `Ctrl+K`
opens 0 palette overlays.

**H4 holds.** One new row, `console.studio.title`, in all ten packs;
`console.studio.backToHome` retired from all ten with its one reader.
`StudioRoute.landingI18n.test.tsx` now pins the crumb through the real
`studioRoutes` and the real `AppHeader` (with `t` answering `«key»`),
pins that no text or attribute on the landing asks for the retired key,
and keeps the Han sweep with its live control. `check:i18n-keys`,
`check:i18n-drift` and `check:i18n-dead-keys` are green (readings
below). The i18n suite's `untranslated-identity-4376` flagged the row in
zh/ja/ko/ru/ar as identical to en, so it is allowlisted there with its
reason (the product name).

**H5: every consumer of `AppHeaderVariant`, and what it does with
`studio`.**

| Consumer | With `studio` |
|---|---|
| `resolvedVariant` default (`appName ? 'app' : 'home'`) | unchanged |
| `isApp` (`=== 'app'`): sidebar trigger, app crumbs, presence,
connection dot, Design in Studio, the keyboard-shortcuts help entry, the
logo hidden below `sm` | false, so none of them; the logo shows at every
width, as on `home` / `orgs` |
| `=== 'home'`: the product wordmark text | not drawn |
| `=== 'orgs'`: the Workspaces crumb | not drawn |
| `=== 'studio'` (new): the Studio crumb | drawn |
| `AppHeader.searchTriggerNeedsPalette-11912.test.tsx`, `Record` over
the union (exhaustive) | gains `studio: '/studio'`; compiles
(`app-shell` `type-check`, whose test project lists the file) |
| `AppHeader.inboxVariant.test.tsx`, two "every variant" loops (not
exhaustive) | `studio` added to both, so their titles stay true; a
comment that spelled the union out now points at the type |
| Mount sites: `HomeLayout`, `AiChatPage` (`home`),
`OrganizationsLayout`, `OrganizationLayout` (`orgs`), `ConsoleLayout`
(`app`) | unchanged; `StudioLanding` is the one `studio` mount |

No `switch` over the variant exists in `app-shell` or `apps/console`.
`apps/console` has no comparison over it.

**H6: the budget.** `pnpm check:eager-closure`, local `vite build` of
the console under the shared verify lock, each tree on its own build:

- base `054fd8441`: 3,391,274 B gzipped (`3311.8 KB`, headroom `0.2 KB`,
336 of 2458 chunks);
- this branch: 3,391,194 B gzipped (`3311.7 KB`, headroom `0.3 KB`, 336
of 2458 chunks);
- ceiling `MAX_EAGER_CLOSURE_GZIP_BYTES` 3,391,484 B, untouched. Delta:
-80 B. The `index` chunk (the one whose built code carries the `studio`
variant prop) fell 51 B; the rest is hash-rename noise spread across
chunks.

The branch build ran at `19fb06ccb`; every commit after it touches test
files only (`git diff --stat 19fb06c HEAD` lists five test files),
none of which is in the bundle. The local base reading is 9 B above the
3,391,265 B the seat quoted from the queue-built tree.

## Tests

New: `AppHeader.studioVariant-11863.test.tsx` (the frame, brand first
then the crumb, and neither sibling variant's drawing; CONTROL: `home`
and `orgs` draw no Studio crumb; the brand's target with and without a
declared landing; the right-hand cluster's inbox, help and account-menu
keys). Changed: `StudioRoute.landingI18n.test.tsx` (rewritten to pin the
crumb), `AppHeader.searchTriggerNeedsPalette-11912.test.tsx` and
`AppHeader.inboxVariant.test.tsx` (the `studio` member), and
`untranslated-identity-4376.test.ts` (one allowlist row).
`StudioRoute.test.tsx`, `StudioRoute.lazyBuilder-11798.test.tsx` and
`StudioRoute.recentPackage-11863.test.tsx` render the landing, so they
now mount the real `AppHeader`, whose inbox, approvals and AI-catalogue
reads reached a real socket (the network-escape guard turned red on the
first run); each stubs `fetch` with an empty answer, and the entry
gate's read stays on the mocked `createAuthenticatedFetch`. Their
wordmark wording now names the header brand. No assertion in those three
changed.

Reverse verification, from the committed `555028609`, run under the
verify lock:

- Leg A (behaviour): `AppHeader.tsx` and `StudioRoute.tsx` swapped to
their base content (disk blobs `0de4209e7fc1` and `e657a9f63280`, equal
to the base blobs; anchor counts `console.studio.title` 1 to 0,
`variant="studio"` 1 to 0, `console.studio.backToHome` 0 to 1). Four
files: `Tests 5 failed | 29 passed (34)`. Red: the new test's "draws the
brand, then the Studio crumb"; the landing test's crumb, retired-key and
brand-link cases, and its Han sweep (it waits for the crumb first).
Green: the 22 `StudioRoute.test.tsx` and lazy-builder cases, the two
Han-instrument controls, and the new test's two variant CONTROLs,
brand-target and cluster cases, which hold on any off-app variant. A
trap restored both from `HEAD`: blobs `c9bf2c535f50` and `9e2098ca83a9`
equal to HEAD's, `git diff HEAD` empty.
- Leg B (types): `node scripts/ablation-replace.mjs` (objectstack) on
`StudioRoute.tsx`, `variant="studio"` to `variant="studioX"` (anchor 1
to 0, blob `9e2098ca83a9` to `a4295167badf`), then the console's `tsc
--noEmit` against the rebuilt `app-shell` dist: exit 2, `error TS2820:
Type '"studioX"' is not assignable to type 'AppHeaderVariant |
undefined'. Did you mean '"studio"'?`. The suggestion names the rebuilt
member, so the console read the rebuilt `.d.ts`. Restored: blob equal to
HEAD, `git diff HEAD` empty.

Union, at `c48ef5bf8` (`git rev-parse --short HEAD`, the last commit),
from the worktree root under the verify lock:

- every test that imports or names `AppHeader` or `StudioRoute`, or
names a `console.studio.` key (39 files, from `git grep -l`; the new
test is one of them): `Test Files 39 passed (39)` · `Tests 330 passed
(330)`;
- `packages/i18n/`: `Test Files 82 passed (82)` · `Tests 1316 passed |
13 skipped (1329)`;
- `scripts/__tests__/`: `Test Files 179 passed | 2 skipped (181)` ·
`Tests 5452 passed | 2 skipped (5454)`;
- type-check, after building the console's dependency closure (`turbo
run build --filter='@object-ui/console^...'`, `Tasks: 34 successful, 34
total`): `pnpm --filter @object-ui/i18n --filter @object-ui/app-shell
--filter @object-ui/console run type-check`, exit 0, each script name
echoed (`packages/i18n type-check$`, `packages/app-shell type-check$`,
`apps/console type-check$`). `tsc --listFilesOnly` shows the app-shell
test project and the console project list every touched test file.

## Gates

At `c48ef5bf8`, all exit 0:

- `pnpm check:i18n-keys`: "Every in-scope call-site key resolves against
the en pack (3307 keys)…";
- `pnpm check:i18n-drift`: "No en value changed in this range." (1 key
added, 1 removed);
- `pnpm check:i18n-dead-keys` (report-only): the same 3307 pack keys and
340 candidates as on base `054fd8441`; no `console.studio` key among
them;
- `pnpm check:readme-exports`: OK, 579 self-imports judged, 579 real, 0
fabricated, 0 unbuilt (after the doc-snippet build closure);
- `pnpm check:skill-examples`: "Every marked skill example holds up
against the built types.";
- `pnpm check:doc-snippets`: "784 of 784 block(s) judged, 0 failed.";
- `node scripts/check-changeset-presence.mjs`: "20 source file(s) of 3
released package(s) changed, and this change declares 1 changeset(s)";
`check-changeset-no-major`: no `major`;
- `pnpm check:changeset-claims` (report-only): one pending changeset
names a file this edits, `.changeset/11806-login-server-unreachable.md`
naming `packages/app-shell/README.md`. Its paragraph is about the
sign-up table, which this does not touch, so it is still true;
- `pnpm check:pending-changeset-literals`: no test source names a
pending changeset;
- `pnpm check:new-line-citations`: "VERDICT
new-cross-file-line-citations: 0 new citation(s)";
- `pnpm check:control-bytes`: OK.

Lint, narrowed: `eslint --no-inline-config --format json` over the 20
changed `.ts`/`.tsx` files (the count is from its JSON output): 0
errors. Each of the 19 files that exist on base carries the same error
and warning counts as its base content linted the same way; the new test
file has 0 errors and 13 warnings, of the two kinds its sibling `11912`
test carries. The config applies to `**/*.{ts,tsx}` and is not
type-aware (`eslint.config.js` sets no `parserOptions.project` or
`projectService`), so this diff cannot move a verdict on an untouched
file. The repo-wide `pnpm lint` is CI's.

Bundle Analysis on this PR: NOT MEASURED here, it had not posted when
this was written.

## Acceptance notes

-
`packages/app-shell/src/__tests__/homeRecoveryRedirectsFollowDeclaration-7373.test.ts`
labels its `StudioRoute.tsx` site "the /studio entry gate + the front
door wordmark", and its header lists `StudioRoute.test.tsx` the same
way. The front door's link is now `AppHeader`'s logo, which
`homeAffordancesFollowDeclaration-7256.test.ts` already pins; the site's
regex still matches the entry gate. The label is stale prose only, and
the file is outside this claim, so it is not edited. Carrier: Q2 of
objectstack-ai#11863 or whoever next touches that file.
- No guide under `content/docs/guide/` names the header's variants (`git
grep AppHeader` there: two mentions, neither a variant list), so no
guide changed.
- The header's Help menu shows "This app's docs" from the navigation
context's `currentAppName`, which no off-app frame clears; `/studio`
inherits that from `home` / `orgs` unchanged. Read-only inference,
noted, not filed.

This was implemented by the dev seat for objectui#11863 (Q1), session
`https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw)_

---------

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants