Repository navigation
fix(app-shell): Installed Apps reads a package the runtime refused to load as not loaded (objectui#11645) - #11759
Conversation
… 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>
✅ Console Performance Budget
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
Size Limits
|
Contract reviewServed-tier: Inputs read: card #11645 (body and all four comments, 5991416133 / 6029233802 / 6029532484 / 6030476046), PR #11759 (body, 16-file list, net diff against ① Derived judgmentsAccept-set changes (what the console reads off the wire):
Public-surface changes (what a published package's accept set gains):
Nothing in the diff is judged wrong. ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
…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>
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 sincec0862c1.Written by the os-dev agent dispatched on claim comment
6029532484(seatdomain:ui#3,mode:subagent), sessionhttps://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8.The server's wire shape, member by member
Read at objectstack
48297ad980:handleListand theNotLoadedMarkerinterface inpackages/cloud-connection/src/marketplace-install-local-plugin.ts, andProtocolIncompatibleDiagnosticinpackages/metadata-core/src/protocol-handshake.ts.LocalInstallEntryinmarketplaceApi.ts)notLoaded.codenotLoaded?: LocalInstallNotLoaded, membercode: stringOS_PROTOCOL_INCOMPATIBLEselectsmarketplace.notLoaded.protocolIncompatible. Any other code selectsmarketplace.notLoaded.otherReason, which names the code.notLoaded.requiredRangerequiredRange: stringmarketplace.notLoaded.protocolIncompatiblewithSampleDatafalse)withSampleData?: boolean, unchangednotLoadedis absentinstalledBymanage_metadata) callers onlyinstalledBy?: string or null, was requirednotLoadedis closed on the server: exactlycodeandrequiredRange.LocalInstallNotLoadeddeclares exactly those two members.codeis typedstring, 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.installedByis 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
LocalInstallEntryandLocalInstallNotLoadedare not public. Afterpnpm --filter @object-ui/app-shell build, a walk of the relative-import closure ofpackages/app-shell/dist/index.d.tsreaches 168 declaration files. None of them containsLocalInstallEntry,LocalInstallNotLoaded,notLoadedormarketplaceApi. Positive controls:MarketplacePackagePageis reached (2 files), and the emitteddist/console/marketplace/marketplaceApi.d.tsdoes carrynotLoaded. The exports map has only.and./styles.css, so no deep import reaches the file.pnpm --filter @object-ui/i18n build,dist/locales/en.js,dist/locales/en.d.tsanddist/locales/zh.jseach carry them. SoClause-②: yesand theminorchangeset stand.New language-pack keys, in all ten packs
marketplace.notLoaded.badgenotLoadedmarketplace.notLoaded.protocolIncompatiblerequiredRangenotLoaded.requiredRange, whennotLoaded.codeisOS_PROTOCOL_INCOMPATIBLEmarketplace.notLoaded.otherReasoncodenotLoaded.code, for any other codemarketplace.uninstall.confirmNotLoadedmanifestId,versionmanifestIdandversionmarketplace.uninstall.successNotLoadedmanifestIdmanifestIdgit grepforOS_PROTOCOL_INCOMPATIBLE,requiredRange,protocolandincompatiboverpackages/i18n/src/locales/en.tsandpackages/app-shell/srcfound nothing relevant. Control:versionBadgeis found in all ten packs.untranslated-identitypin refuses an English copy in zh, ja, ko, ru and ar, and it is green.uninstall.*NotLoadedkeys. 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: adestructive"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 bynotLoaded. A row withoutnotLoadedadds no node.MarketplacePackagePage.tsx: the local menu's re-seed and purge items are not drawn for anotLoadedentry. 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 missingwithSampleDataread as no sample data), which posts a re-seed into objects the runtime never registered. It also showed purge, disabled.marketplaceApi.ts: types only..changeset/11645-installed-not-loaded.md:minorfor@object-ui/app-shelland@object-ui/i18n.Tests
InstalledListWidget.notLoaded-11645.test.tsx, 9 cases.marketplaceApiis not mocked. One stubbedfetchanswers with the listing body the server landed, over a ledger that DELETE changes. The realI18nProviderrenders 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:^16, Uninstall enabled (en, zh);installedBy: badge and reason (en, zh);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/at79a84a7: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 at053d441: 15/15.pnpm exec vitest run packages/i18n/at79a84a7(i18n has not changed since):Test Files 81 passed (81),Tests 1310 passed | 13 skipped (1323).pnpm --filter @object-ui/i18n type-checkandpnpm --filter @object-ui/app-shell type-checkat053d441: exit 0. The script name echoes astype-check.tsc -p tsconfig.test.json --listFileslists both new test files (2 of 5063 lines). Control: the existingmarketplaceDates.displayLocale-10331.test.tsxis listed too.Reverse verification (one-off, not kept)
053d441). Then thec0862c1blobs ofInstalledListWidget.tsxandMarketplacePackagePage.tsxwere checked out, under a trap that restoresHEAD.git hash-objectequalled the base blobsd2eaeb1dande06fd029, and thenotLoadedcount went from 10 to 0 (widget) and from 2 to 0 (page).0e1e48aeand94d89725,git diff HEADis empty on both paths, the index is clean, and the count is back to 10.installedBy, one without, one with a catalog id distinct from its manifest id) and hashed the container'sinnerHTML. Base and head are identical: en 6659 bytes, sha256fdfbc6d0…; zh 6461 bytes, sha256250c012d…. Acmpof the two dumps also reports them identical.Gates, at
053d441Each 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, OKpnpm check:phantom-deps: 0.pnpm check:self-import: 0pnpm changeset:check: 0, "No changeset declares amajorbump."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 --testover the 16 paths: "NOT GOVERNED".ts/.tsxfiles: exit 0, 0 errors, 49 warnings, 0 of them on a line this diff adds (mapped against thegit diff -U0hunks).isPathIgnoredis false for the touched files, and the--format jsonoutput has 15 results.parserOptions.projectorprojectService, so linting is not type-aware and this diff cannot change the verdict on an untouched file.pnpm lintis left to CI.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 theComponentRegistry.registercall. Left to CI.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.
marketplace.uninstall.confirmandsuccessInDetail, 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.MarketplacePage) badges a local install "Installed vX" whether or not it is loaded.content/docsor the app-shell README describes the Installed Apps rows, so no doc page changes.Generated by Claude Code