Skip to content

fix(app-shell): Installed Apps reads a package the runtime refused to load as not loaded (objectui#11645) - #11759

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11645-installed-not-loaded
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-11645-installed-not-loaded

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11645

Clause-②: yes

Installed Apps now reads an install-local entry the runtime refused to load at startup as Not loaded, names the reason in plain words, and keeps Uninstall on the row. The package's Details page no longer offers re-seed or purge for such an entry. A loaded entry renders as before, byte for byte (measured below).

This is the console half of objectstack-ai/objectstack#21822. The server half is objectstack-ai/objectstack#21833 (48297ad980), carried by @objectstack/* 17.7.0, which objectui resolves since c0862c1.

Written by the os-dev agent dispatched on claim comment 6029532484 (seat domain:ui#3, mode:subagent), session https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.

The server's wire shape, member by member

Read at objectstack 48297ad980: handleList and the NotLoadedMarker interface in packages/cloud-connection/src/marketplace-install-local-plugin.ts, and ProtocolIncompatibleDiagnostic in packages/metadata-core/src/protocol-handshake.ts.

Listing item member (server) Present when Console type (LocalInstallEntry in marketplaceApi.ts) What the console does with it
notLoaded.code the startup rehydrate refused this entry notLoaded?: LocalInstallNotLoaded, member code: string OS_PROTOCOL_INCOMPATIBLE selects marketplace.notLoaded.protocolIncompatible. Any other code selects marketplace.notLoaded.otherReason, which names the code.
notLoaded.requiredRange same member requiredRange: string interpolated into marketplace.notLoaded.protocolIncompatible
withSampleData loaded entries only (on a refused entry it is omitted, not false) withSampleData?: boolean, unchanged Details draws re-seed and purge only when notLoaded is absent
installedBy operator (manage_metadata) callers only installedBy?: string or null, was required unchanged rendering: the "by" line is drawn only when present
  • notLoaded is closed on the server: exactly code and requiredRange. LocalInstallNotLoaded declares exactly those two members.
  • code is typed string, not the server's 'OS_PROTOCOL_INCOMPATIBLE' literal. The server's own acceptance notes name other not-loaded states that are unmarked today. If a later server marks one, the console still reads the row as not loaded and names the code, instead of hiding the row the operator needs to uninstall. This is not a lenient alias: there is one key, notLoaded, read one way.
  • installedBy is now optional. The server omits it for a narrowed caller, and the narrowed-caller pin below needs a fixture without it. This is a bounded correction on the same interface: its only reader already guards with &&, so no reader changes.

Public surface, measured on the BUILT declarations

  • LocalInstallEntry and LocalInstallNotLoaded are not public. After pnpm --filter @object-ui/app-shell build, a walk of the relative-import closure of packages/app-shell/dist/index.d.ts reaches 168 declaration files. None of them contains LocalInstallEntry, LocalInstallNotLoaded, notLoaded or marketplaceApi. Positive controls: MarketplacePackagePage is reached (2 files), and the emitted dist/console/marketplace/marketplaceApi.d.ts does carry notLoaded. The exports map has only . and ./styles.css, so no deep import reaches the file.
  • The five language-pack keys are public. After pnpm --filter @object-ui/i18n build, dist/locales/en.js, dist/locales/en.d.ts and dist/locales/zh.js each carry them. So Clause-②: yes and the minor changeset stand.

New language-pack keys, in all ten packs

Key en value Placeholders Fed by
marketplace.notLoaded.badge Not loaded none presence of notLoaded
marketplace.notLoaded.protocolIncompatible This runtime did not load this package: it targets protocol {{requiredRange}}, which this runtime does not support. requiredRange notLoaded.requiredRange, when notLoaded.code is OS_PROTOCOL_INCOMPATIBLE
marketplace.notLoaded.otherReason This runtime did not load this package ({{code}}). code notLoaded.code, for any other code
marketplace.uninstall.confirmNotLoaded Uninstall {{manifestId}} v{{version}} from this runtime? (blank line) The cached manifest will be removed. This runtime did not load the package, so none of it is running. manifestId, version the row's manifestId and version
marketplace.uninstall.successNotLoaded Removed {{manifestId}}. It was not loaded, so no restart is needed. manifestId the row's manifestId
  • "targets protocol" is the server's own wording: the handshake message reads "package 'ID' targets protocol RANGE ... but this runtime is protocol VERSION".
  • No console phrase existed for the range. git grep for OS_PROTOCOL_INCOMPATIBLE, requiredRange, protocol and incompatib over packages/i18n/src/locales/en.ts and packages/app-shell/src found nothing relevant. Control: versionBadge is found in all ten packs.
  • The nine other packs are translated, not copied. The untranslated-identity pin refuses an English copy in zh, ja, ko, ru and ar, and it is green.
  • One decision for the reviewer: the two uninstall.*NotLoaded keys. The card asks for the row to keep Uninstall. The existing texts for that action say the package "will remain loaded in the running kernel until the next restart" (confirm) and "Restart the runtime to fully unload it from the running kernel" (result). Both contradict the row's own "Not loaded". If you read these as beyond the card, dropping them removes two keys and one ternary in each place.

What changed

  • InstalledListWidget.tsx: a destructive "Not loaded" badge beside the version badge, and one reason line under the meta line. Uninstall stays and is enabled. The confirm and the result text are chosen by notLoaded. A row without notLoaded adds no node.
  • MarketplacePackagePage.tsx: the local menu's re-seed and purge items are not drawn for a notLoaded entry. Uninstall stays, and so does the primary Reinstall: that is the compatible re-install the server documents as reachable. Measured before this change: a refused entry's menu offered "Add sample data", enabled (the missing withSampleData read as no sample data), which posts a re-seed into objects the runtime never registered. It also showed purge, disabled.
  • marketplaceApi.ts: types only.
  • Locale packs: five keys in each of the ten packs.
  • .changeset/11645-installed-not-loaded.md: minor for @object-ui/app-shell and @object-ui/i18n.

Tests

  • New InstalledListWidget.notLoaded-11645.test.tsx, 9 cases. marketplaceApi is not mocked. One stubbed fetch answers with the listing body the server landed, over a ledger that DELETE changes. The real I18nProvider renders in en and zh. Expected text is the pack's value, read from the pack and interpolated; no sentence is copied into the test. It pins:
    • the operator's refused entry: badge, reason naming ^16, Uninstall enabled (en, zh);
    • the narrowed caller's entry, with no installedBy: badge and reason (en, zh);
    • a loaded entry: no badge and no reason, Uninstall enabled (en, zh);
    • Uninstall on the refused row: the not-loaded confirm, DELETE for its manifest id, the not-loaded result, and the re-read listing without the row;
    • a loaded row keeps the loaded texts;
    • an unknown code still reads not loaded, names the code, and keeps Uninstall.
  • New MarketplacePackagePage.notLoadedMenu-11645.test.tsx, 6 cases, run in the catalog view (marketplace on) and in the offline local view (objectui#11627). They pin: the refused entry's menu holds Uninstall alone; Uninstall still issues DELETE; a loaded entry still gets re-seed and purge.
  • pnpm exec vitest run packages/app-shell/ at 79a84a7: Test Files 1054 passed | 1 skipped (1055), Tests 10317 passed | 9 skipped (10326). The one later commit, 053d441, edits one of the new test files only. Both new files re-ran at 053d441: 15/15.
  • pnpm exec vitest run packages/i18n/ at 79a84a7 (i18n has not changed since): Test Files 81 passed (81), Tests 1310 passed | 13 skipped (1323).
  • pnpm --filter @object-ui/i18n type-check and pnpm --filter @object-ui/app-shell type-check at 053d441: exit 0. The script name echoes as type-check. tsc -p tsconfig.test.json --listFiles lists both new test files (2 of 5063 lines). Control: the existing marketplaceDates.displayLocale-10331.test.tsx is listed too.

Reverse verification (one-off, not kept)

  • The fix was committed (053d441). Then the c0862c1 blobs of InstalledListWidget.tsx and MarketplacePackagePage.tsx were checked out, under a trap that restores HEAD.
  • Proof that the change landed on disk: git hash-object equalled the base blobs d2eaeb1d and e06fd029, and the notLoaded count went from 10 to 0 (widget) and from 2 to 0 (page).
  • Proof of the restore: the hashes equal the head blobs 0e1e48ae and 94d89725, git diff HEAD is empty on both paths, the index is clean, and the count is back to 10.
  • Pre-fix: 8 failed, 9 passed (17).
    • The 8 that fail are every not-loaded pin: refused entry (en, zh), narrowed caller (en, zh), the Uninstall flow, the unknown code, and the Details menu in both views.
    • The 9 that pass on both sides are the preservation pins: loaded row (en, zh), the loaded row's Uninstall texts, Details DELETE in both views, and Details' loaded menu in both views. The other two are the markup probe below.
    • With the fix: 17 passed.
  • A loaded row renders as before, byte for byte. A one-off probe rendered Installed Apps over two loaded entries (one with installedBy, one without, one with a catalog id distinct from its manifest id) and hashed the container's innerHTML. Base and head are identical: en 6659 bytes, sha256 fdfbc6d0…; zh 6461 bytes, sha256 250c012d…. A cmp of the two dumps also reports them identical.

Gates, at 053d441

Each line gives the exit code and the gate's own verdict.

  • pnpm check:control-bytes: 0, "check-control-bytes: OK (scanned 7750 tracked text file(s); skipped 85 binary)"
  • pnpm check:test-path-roots: 0, "check-test-path-roots: OK"
  • pnpm check:changeset-claims: 0, "No pending changeset names a file this change touches."
  • pnpm check:pending-changeset-literals: 0, "No test source names a pending changeset."
  • pnpm check:i18n-keys: 0, "Every in-scope call-site key resolves against the en pack (3291 keys)…"
  • pnpm check:i18n-drift: 0, "0 en value(s) changed (5 key(s) added, 0 removed …)"
  • pnpm check:i18n-dead-keys: 0 (report-only). None of the five new keys is in its candidate list.
  • pnpm check:i18n-designer-parity: 0, "Every en row has a zh row, and every shared row carries the same placeholders."
  • pnpm check:new-line-citations: 0, "0 new citation(s)"
  • pnpm check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape: 0, OK
  • pnpm check:phantom-deps: 0. pnpm check:self-import: 0
  • pnpm changeset:check: 0, "No changeset declares a major bump."
  • node scripts/check-changeset-presence.mjs: 0, "15 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"
  • node scripts/check-governed-queue-guard.mjs --test over the 16 paths: "NOT GOVERNED"
  • eslint, narrowed to the 15 touched .ts/.tsx files: exit 0, 0 errors, 49 warnings, 0 of them on a line this diff adds (mapped against the git diff -U0 hunks).
    • Population: isPathIgnored is false for the touched files, and the --format json output has 15 results.
    • Invariance: the resolved config has no parserOptions.project or projectService, so linting is not type-aware and this diff cannot change the verdict on an untouched file.
    • The repo-wide pnpm lint is left to CI.
  • NOT MEASURED: check:sdui-registration-pins. It exited 2 with PREREQUISITE NOT MET, because it needs a console build (apps/console/dist). This diff does not touch the ComponentRegistry.register call. Left to CI.
  • NOT MEASURED: check:eager-locale-catalogues, which also needs a console build. The diff adds keys inside existing packs and no static import. Left to CI.

Acceptance notes

These were observed here and not changed. They are outside this card's surface, and no issues were filed for them.

  • The Details header still shows the green "Installed · vX" badge for a not-loaded entry. The card's Details step covers actions only, so the not-loaded marker is drawn on Installed Apps alone.
  • Details' own "Uninstall from this runtime" still uses marketplace.uninstall.confirm and successInDetail, which say the app stays loaded until a restart. Details' Uninstall is not an action that needs a loaded package, so it is outside the card's Details step.
  • The catalog page (MarketplacePage) badges a local install "Installed vX" whether or not it is loaded.
  • Docs: no page in content/docs or the app-shell README describes the Installed Apps rows, so no doc page changes.

Generated by Claude Code

claude added 2 commits October 7, 2026 02:39
… load as not loaded (objectui#11645)

The install-local listing marks an entry the startup rehydrate refused with
`notLoaded: { code, requiredRange }` in place of `withSampleData`
(objectstack#21822, `@objectstack/*` 17.7.0). InstalledList drew every entry
as installed. The row now carries a "Not loaded" badge and the reason in
plain words, keeps Uninstall (whose confirm and result drop the
"stays loaded until restart" caveat), and Details no longer offers re-seed
or purge for such an entry. A loaded entry renders as before.

Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8
Co-authored-by: Claude <noreply@anthropic.com>
…ectui#11645)

The loaded-entry case no longer leans on the refused row's badge as its
control, so it holds on both sides of the fix; the refused-row cases are
what show the queries find the badge and the reason.

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

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 332 chunks) 3507.8 KB 3551.8 KB
Main entry chunk (gzip) 155.3 KB 350 KB
Entry file index-DkB6Hf-Q.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) 17.82KB 6.58KB
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) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.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) 578.94KB 139.21KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 235.41KB 65.46KB
fields (index.js) 266.88KB 67.46KB
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.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) 37.51KB 10.04KB
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.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 199.63KB 47.46KB
plugin-dashboard (index.js) 144.22KB 38.99KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.73KB 65.20KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 176.62KB 45.75KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 238.51KB 65.53KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.85KB 29.12KB
plugin-map (index.js) 25.60KB 8.62KB
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) 90.64KB 22.85KB
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.08KB 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: 053d44156e271ef37776fbdec53361992eaccc71
Local-runs: none

Inputs read: card #11645 (body and all four comments, 5991416133 / 6029233802 / 6029532484 / 6030476046), PR #11759 (body, 16-file list, net diff against main at the head above, merge-base c0862c1), and the 43 check-runs on the head: 40 success, 3 skipped by design (Test (coverage), its shard matrix, dependabot), 0 failure, 0 in progress. Head repo is the base repo (not a fork); 637 added / 21 removed lines; no governed path in the file list (.claude/**, skills/**, docs/adr/**, AGENTS.md, CLAUDE.md all untouched), so this review is owed by the claim's Clause-②: yes alone. No earlier ## Contract review record exists on the PR or the card.

① Derived judgments

Accept-set changes (what the console reads off the wire):

  1. LocalInstallEntry.notLoaded?: LocalInstallNotLoaded (marketplaceApi.ts) — new optional member of the install-local listing item. RIGHT: it is the marker the card names (code + the declared range), optional because a loaded entry omits it; both new tests drive it through an unmocked marketplaceApi over a stubbed fetch, so the pin is against the listing body, not a prop.
  2. LocalInstallNotLoaded.code: string, not the one literal the server sends today. RIGHT, and not a lenient alias: an unknown code is rendered loudly (the "Not loaded" badge plus the code itself through marketplace.notLoaded.otherReason), never swallowed and never a reason to drop the row the operator must still uninstall; there is one key, read one way, no ?? and no shim. Pinned by the "a refusal code the console has no sentence for" case, which also asserts the protocol sentence is not guessed for another code.
  3. LocalInstallNotLoaded.requiredRange: string required — matches the closed two-member marker the PR reads off the server. RIGHT; and the otherReason branch does not read it, so a later marker without it cannot break the row.
  4. LocalInstallEntry.installedBy required to optional (string | null to ?: string | null). RIGHT: a widening on an in-surface reader type whose only consumer already guards with a logical-and (visible in the diff context: the "by" line renders only when present); the narrowed-caller fixture needs the absence, and the pin for it is lit (the row renders, the operator-only line is what is absent).
  5. MarketplacePackagePage local menu now branches on notLoaded before reading withSampleData. RIGHT: before this, the marker's omission of withSampleData read as "Add sample data" and offered a re-seed into objects the runtime never registered; re-seed and purge are hidden for a notLoaded entry, Uninstall stays, Reinstall stays. Reinstall is the compatible re-install the card itself says must stay reachable, so keeping it is the card's reading, not a gap. Pinned in both menu-drawing shapes (catalog view, offline local view) with a loaded-entry control at three items.
  6. InstalledList row: destructive badge, one reason line, Uninstall enabled; the row's confirm and result switch on notLoaded. RIGHT against the card's step and Done-when; a row without the marker adds no node, and the loaded-row cases are pure preservation pins (the second commit made them so).
  7. No REST path, query set, metadata schema or @objectstack/* import changes; nothing in packages/spec-governed territory. RIGHT; the claim's floor note (raise an importer's floor if a 17.7.0-only export is needed) is satisfied vacuously: the marker type is hand-declared beside LocalInstallEntry, which is that file's existing pattern for this listing.

Public-surface changes (what a published package's accept set gains):

  1. @object-ui/i18n: five new keys (marketplace.notLoaded.badge, .protocolIncompatible, .otherReason, marketplace.uninstall.confirmNotLoaded, .successNotLoaded) in all ten packs. PUBLIC and RIGHT: ./locales and ./locales/* are in the package's exports, so the keys are published accept-set; additive only (check:i18n-drift: 0 en values changed, 5 added). Every pack carries the same placeholder set per key (requiredRange; code; manifestId + version; manifestId), and the nine non-en packs are translations, not copies. Code comments in the packs carry no model identifier.
  2. @object-ui/app-shell: LocalInstallEntry / LocalInstallNotLoaded are NOT public. RIGHT under the rule that the published face is the exports map: app-shell exports . (dist/index.d.ts) and ./styles.css only; the PR's walk of the built declaration closure (168 files, zero hits for either name, positive control reached) is consistent with that map, and src/index.ts exports the marketplace components, not the API module. Shipped bytes, not published accept-set; the user-visible behaviour change is what the changeset covers.
  3. No existing string, key, export or type member is removed or renamed. RIGHT: nothing narrows, so no migration text and no ADR-0087 disposition is owed.

Nothing in the diff is judged wrong.

② Semver level

  • .changeset/11645-installed-not-loaded.md declares minor for @object-ui/app-shell and @object-ui/i18n; both are in the fixed group, so one bump lifts the group consistently.
  • PR body line 3 carries Clause-②: yes, copied from the claim as the protocol asks; yes requires at least minor, and minor is what is declared. No (widening) / (narrowing) arm is written, which is correct: the arm is optional and nothing here narrows.
  • The level matches what the diff publishes: the five i18n keys are the public addition (item 8), which is exactly what makes the clause yes; app-shell alone would have been a patch-level fix, and under the fixed group the minor is the right single declaration. No major (Changeset Bump Policy green); presence declared (Changeset Declaration, Changeset Claim Re-read, Changeset Fixed Group Check all green).
  • The changeset body names the behaviour and the five keys, cites the card, carries no model identifier, and needs no FROM-to-TO mapping because nothing is removed.

③ Boundary flags

  1. open_questions[0] — keep marketplace.uninstall.confirmNotLoaded / successNotLoaded (A) or drop them (B)? ANSWER: A, keep. The card's Done-when is "Uninstall works from that row", so the row's Uninstall flow is in scope, and the loaded-package texts tell the operator the app "will remain loaded in the running kernel until the next restart" in the very flow the card accepts on: the opposite of the badge beside the button. Two additive keys in ten packs, pinned by the Uninstall-flow case and its loaded-row control; the minor already covers them. The matching deviations entry is answered by the same decision.
  2. deviations — installedBy made optional: ACCEPTED, see ①.4.
  3. deviations — full app-shell suite ran at 79a84a7, not at the head: MOOT. The head commit 053d441 touches one file, the new InstalledListWidget.notLoaded-11645.test.tsx (+2/-2), and the gate verdicts are the head's own check-runs: Test (shard 1/8) through (8/8), Test, Test (dist pins), Type Check, Lint all success at 053d441.
  4. deviations — check:sdui-registration-pins and check:eager-locale-catalogues NOT MEASURED locally, "left to CI": CLOSED BY CI. Bundle Analysis (performance-budget.yml) fires on packages/** for pull requests, ran on this head, and is success; that job runs check-eager-locale-catalogues.mjs inside its budget step and pnpm check:sdui-registration-pins as the following unconditional step after the console build. The job's own PR comment (6030489536) reads PASS.
  5. deviations — repo-wide pnpm lint left to CI: CLOSED, Lint is success on the head.
  6. deviations — harness attribution reminder vs the repo rule: VERIFIED on both commits (79a84a7, 053d441): each ends with the model-free trailer pair the repo rule names (the session-URL trailer and the plain Co-authored-by: Claude line), with no model identifier in either; PR body ends with the session-URL footer; PR title, body, changeset and code comments carry no model identifier. Closed.
  7. deviations — worktree removed after the PR opened: not a boundary; nothing to answer.
  8. A flag the report did not raise: marketplaceApi.ts lies outside the claim's named file surface ("stop on breach; explain in the report"). ACCEPTED here: the edit is types only and is the minimum the card's step needs (the widget cannot read a member its type does not declare); the PR body says so ("types only") and ①.9 shows it adds no public surface. Recorded so the seat's checklist sees the breach was judged, not missed.
  9. out_of_scope_findings (three, all "carrier: none, noted, not filed"): the dev correctly did not file (the dev does not POST issues; the seat does). ESCALATED to the dispatching seat: file ONE family card for the Details/catalog half of the not-loaded marker — Details header still badges "Installed" for a notLoaded entry, Details' own Uninstall still says the app stays loaded until restart, the catalog page badges "Installed" regardless of load — with the dedupe words the report gives. Same family as console(marketplace): Installed Apps shows a package the runtime refused to load as installed — render the listing's not-loaded marker (the console half of objectstack-ai/objectstack#21822) #11645, outside this card's step (which scopes Details to actions), so not a condition of this verdict.
  10. The report's CI line (in_progress at report time): RESOLVED as stated in the inputs line above; nothing red, nothing pending.

Implemented-by: claude/issue-11645-installed-not-loaded
Reviewed-by: session_01CGZy1BGCjdN5cXqL9cnvB8

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 04:13
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 04:13
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 7b17705 Oct 7, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11645-installed-not-loaded branch October 7, 2026 04:30
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…rom globalActions (objectui#11439) (objectstack-ai#11764)

Fixes objectstack-ai#11439

Clause-②: no

## What changes

An action's translated copy is read from ONE bundle node, chosen by the
object the action belongs to, the way `@objectstack/spec` 17.7.0 reads
it (`actionTranslationNode` behind `lookupActionField`). This implements
triage's amended ruling on this card (comment 6030552631, amending
5942994297), route B.

- `packages/i18n/src/useObjectLabel.ts`, `actionSuffixes`: a key object
reads `objects.OBJECT._actions.ACTION.TAIL` only. No key object reads
`globalActions.ACTION.TAIL` only. The object-then-global chain
objectui#3372 added is gone. The eight action resolvers (`actionLabel`,
`actionConfirm`, `actionSuccess`, `actionOutcome`, `actionDescription`,
`actionResultDialog`, `actionParamText`, `actionParamOptionLabel`) keep
their signatures, and their docblocks now state the rule.
`getAppNamespaces` still counts `globalActions` as an app scope, because
object-less actions need it.
- `packages/react/src/hooks/useActionTextLocalizer.ts`: the key object
is the action's declared `objectName`, else the host the caller passes.
That is what `translateObject` stamps on an embedded action. It is also
what the runtime-side resolvers in `useConsoleActionRuntime` and
`RecordDetailView` already compute for the description, params and
result dialog. No parameter is added: `objectName` is a spec key the
action object already carries.
- `packages/i18n/src/__tests__/useObjectLabel-globalAction.test.tsx`:
the docblock is corrected. The two bound-action cases (`actionLabel` /
`actionSuccess` on `crm_case` / `log_call`) are re-pointed to the
object-scoped copy, each with a comment citing 6030552631. The case
"still resolves a globalAction when objectName is omitted" is
byte-identical.
- `apps/console/src/preview-samples.ts`, the `translation` sample:
`close_order`, which the `action` sample binds to `sales_order`, moves
from `globalActions.close_order` to
`objects.sales_order._actions.close_order` as a `{ label }` node.
`fields.amount` becomes a `{ label }` node.
- `.changeset/11439-bound-action-copy.md`: patch on `@object-ui/i18n`
and `@object-ui/react`.

**Behaviour change, in plain words:** a bound action's copy filed under
`globalActions` no longer applies, on any of the action's texts. Move it
to `objects.OBJECT._actions.ACTION`. An action with no object still
reads `globalActions`.

No input, export, prop or language-pack key is added.

## Dispatch sites measured for the key-object rule

Every caller of `useActionTextLocalizer` passes its host object and
draws actions from that object's own `actions` array, so a declared
`objectName` there equals the host or is absent:

- `DeclaredActionsBar`: its `objectName` prop. The dispatch stamps that
host as `objectName`.
- `ObjectView`, toolbar and row actions: `objectDef.name`.
- `RecordDetailView`, record actions: `objectDef.name`.
- `RelatedRecordActionsBridge`: `childDef.name`. The dispatch stamps the
child object as `objectName`.
- `record:quick_actions` and `record:related_list`: the context object,
or `undefined` when there is none.
- `page:header`: `ctx.objectName`, with ids resolved against that
object's actions.
- The permission-set clone dispatch in `PermissionMatrixEditor`:
`PERMISSION_SET_OBJECT`.

None of these files changes. With the localizer reading the declared
`objectName` first, each one already reaches `actionSuffixes` with the
key object. The localizer line changes one case: an action that declares
`objectName` but is drawn where the caller passes no object. It now
reads its own object's copy instead of `globalActions`.

## Census of objectui's own bundles

`git grep -c globalActions` over the tree at `77c12b9`, CHANGELOGs
excluded, finds one bundle-data hit: the preview sample's `close_order`,
which is bound and moves here. It also serves as the live positive
control, since the card's census comment named it. The spec-translations
passthrough fixture's `globalActions.save` names no declared action, so
it is object-less and stays. The other hits are resolver code, UI chrome
(the `TranslationPreview` category) and catalogue keys.

## Sample parse: `TranslationDataSchema` from `@objectstack/spec/system`
17.7.0

- **Before:** 2 issues, `objects.sales_order.fields.amount:
invalid_type` and `globalActions.close_order: invalid_type`.
- **After:** success.

The new
`apps/console/src/__tests__/preview-samples-translation-data-11439.test.ts`
pins this. In the spec-valid test, the sample stays in `KNOWN_STALE` for
its record-vs-collection question. This PR does not touch that question,
and that test's reverse row stays green.

## Tests

**New pins:**
- In the pin file, "never reads globalActions copy for a bound action":
for an action translated only under `globalActions`, all eight resolvers
keyed on `crm_case` show the authored text. With no key object, they
read the global node (control).
-
`packages/react/src/hooks/__tests__/useActionTextLocalizer.keyObject-11439.test.tsx`:
  - a bound action with only global copy shows its authored text;
  - a bound action with object copy reads that copy;
  - the declared `objectName` is the key when the caller passes no host;
  - the declared `objectName` wins over the caller's host;
  - an embedded action keys on its host;
  - an action with no key object reads `globalActions`.

**Ablations.** Each went through `ablation-replace.mjs`. The mutation
landing was shown by anchor count and blob hash. The restore was proven
by blob equal to HEAD and an empty `git diff HEAD`. The subjects resolve
through vitest's source alias, so no build sits in between.
1. The old chain restored in `actionSuffixes` turns 4 pins red: the pin
file's "never reads globalActions copy for a bound action" (expected
'合并工单' to be 'Merge Cases') and three localizer pins (expected
'关闭订单(全局)' to be 'Close Order').
2. A localizer that ignores the declared `objectName` turns 2 pins red:
"the declared objectName is the key even where the caller knows no
object" and "the declared objectName wins over the host the caller
passes".
3. The sample's `fields.amount` put back as a flat string turns the
parse pin red (`objects.sales_order.fields.amount: invalid_type`).

## Local gates

All runs are at HEAD `48858e9`, from the worktree root.

- `pnpm exec vitest run --maxWorkers=2 packages/i18n/ packages/react/`
plus the action-copy consumers: `page-header-action-i18n`,
`page-header-action-ids`, `record-quick-actions.actionText-i18n`,
`record-quick-actions.resultDialog`,
`RecordRelatedListRenderer.authoredActions-11163`,
`record-alert.resultDialog`, `DeclaredActionsBar`,
`useConsoleActionRuntime.overrideNotice`, and the four console
`preview-samples-*` tests. Result: `Test Files 202 passed (202)`, `Tests
2835 passed | 13 skipped (2848)`.
- `pnpm --filter @object-ui/i18n type-check` exit 0 and `pnpm --filter
@object-ui/react type-check` exit 0, after `pnpm --filter
'@object-ui/react^...' build`. Each package's test project lists the
touched test (`--listFilesOnly`).
- `@object-ui/console` `type-check`: **NOT MEASURED** locally. It
resolves every workspace dependency through built `dist`, so it needs
the whole console closure built. It is declared to CI's Type Check job.
In its place, a narrowed `tsc` ran over the six touched source and test
files with root paths extended to sources (200 workspace files in the
program) and exited 0. A deliberately mistyped control file added to the
same program went red with TS2322.
- `pnpm exec eslint` on the six touched files: 0 errors. There are no
new warnings: the localizer's `as any` count is 8, as on the base.
- `check:control-bytes`, `check:test-path-roots`,
`check:changeset-claims`, `check:pending-changeset-literals`,
`check:new-line-citations` (0 new) and
`scripts/check-changeset-presence.mjs`: all exit 0.

## Acceptance notes

- **Object-less actions on an object's bar.** Such an action now reads
that object's scope only, because the host is its key object (ruling
rule 1). The retriage found no measured producer.
- **Boundary, noted and not filed:** an action that declares an
`objectName` different from the object whose `actions` array carries it.
- The localizer keys its label, confirm, success and outcome copy on the
declared object, as `translateObject` does.
- `DeclaredActionsBar`, `RelatedRecordActionsBridge` and the clone
dispatch stamp the host as the dispatched `objectName`, so the
runtime-side description, params and result dialog would key on the
host.
  - No measured producer writes that shape. Carrier: none.
- **Not this PR:**
- the three objectstack texts that describe objectui's reader as
object-scoped first, then `globalActions`: the `action.zod.ts` docblock,
the `globalActions` docblock in `translation.zod.ts`, and
`packages/spec/liveness/action.json`. The landing comment names them for
the objectstack `domain:spec` lane;
- objectui#11755, the translation designer's object-rendering row. It is
graded on its own.
- **Docs:** no README or guide page describes the `globalActions`
fallback (`git grep` over `content/`, the two package READMEs and
`skills/`), so none changes.
- `origin/main` moved to `7b17705` (objectstack-ai#11759) after this branch's base.
That commit touches none of these files.

Session: `https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8`

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

---------

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

Projects

None yet

2 participants