Skip to content

feat(console): make the orphaned console pages targetable by navigation, retire the Developer Hub (objectui#10520) - #10576

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-10520-orphaned-console-pages
Sep 25, 2026
Merged

os-litant merged 4 commits into
mainfrom
claude/issue-10520-orphaned-console-pages

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #10520
Clause-②: yes

What this does

Retiring the System Hub card wall (objectui#3743, PR objectui#10507) left three console pages with no in-app link. A fourth page, IntegrationsPage, was linked only from one of them, the Developer Hub. Framework navigation reaches a console page only through a type: 'component' item whose componentRef is a component-registry key, never a console path. So each page is either kept and made targetable, or retired when navigation already covers everything it offers.

page verdict registry key standalone route
AuditLogPage KEEP audit:log (new) system/audit-log stays
AiPendingActionsPage KEEP ai:approvals (new) system/ai-approvals stays
IntegrationsPage KEEP developer:integrations (new) developer/integrations stays
DeveloperHubPage RETIRE none developer removed
  • registerSystemComponents.tsx (new, wired by one side-effect import in main.tsx) registers audit:log and ai:approvals in the shape registerApprovalsComponents.tsx uses: a lazy import, a Suspense fallback, ref, label, source: '@object-ui/console'.
  • registerDeveloperComponents.tsx gains developer:integrations beside the other three developer:* keys. Its docblock said the standalone routes "remain reachable from the Console-shell Developer Hub"; that sentence is replaced.
  • AppContent.tsx loses the developer route and the DeveloperHubPage lazy import. DeveloperHubPage.tsx is deleted; it had no tests.
  • The docblocks of AuditLogPage and AiPendingActionsPage described the retired hub card and a Studio mount that does not exist in this repository; both now name the route and the key.
  • packages/app-shell/** is untouched. registerAppComponent is used as it is.

Measurements, per page (objectui origin/main at the branch base, objectstack origin/main)

A page counts as a DUPLICATE only when a navigation-reachable surface covers every capability it offers.

M1 AuditLogPage vs the sys_audit_log object view: KEEP. The object view is Setup's nav_audit_logs, contributed by plugin-audit into group_diagnostics. Capability by capability:

  • Filters by action, object, actor and date range: the object's list views carry the generic list filter bar (showFilters defaults on in ObjectView), and recent pages at 50. I did not measure each of the four filters one by one; the verdict does not rest on them.
  • Detail drawer with the before and after JSON of a change: no equivalent. old_value, new_value and metadata are Field.textarea columns that the writer fills with compact JSON.stringify output. On the record page they render through the textarea widget's readonly branch, which prints the raw string with whitespace kept. The page runs JSON.stringify(JSON.parse(...), null, 2) and shows Before and After side by side.

So Setup would carry two audit entries, the object and the page. Which one Setup shows is for the objectstack card.

M2 AiPendingActionsPage: KEEP. The seat expected this. The AiPendingActionsInbox docblock says it also renders in Studio's assistant builder. In this repository its only mount is this page: a git grep of AiPendingActionsInbox over packages and apps, tests excluded, finds the export and this page and nothing else. Studio's navigation has no pending-actions entry (its AI group is Agents, Tools and Skills). What else shows pending actions is scoped to one context: AiChatPage shows the approval card of its own conversation, and BuildDebugDrawer shows the pending actions of one build. So this page is the only surface that lists the whole queue.

M3 DeveloperHubPage: RETIRE. It was a wall of four cards: integrations, api-console, flow-runs and public-forms. After this PR all four are registered developer:* keys, and the new pin asserts it. Studio's group_developer already names developer:api-console, developer:flow-runs and developer:public-forms. No destination was left without a key, so the needs_decision fork did not arise.

What a /apps/APP/developer bookmark shows now:

  • With an active app, the segment falls through to app-shell's generic :objectName route. It lands in place, with no redirect, inside the app's shell. The real ObjectView answers a name that is not an object with its objectNotFound empty state. In en that reads "Object Not Found" and 'The object "developer" does not exist in the current configuration.' I measured that branch once with a throwaway render of the real ObjectView, which printed the three console.objectView.objectNotFound* keys; the throwaway file is not in this PR. It is not a blank screen, so I added no redirect. There is also nothing to redirect to. No in-app link produces this URL, and the hub's four cards have no single canonical destination. objectui#3655 likewise declined to bind a bookmark to a surface nobody chose. (The retired static route used to shadow any object named developer; with it gone, /apps/APP/developer means what /apps/APP/NAME means everywhere else.)
  • With no apps configured, nothing changes. isSystemRoute keys on a system segment, so /developer never reached the host fragment on that branch, and the hub route was unreachable there before this PR.
  • Both are pinned in AppContent.systemHubRoutes.test.tsx, against the real DefaultAppContent and the real systemRoutes.

M4 IntegrationsPage: KEEP. It shows the environment's base URL (copyable), the generated CRUD endpoints of each business object, an auth explainer, a cURL sample that sends x-api-key, and AgentConnectSection.

  • The API Console (developer:api-console) discovers endpoints and sends session-authenticated requests. It shows only the relative request path: no base URL, no cURL, no x-api-key.
  • objectstack's mcp:connect-agent page (Setup and Account) covers the MCP-connect half: the MCP URL, key minting and the SKILL.md download.
  • Nothing in Studio covers the base URL or the cURL sample, so the page is kept.

Namespaces. Before this PR, no registry key or namespace named AuditLogPage or AiPendingActionsPage. I grepped app-shell builtinComponents.tsx, plugin-chatbot and the console; the only registerAppComponent keys are metadata:*, developer:*, automation:packaged, studio:builder, account:profile_card and approvals:inbox. Each new key is named for the capability that owns the data, the way approvals:inbox is, not for the console path:

  • audit is plugin-audit, which owns sys_audit_log and registers the audit kernel service.
  • ai is the kernel service (CoreServiceName ai) that serves /api/v1/ai/pending-actions.

ai:approvals sits in the app-component registry, which is separate from the SDUI block registry that holds the protocol's ai:* block types (ai:chat_window, ai:suggestion). The two do not collide.

Console docs. content/docs/**, apps/console/README.md and apps/console/docs/** name none of the four pages or routes. ROADMAP.md names /system/audit-log as a standalone page, which stays true.

Tests and gates (all at d8c18e1a3, the final commit)

  • New pin file orphanedPageComponentRefs-10520.test.tsx. For each of the three keys it asserts that:

    1. the key is registered by the module main.tsx imports;
    2. the key addresses component/NS/NAME, built from the ref through componentRefToUrlSegments;
    3. that URL, rendered by app-shell's real DefaultAppContent, reaches the page through its component/:ns/:name/* route, which serves ComponentNavView;
    4. the standalone route, from the real systemRoutes fragment, still renders the page.

    It also asserts that all four of the retired hub's destinations are registered keys. The pages are stubbed at the exact specifiers the register modules and AppContent lazy-import.

  • AppContent.systemHubRoutes.test.tsx gains a the retired Developer Hub URL (objectui#10520) describe. The with-app landing is the retirement pin. The zero-app measurement shows the branch is unchanged. A sub-page check shows developer/integrations still resolves.

  • Why not ComponentNavView directly: its first draft imported ComponentNavView by source path, and the console type-check then failed with TS6133 inside that app-shell file, whose unused React import breaks the console's noUnusedLocals. Routing through the exported DefaultAppContent exercises the same lazy ComponentNavView and needs no app-shell edit.

  • pnpm --filter @object-ui/console test: 117 files and 1288 tests passed, which equals the 117 tracked console test files.

  • The two focused files together: 35 passed.

  • The other suites that name these pages: AppContent.pseudoRouteSegments.test.tsx (app-shell) and dollar-dialect-alias-census.test.ts (scripts), 73 passed.

  • pnpm --filter @object-ui/console type-check: exit 0, after building the dependency closure (turbo run build --filter=@object-ui/console^..., 34 tasks, exit 0). tsc --listFiles includes both test files, so type-check covers them.

  • pnpm --filter @object-ui/console build: exit 0. The post-build gates all passed:

    • check-eager-closure-budget: "Console eager closure is 3046.6 KB gzipped across 329 of 2308 chunks (budget: 3104.5 KB, headroom: 57.8 KB)";
    • check-eager-locale-catalogues, check:sdui-registration-pins and check:docs-route-closure: exit 0.
  • Changeset and source gates, all exit 0:

    • check-changeset-presence ("9 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)");
    • check-changeset-no-major, check-changeset-fixed, check-changeset-overwrite;
    • check:changeset-claims ("No pending changeset names a file this change touches");
    • check:pending-changeset-literals;
    • check:new-line-citations ("0 new citation(s)");
    • check:control-bytes, check-vi-mock-specifiers, check-vi-mock-inherit, check-test-path-roots, check-vi-mock-override-shape;
    • check:unreferenced-sources, check:side-effects-array, check:phantom-deps, check:self-import, check:i18n-keys.
  • check-governed-queue-guard --test on the 10 changed paths: NOT GOVERNED.

  • Lint is a proven narrowing, not the repo-wide pnpm lint:

    1. the population is the 8 changed .ts/.tsx files, all inside eslint.config.js's scope;
    2. eslint --no-inline-config --format json reports 8 files, 0 errors and 18 warnings, the same react-refresh and no-explicit-any kinds that registerApprovalsComponents.tsx already carries;
    3. the config enables no type-aware linting (no parserOptions.project or projectService), and no custom object-ui/* rule reads another file, so this diff cannot move a verdict on an untouched file.

    Repo-wide lint is left to CI.

Reverse proofs (from the committed state, through ablation-replace.mjs: the anchor must hit, and the restore is proven against the HEAD blob)

mutation red green
registerAppComponent call for audit:log turned into a bare object 2: audit:log registered, component URL 11, including audit:log's standalone route
the same for ai:approvals 2: ai:approvals registered, component URL 11, including its standalone route
the same for developer:integrations 3: registered, component URL, all-four-destinations premise 10, including its standalone route
developer route plus DeveloperHubPage.tsx put back from the base commit 1: the retirement pin, because the hub renders and the object route never does 21

The component-URL failures show app-shell's own "Component not registered" state in the DOM dump. After every leg, the tool restored blob == HEAD with an empty git diff HEAD, and the tree was clean.

Acceptance notes

For the objectstack navigation card (the seat files it, Blocked-by: objectstack-ai/objectui#10520):

  • audit:log: Setup group_diagnostics, beside nav_audit_logs. The natural contributor is plugin-audit's own navigationContributions, the ADR-0029 K2 owner, which adds no gate: the entry lives and dies with the plugin, as nav_audit_logs does. If it lands in platform-objects' setup-nav.contributions.ts instead, gate it requiresService: 'audit'. Setup then carries two audit entries.
  • ai:approvals: Setup group_approvals, which already gates on manage_platform_settings. Gate: requiresService: 'ai', the kernel service discovery reports as services.ai. It is absent on a Community Edition boot, where objectstack's QA checklist row K2 records /api/v1/ai/pending-actions answering 501 behind the old card. That row names the now-deleted SystemHubPage. The gate closes its "ungated entry" half; its "error-blind inbox" half is in plugin-chatbot and is untouched here.
  • developer:integrations: Studio group_developer, beside nav_api_console, nav_flow_runs and nav_public_forms. The neighbours carry no gate beyond Studio's app-level studio.access.

Other notes:

  • .claude/skills/objectui-contributor/guides/console-development.md still lists DeveloperHubPage in its pages/developer/ tree, beside the SystemHubPage line objectui#10507 already made stale. It is on the governed surface, so it is ⛔ not edited in this code PR. Carrier: the seat, as a separate Tier S doc PR.
  • AiPendingActionsInbox's docblock still names a system/ai/pending-actions console path and a Studio mount. Both are stale comments in plugin-chatbot, outside this surface. Carrier: none.
  • Out-of-scope finding, handed to the seat, not filed: ToolPreview's "Open in API Console" link is /developer/api-console?path=..., with no /apps/APP prefix. The console's root routes declare no /developer path, and their * catch-all redirects to /. ApiConsolePage also reads no search params, so the path preset would be dropped even at the right URL. I measured this by reading both route tables and the page, not with a running probe.

Generated by Claude Code

…and retire the Developer Hub

Retiring the System Hub card wall left three console pages with no in-app
link, and a fourth reachable only through one of them. Framework
navigation reaches a console page only through a registry key, so:

- AuditLogPage is kept and registered as `audit:log`: the sys_audit_log
  object view has no equivalent of its before/after JSON drawer.
- AiPendingActionsPage is kept and registered as `ai:approvals`: nothing
  else lists the whole AI pending-action queue.
- IntegrationsPage is kept and registered as `developer:integrations`:
  no Studio surface shows the base URL or the x-api-key cURL sample.
- DeveloperHubPage is retired: all four of its cards now point at
  `developer:*` registry keys. Its route and page file go; the sub-page
  routes stay for bookmarks.

The standalone routes of the kept pages stay; each key is additive.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…oper Hub URL

- `audit:log` and `ai:approvals` resolve through app-shell's real
  ComponentNavView to their pages, and the standalone system routes keep
  rendering them.
- `developer:integrations` does the same, and all four of the retired
  hub's destinations are registered `developer:*` keys.
- A `/apps/:app/developer` bookmark now falls through to the generic
  object route in place (never a blank screen); the sub-page routes still
  resolve.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
…r Hub retirement

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BP8CMtACxTdLjqR6rhd33C
Importing app-shell's ComponentNavView by source path put that file into
the console's tsc program, under compiler options it is not written
against, and type-check failed on it. The three keys are now driven
through app-shell's exported DefaultAppContent, whose
`component/:ns/:name/*` route is what serves ComponentNavView in the
shipped console. The two per-module pin files merge into one.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3047.2 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-B4Xyr6NB.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.68KB 6.20KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
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.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 545.30KB 130.42KB
core (index.js) 9.22KB 3.71KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 258.75KB 65.60KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
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.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.40KB 14.61KB
plugin-charts (index.js) 74.94KB 20.89KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.50KB 35.37KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.59KB 61.50KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 147.92KB 37.83KB
plugin-gantt (index.js) 169.62KB 41.91KB
plugin-grid (index.js) 215.37KB 58.92KB
plugin-kanban (index.js) 48.26KB 15.04KB
plugin-list (index.js) 114.43KB 28.26KB
plugin-map (index.js) 22.42KB 7.38KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.64KB 8.94KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.31KB 21.78KB
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.66KB 3.50KB
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) 114.58KB 37.60KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
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: d8c18e1a35ffd7b06216889df76da8357ab19a42

Rendered by an isolated review subagent spawned by the domain:ui#4 seat; its served tier was checked against its transcript stamps (99 of 99 model stamps at the review tier). Adopted by this seat.

Read: the card body of objectui#10520, triage 5829805557 with its execution notes, claim 5830043716, amendment 5830060321, the os-dev-report 5830598100, the PR body, all four commits, registerApprovalsComponents.tsx and its pin, and on objectstack origin/main (a08e059c6) account.app.ts, studio.app.ts, setup.app.ts, setup-nav.contributions.ts, plugins/plugin-audit/src/audit-plugin.ts, plugins/plugin-audit/src/objects/sys-audit-log.object.ts, audit-writers.ts, spec/src/system/core-services.zod.ts, mcp/src/connect-ui.ts and docs/qa/platform-checklist/FOLLOW-UPS.md. The PR's true merge-base is d22b37bd8 (the API's base.sha is stale); every diff below is against that base and is the 10-file diff GitHub reports.

① Derived judgments

  • (a) KEEP AuditLogPage as audit:log: right. On objectstack main, old_value, new_value and metadata are Field.textarea columns of sys_audit_log, and the writer serialises them with a compact JSON.stringify(v). objectui's TextAreaField read-only branch renders the value as a whitespace-pre-wrap div, so the object's record page prints the compact string verbatim. The record page's per-record History tab is the only diff-aware surface, and it is explicitly disabled when the viewed object is sys_audit_log itself and filtered to one record_id + object_name; it does not cover a cross-object audit browser. The page's drawer runs JSON.stringify(JSON.parse(s), null, 2) for Before, After and metadata. So the claim is true and the verdict rests on a real gap. On filters: the object's six list views (recent at pageSize: 50, the others 50 or 100) carry no authored filter widgets; ObjectView sets showFilters: activeView?.showFilters !== false, so the generic filter bar is on. The dev did not measure the four filters one by one and says so; the KEEP does not rest on them.
  • (a) KEEP AiPendingActionsPage as ai:approvals: right. A git grep of AiPendingActionsInbox over packages and apps at head, tests excluded, finds the component, its barrel export, its two helper files, one i18n comment and this page. No Studio mount exists in objectui. AiChatPage reads pendingActionId per tool result of its own conversation; BuildDebugDrawer renders report.pendingActions of one build. Studio's group_ai is nav_agents, nav_tools, nav_skills. This page is the only whole-queue surface.
  • (a) KEEP IntegrationsPage as developer:integrations: right. The page renders a copyable window.location.origin + /api/v1 base URL, per-object CRUD endpoints, an auth explainer, a curl sample with -H "x-api-key: ..." and AgentConnectSection. ApiConsolePage uses client.baseUrl only to send requests and shows relative paths; it has no cURL and no x-api-key. The only other x-api-key in objectui outside this page tree is app-shell's ConnectAgentWidget (mcp:connect-agent, contributed by objectstack's @objectstack/mcp into Setup and Account, not Studio), which shows the MCP URL, minting and the SKILL.md download, not the REST base URL or a data cURL. The body's M4 reading is accurate.
  • (b) RETIRE DeveloperHubPage: right. At head the app-component registry holds developer:api-console, developer:flow-runs, developer:public-forms, developer:integrations (console) and developer:packages (app-shell); Studio's group_developer names the first three. Residual references at head: (1) .claude/skills/objectui-contributor/guides/console-development.md still lists DeveloperHubPage (and SystemHubPage), governed, disclosed; (2) app-shell's AppContent.pseudoRouteSegments.test.tsx keeps a stub Route path="developer" with testid developer-hub-page and an it.each row ['/apps/setup/developer', 'developer-hub-page'] in its "the /apps/setup family is unaffected" table. That fixture calls itself "the host's fragment, reduced to the entries this file depends on", and it now mirrors a route the host no longer declares. It still passes (it renders its own stub), so it is a stale mirror rather than a failure; packages/app-shell/** is outside the claim, so leaving it is correct, but the report lists the suite only as "73 passed" and does not disclose the stale row. Carrier for the seat. Nothing else in source, tests, i18n, scripts/, .github/, content/docs, apps/console/docs, README or ROADMAP names the hub or a bare developer route; the census test names AuditLogPage.tsx as a path fixture only.
  • (c) The bare /apps/:app/developer bookmark: not blank, as claimed. Console App.tsx has no /developer root route; /apps/:appName/* hands the tail to DefaultAppContent. With an active app, one segment after the app matches the dynamic :objectName route (the static developer/... sub-routes need two segments, and :objectName/:maybeRecordId needs two), whose element is the real ObjectView; its wrapper renders the console.objectView.objectNotFound* Empty state when objects.find misses ("Object Not Found" / 'The object "developer" does not exist in the current configuration.'). With zero apps, isSystemRoute keys on a system segment, so the guard returns the "No Apps Configured" state before any route table, exactly as it did before (the hub route sat in extraRoutesNoApp but was unreachable there). Both landings are pinned against the real DefaultAppContent and real systemRoutes. The shadowing argument is not sound as stated: a static developer route already shadowed any object named developer (a static segment outranks :objectName), so a redirect would only preserve the shadow that existed. The conclusion still holds for other reasons: zero in-app producers (unlike /system, which objectui#3743 redirected because app-shell senders still target it), no single canonical destination among four cards (the repo's own precedent at objectui#3655 refuses to bind bookmarks to a surface nobody chose), and a labelled not-found state rather than a spinner or blank. The seat should not require a redirect; it may ask that the reasoning be stated as "no producer and no canonical target" rather than the shadowing claim.
  • (d) Registrations: match the template. Both registerSystemComponents.tsx entries and the new developer:integrations entry carry a lazy import at the page's exact specifier, a Suspense fallback, ref, label, source: '@object-ui/console'. main.tsx gains exactly one comment line, one import './registerSystemComponents'; and one blank line. The system/audit-log, system/ai-approvals and developer/integrations Route lines are byte-identical to the base. URL shapes: componentRefToUrlSegments splits on : and ComponentNavView rebuilds ${ns}:${name} from the component/:ns/:name/* route, so audit:log is component/audit/log and ai:approvals is component/ai/approvals, as both docblocks say and the pin asserts.
  • (e) Namespaces: no collision path. ComponentNavView resolves a componentRef solely through getAppComponent, a module-level Map in services/componentRegistry.ts; nothing in app-shell bridges the SDUI ComponentRegistry (which holds ai:suggestion, ai:chat_window, ai:input, ai:feedback as block types) into that lookup. The only other componentRef reads in app-shell are two equality checks in UnifiedSidebar and two preview pickers. No audit:* or ai:* key existed in the app-component registry before this PR. The naming justification is consistent: approvals:inbox is named for the approvals service, audit:log for plugin-audit, which registers the audit kernel service and owns sys_audit_log, and ai:approvals for CoreServiceName ai, which serves /api/v1/ai/pending-actions.
  • (f) Pins are real. orphanedPageComponentRefs-10520.test.tsx imports getAppComponent, componentRefToUrlSegments and DefaultAppContent from @object-ui/app-shell, the real systemRoutes from ../AppContent, and side-effect-imports the two register modules; only the three pages (at their lazy-import specifiers), the app-shell lazy pages, the shell chrome and the providers are stubbed, the same set AppContent.systemHubRoutes.test.tsx stubs. The negative assertion Component not registered is app-shell's own empty state, so a misspelled key fails. Case counts: 3 refs times 4 cases plus the all-four-destinations case is 13; the extended systemHubRoutes file is 9 + 6 + 4 + 3 is 22; together 35, matching the report. Reverse-proof shape is consistent: a broken audit:log or ai:approvals registration reds exactly its "registered" and "component URL" cases (2 of 13, standalone route stays green); a broken developer:integrations also reds the four-destinations case (3 of 13); restoring the hub route reds only the with-app retirement pin (1 of 22), since the zero-app branch never reaches the fragment and the sub-page route is unaffected. One nit, inherited from the template: the pin does not verify that main.tsx performs the import; the template's docblock discloses this limitation, the new file's does not.
  • (g) Docblocks: true at head, with two unqualified measurement claims. registerSystemComponents.tsx, AuditLogPage.tsx and AiPendingActionsPage.tsx qualify their comparisons as "a one-time reading, and nothing re-derives it". Two sentences state a measurement as permanent: registerDeveloperComponents.tsx "The framework's Studio app names the first three from its Developer navigation group" (true on objectstack main today; cross-repo and unqualified, though the base docblock made the same claim), and the AppContent.tsx comment "A /developer bookmark falls through to app-shell's generic :objectName route" (true only with an active app; the zero-app landing is the no-apps state, which the comment leaves to the test it cites). The test docblock's "ComponentNavView is not on app-shell's barrel" is true (index.ts does not export it), and the console tsconfig.json does set noUnusedLocals: true. Pre-existing and unchanged, so outside this check but worth carrying: AuditLogPage.tsx still says the field shape mirrors framework/packages/platform-objects/src/audit/sys-audit-log.object.ts; on objectstack main that file is packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts.
  • (h) Changeset: every sentence true, no line-address citations, frontmatter '@object-ui/console': minor. "Framework navigation can now reach" is a capability statement; the entries do not exist yet, and the changeset's own last sentence of the additive paragraph says they belong to the framework. The three per-key sentences match the pages. The Breaking paragraph's landings match the pins. Pending changesets at head that name this territory: 3743-retire-system-hub-card-wall.md (System Hub only; its "redirects are unchanged" sentence stays true), 8504-emptyvalue-remaining-carriers.md (names the Public Forms object column and the plugin-chatbot JsonBlock, both untouched) and 4850-misplaced-eslint-disable.md (PublicFormsPage.tsx, untouched). None is made false.
  • (i) Out-of-scope findings: all three confirmed. ToolPreview.tsx renders href={/developer/api-console?path=/api/v1/ai/tools/${encodeURIComponent(toolName)}/execute} with no /apps/APP prefix; console App.tsx declares no /developer root route and its path="*" element is RedirectWithSplash to="/"; ApiConsolePage.tsx calls neither useSearchParams nor useLocation (its URLSearchParams use parses saved history entries). console-development.md lists DeveloperHubPage in pages/developer/ and SystemHubPage.tsx in pages/system/. plugin-chatbot/src/AiPendingActionsInbox.tsx says "Designed to drop into both Console (system/ai/pending-actions) and Studio (assistant builder traces panel)": the console route is system/ai-approvals and no Studio mount exists.
  • (j) Navigation notes: verified against objectstack main. setup.app.ts declares the anchors group_approvals and group_diagnostics, each with requiredPermissions: ['manage_platform_settings']; plugin-audit's navigationContributions puts nav_audit_logs (type: 'object', objectName: 'sys_audit_log', no item gate) into group_diagnostics under ADR-0029 D7, and the plugin calls ctx.registerService('audit', ...), so requiresService: 'audit' is a real gate if the entry comes from setup-nav.contributions.ts. Precision note for the seat: "adds no gate" is true of the item, but the group_diagnostics anchor already gates on manage_platform_settings. ai is a CoreServiceName whose remedy text says no implementation ships in the open framework, so requiresService: 'ai' (spec: "Hide/disable this entry unless the named kernel service is registered") is the right gate and matches account.app.ts's requiresService: 'approvals' on nav_account_approvals. studio.app.ts group_developer holds nav_api_console, nav_flow_runs, nav_public_forms with no item gates; the app carries requiredPermissions: ['studio.access']. FOLLOW-UPS.md row K2 names SystemHubPage.tsx and the 501 poll, as the body says.

② Semver level

minor on @object-ui/console is right. objectui AGENTS.md: "推论:changeset 里不要声明 major —— fixed 组任一 major 都会把全组推上去、脱离 objectstack 的节奏 ... objectui 自身的破坏性变更也标 minor(在正文里写清 breaking 语义即可)". Three new registry keys widen the componentRef set the console resolves; the removed /apps/:app/developer route narrows it, so the level is minor and the narrowing must be stated as breaking in the body. The changeset does so in an explicit "Breaking:" paragraph naming the route and both landings. The PR body carries Clause-②: yes, the RETIRE row and the M3 measurement but does not itself use the word "breaking"; the claim's requirement is on the changeset body, which meets it. No major anywhere; check-changeset-no-major is among the green checks.

③ Boundary flags

  • Model identifiers: the title, the body and all four commit messages with their trailers were scanned; none found. Commit trailers are a co-author line and a session-URL line only.
  • File surface (10 files, diff against merge-base d22b37bd8, +451/-113): added .changeset/10520-orphaned-console-pages.md; modified apps/console/src/AppContent.tsx (one Route line and one lazy import removed, one comment block added inside systemRoutes); modified apps/console/src/__tests__/AppContent.systemHubRoutes.test.tsx; added apps/console/src/__tests__/orphanedPageComponentRefs-10520.test.tsx; modified apps/console/src/main.tsx (one import line plus its comment, the amendment); removed apps/console/src/pages/developer/DeveloperHubPage.tsx; modified apps/console/src/pages/system/AiPendingActionsPage.tsx and AuditLogPage.tsx (docblocks only); modified apps/console/src/registerDeveloperComponents.tsx; added apps/console/src/registerSystemComponents.tsx. All inside the claim plus amendment. IntegrationsPage.tsx and auditLogActions.ts are in the claim and untouched. packages/app-shell/** is unchanged (git diff --quiet confirms) and the PR touches nothing in objectstack. Nothing else.
  • Serial: 11 open PRs; the other 10, including the 1,732-file release PR chore: release packages #5400 (18 pages read), touch none of these paths, none of apps/console/src/pages/{system,developer}/, apps/console/src/register*.tsx, or app-shell's AppContent.tsx, componentRegistry.ts, ComponentNavView.tsx.
  • mergeable_state: behind (mergeable: true, draft: true). The branch is four commits on d22b37bd8; origin/main has moved on without touching this surface.
  • Check-runs on the head: 43 total, all completed on the first poll at 10:17Z: 40 success, 3 skipped, 0 failed, 0 in progress. The three skips are Test (coverage), Test (coverage shard ${{ matrix.shard }}/4) and dependabot, matrix and bot jobs that skip on a PR by design.
  • PR body: every factual sentence checked is true at head (route tables, registry keys, grep results, the 117 tracked console test files, the 8 lintable changed files, the reverse-proof case counts, the objectstack group ids, gates and service names, the QA row). Fixes #10520 is the only closing keyword. No line-address citations, no angle brackets, no placeholders.
  • Residuals for the seat, none a contract breach: the stale developer fixture row in app-shell's AppContent.pseudoRouteSegments.test.tsx (out of surface, undisclosed as stale); the pre-existing stale object-file path in AuditLogPage.tsx's docblock; the two unqualified measurement sentences in (g); the unsound shadowing sentence in (c), whose conclusion nonetheless stands.

Implemented-by: claude/issue-10520-orphaned-console-pages
Reviewed-by: session_01BP8CMtACxTdLjqR6rhd33C

VERDICT: PASS


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review September 25, 2026 10:25
@os-litant
os-litant added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 50e41f7 Sep 25, 2026
45 checks passed
@os-litant
os-litant deleted the claude/issue-10520-orphaned-console-pages branch September 25, 2026 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants