diff --git a/.changeset/25098-setup-packages-to-studio.md b/.changeset/25098-setup-packages-to-studio.md new file mode 100644 index 00000000000..deccdfca094 --- /dev/null +++ b/.changeset/25098-setup-packages-to-studio.md @@ -0,0 +1,12 @@ +--- +'@objectstack/platform-objects': patch +--- + +feat(platform-objects): Setup no longer carries the unfiltered Packages entry; the full package list lives in Studio + +Clause-②: no +Metadata-protocol: none + +The full `developer:packages` list is a development surface, so its one unfiltered navigation entry is now Studio's Overview → Packages, which is unchanged. Setup's `group_apps` drops its `nav_packages` entry. Setup keeps the marketplace-filtered "Installed Apps" door (`nav_marketplace_installed`, contributed by `@objectstack/cloud-connection` wherever the marketplace capability is present), so its Apps group reads Browse Marketplace, Installed Apps and Packaged Actions. + +The Setup copies of the entry's label (`apps.setup.navigation.nav_packages` in `en`, `zh-CN`, `ja-JP` and `es-ES`) and their recorded source hashes are removed with it; Studio's copies stay. A bookmark to the Packages page keeps working, since the page itself did not move. diff --git a/packages/cli/test/connect-agent-both-halves-wire.pin.test.ts b/packages/cli/test/connect-agent-both-halves-wire.pin.test.ts index a57374277f3..751241141a2 100644 --- a/packages/cli/test/connect-agent-both-halves-wire.pin.test.ts +++ b/packages/cli/test/connect-agent-both-halves-wire.pin.test.ts @@ -254,14 +254,15 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', ( .toContain('nav_connect_agent'); // The Setup tree the platform admin is served BY THIS COMPOSITION: 9 // authored groups less `group_approvals`, which nothing contributes - // into and the filter collapses, plus the 25 entries - // `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one. + // into and the filter collapses, plus the 24 entries + // `SETUP_NAV_CONTRIBUTIONS` carries, plus the MCP one. (25 until + // #25098 moved the unfiltered Packages entry to Studio.) // `nav_settings_branding` is not among them, and not because of a // tenancy wall (this harness boots none): since #24766 it is // contributed by `@objectstack/service-settings` together with the // `branding` namespace, offered only where no organization wall stands, // and this harness composes platform-objects and the MCP bundle alone. - expect(allowedIds).toHaveLength(34); + expect(allowedIds).toHaveLength(33); expect(allowedIds).not.toContain('nav_settings_branding'); // The two per-user stores are reached from the User record page, not // from Setup's menu (#23803). @@ -284,8 +285,8 @@ describe('#17647 — Connect-an-Agent visibility: BOTH halves, over the wire', ( expect(withAccountHalf.statusCode).toBe(200); expect(withoutAccountHalf.statusCode).toBe(200); expect(navIds(withAccountHalf.body?.item)).toEqual(navIds(withoutAccountHalf.body?.item)); - // 34: the composition the count in the test above states. - expect(navIds(withAccountHalf.body?.item)).toHaveLength(34); + // 33: the composition the count in the test above states. + expect(navIds(withAccountHalf.body?.item)).toHaveLength(33); // The mechanism behind the invariance, asserted rather than assumed: // the bundle aims exactly one contribution at `setup` and one at diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index e120cb385a2..e8992d3f9d5 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -39,16 +39,19 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ ], }, { - // Package ADMINISTRATION is a platform/operator concern (ADR-0084: - // packages are Operate, out of the builder) — so its home is Setup, not - // the application builder. The console binds `developer:packages` to the - // existing package-management page. Building/creating apps is a separate - // journey (Home cover → /studio); this entry is for install/inspect/admin. + // No unfiltered Packages entry here (#25098, ruling 甲 amending #23892 B): + // the full `developer:packages` list is a development surface, so its one + // unfiltered door is Studio's Overview → Packages (`studio.app.ts`). Setup + // reaches installed packages through the marketplace-filtered "Installed + // Apps" door (`nav_marketplace_installed`, `developer:packages` with + // `params: { source: 'marketplace' }`), which + // `@objectstack/cloud-connection` contributes to this group wherever the + // marketplace capability is present. `setup-packages-door.test.ts` pins + // both halves. app: 'setup', group: 'group_apps', priority: BASE_PRIORITY, items: [ - { id: 'nav_packages', type: 'component', label: 'Packages', componentRef: 'developer:packages', icon: 'package' }, // Packaged actions are OPERATIONAL state — on/off per packaged action // (ADR-0126 §7.4: "Studio keeps the editing; Setup gets the operational // state"). ADR-0146 D4 keeps the action switch here while the flow diff --git a/packages/platform-objects/src/apps/setup-packaged-actions-nav.test.ts b/packages/platform-objects/src/apps/setup-packaged-actions-nav.test.ts index 782109d709e..c1e5eacaeb2 100644 --- a/packages/platform-objects/src/apps/setup-packaged-actions-nav.test.ts +++ b/packages/platform-objects/src/apps/setup-packaged-actions-nav.test.ts @@ -23,7 +23,7 @@ // `sys_metadata_activation` ledger this package registers (#12419) and work // on compositions with NO automation service; gating the entry on that // service would hide a page that still works there. -// - no `requiredPermissions` — matches `nav_packages` beside it: the Setup +// - no `requiredPermissions` — like the rest of `group_apps`: the Setup // app's own `setup.access` gates the app, and the activation WRITE doors // enforce `manage_metadata` / the ADR-0126 §5 operator gate server-side. import { describe, it, expect } from 'vitest'; @@ -72,7 +72,7 @@ describe('the Setup Packaged Actions entry targets the console component (ADR-01 }); }); - it('lives in group_apps beside Packages — package administration is Operate (ADR-0084)', () => { + it('lives in group_apps — operational package state is Operate (ADR-0084)', () => { expect(entry().group).toBe('group_apps'); }); @@ -93,8 +93,10 @@ describe('the Setup Packaged Actions entry targets the console component (ADR-01 describe('Setup › Packaged automation stays retired (ADR-0146 D11)', () => { it('no entry names the retired id or the retired `automation:packaged` ref', () => { const items = allItems().map(({ item }) => item); - // Control: the walk reads the real array. - expect(items.some((i) => i.id === 'nav_packages')).toBe(true); + // Control: the walk reads the real array. (It used to name Setup's + // `nav_packages`, which moved to Studio in #25098; + // `setup-packages-door.test.ts` pins that move.) + expect(items.some((i) => i.id === 'nav_packaged_actions')).toBe(true); expect(items.filter((i) => i.id === 'nav_packaged_automation')).toEqual([]); expect(items.filter((i) => i.componentRef === 'automation:packaged')).toEqual([]); }); diff --git a/packages/platform-objects/src/apps/setup-packages-door.test.ts b/packages/platform-objects/src/apps/setup-packages-door.test.ts new file mode 100644 index 00000000000..03e76374b5e --- /dev/null +++ b/packages/platform-objects/src/apps/setup-packages-door.test.ts @@ -0,0 +1,83 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// The full package list lives in Studio; Setup carries no unfiltered door to it +// (#25098, ruling 甲 amending #23892 ruling B). +// +// The console's `developer:packages` component is the one list of what is +// installed. Opened with no `source` filter it is a development surface, so its +// unfiltered entry lives in Studio only (Overview → Packages). Setup reaches +// installed packages through the marketplace-filtered "Installed Apps" door — +// `developer:packages` with `params: { source: 'marketplace' }` — which +// `@objectstack/cloud-connection` contributes to Setup's `group_apps` wherever +// the marketplace capability is present, and pins on its side +// (`marketplace-installed-ui-entry.pin.test.ts`). +// +// This file is the platform-objects half: what THIS package ships for each app. +// Both directions are pinned, because either regression is silent at runtime — +// an unfiltered Setup entry simply renders, and a Studio entry that goes +// missing leaves the full list reachable only by a hand-typed URL. +import { describe, it, expect } from 'vitest'; + +import { SETUP_APP } from './setup.app.js'; +import { SETUP_NAV_CONTRIBUTIONS } from './setup-nav.contributions.js'; +import { STUDIO_APP } from './studio.app.js'; + +type NavItem = { + id?: string; + type?: string; + componentRef?: string; + params?: Record; + children?: NavItem[]; +}; + +const PACKAGES_REF = 'developer:packages'; + +/** Every nav item in a tree, depth-first. */ +const walk = (items: unknown[] | undefined, out: NavItem[] = []): NavItem[] => { + for (const raw of items ?? []) { + const item = raw as NavItem | undefined; + if (!item) continue; + out.push(item); + if (Array.isArray(item.children)) walk(item.children, out); + } + return out; +}; + +/** A `developer:packages` entry with no `source` filter: the full list. */ +const isUnfilteredPackagesDoor = (item: NavItem): boolean => + item.componentRef === PACKAGES_REF && item.params?.source === undefined; + +const setupItems = (): NavItem[] => [ + ...walk(SETUP_APP.navigation as unknown[]), + ...SETUP_NAV_CONTRIBUTIONS.flatMap((c) => walk(c.items as unknown[])), +]; + +describe('the unfiltered package list is a Studio surface, not a Setup one (#25098)', () => { + it('the predicate tells the full list from the marketplace-filtered door', () => { + // Controls on the predicate itself, so the absence below cannot pass + // because the predicate matches nothing. + expect(isUnfilteredPackagesDoor({ type: 'component', componentRef: PACKAGES_REF })).toBe(true); + expect( + isUnfilteredPackagesDoor({ type: 'component', componentRef: PACKAGES_REF, params: { source: 'marketplace' } }), + ).toBe(false); + }); + + it('Setup ships no unfiltered `developer:packages` entry', () => { + const items = setupItems(); + // Control: the walk reads the real Setup shell and its contributions. + expect(items.some((i) => i.id === 'group_apps')).toBe(true); + expect(items.some((i) => i.id === 'nav_packaged_actions')).toBe(true); + + expect(items.filter(isUnfilteredPackagesDoor)).toEqual([]); + expect(items.filter((i) => i.id === 'nav_packages')).toEqual([]); + }); + + it('Studio still ships the unfiltered entry, under Overview', () => { + const overview = walk(STUDIO_APP.navigation as unknown[]).find((i) => i.id === 'group_overview'); + expect(overview, 'Studio lost its group_overview').toBeDefined(); + + const doors = walk(overview!.children).filter(isUnfilteredPackagesDoor); + expect(doors).toHaveLength(1); + expect(doors[0]).toMatchObject({ id: 'nav_packages', type: 'component', componentRef: PACKAGES_REF }); + }); +}); diff --git a/packages/platform-objects/src/apps/translations/en.ts b/packages/platform-objects/src/apps/translations/en.ts index cdaddf1efbd..9f7486d75a7 100644 --- a/packages/platform-objects/src/apps/translations/en.ts +++ b/packages/platform-objects/src/apps/translations/en.ts @@ -70,12 +70,6 @@ export const en: TranslationData = { nav_system_overview: { label: 'System Overview' }, // Apps / Marketplace - // `nav_packages` is a SETUP entry (package administration is an - // operator concern — ADR-0084), distinct from the same-id entry under - // `apps.studio.navigation`. Two apps carrying one nav id is normal; - // each app resolves its own subtree, so the studio copy never answers - // for this one. Its absence here was #5750's first cause. - nav_packages: { label: 'Packages' }, nav_packaged_actions: { label: 'Packaged Actions' }, nav_marketplace_browse: { label: 'Browse Marketplace' }, nav_marketplace_installed: { label: 'Installed Apps' }, diff --git a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.ts b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.ts index b1a34935c5f..e1673e5ad08 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.source-hashes.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.source-hashes.ts @@ -71,7 +71,6 @@ export const esESSourceHashes: Readonly> = { 'apps.setup.description': 'deacdfc679232a4a', 'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf', 'apps.setup.navigation.group_apps.label': '699b367291abcbe6', - 'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73', 'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3', 'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd', 'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60', diff --git a/packages/platform-objects/src/apps/translations/es-ES.ts b/packages/platform-objects/src/apps/translations/es-ES.ts index 4e2f74aece4..c3829fc56d1 100644 --- a/packages/platform-objects/src/apps/translations/es-ES.ts +++ b/packages/platform-objects/src/apps/translations/es-ES.ts @@ -36,9 +36,6 @@ export const esES: TranslationData = { navigation: { group_overview: { label: 'Resumen' }, group_apps: { label: 'Aplicaciones' }, - // Setup's own `nav_packages` — distinct from the same id under - // `apps.studio.navigation`, which does not answer for this app (#5750). - nav_packages: { label: 'Paquetes' }, nav_packaged_actions: { label: 'Acciones empaquetadas' }, nav_marketplace_browse: { label: 'Explorar Marketplace' }, nav_marketplace_installed: { label: 'Aplicaciones instaladas' }, diff --git a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.ts b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.ts index b0ada86f820..56853d088ff 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.source-hashes.ts @@ -71,7 +71,6 @@ export const jaJPSourceHashes: Readonly> = { 'apps.setup.description': 'deacdfc679232a4a', 'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf', 'apps.setup.navigation.group_apps.label': '699b367291abcbe6', - 'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73', 'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3', 'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd', 'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60', diff --git a/packages/platform-objects/src/apps/translations/ja-JP.ts b/packages/platform-objects/src/apps/translations/ja-JP.ts index 89a8f775e7c..c774f18a705 100644 --- a/packages/platform-objects/src/apps/translations/ja-JP.ts +++ b/packages/platform-objects/src/apps/translations/ja-JP.ts @@ -36,9 +36,6 @@ export const jaJP: TranslationData = { navigation: { group_overview: { label: '概要' }, group_apps: { label: 'アプリ' }, - // Setup's own `nav_packages` — distinct from the same id under - // `apps.studio.navigation`, which does not answer for this app (#5750). - nav_packages: { label: 'パッケージ' }, nav_packaged_actions: { label: 'パッケージアクション' }, nav_marketplace_browse: { label: 'マーケットプレイスを閲覧' }, nav_marketplace_installed: { label: 'インストール済みアプリ' }, diff --git a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.ts b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.ts index ec80aaf76b4..f6686899170 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.source-hashes.ts @@ -71,7 +71,6 @@ export const zhCNSourceHashes: Readonly> = { 'apps.setup.description': 'deacdfc679232a4a', 'apps.setup.navigation.group_overview.label': 'eebdbe1cc7b59cdf', 'apps.setup.navigation.group_apps.label': '699b367291abcbe6', - 'apps.setup.navigation.nav_packages.label': '8cd8f29658025f73', 'apps.setup.navigation.nav_packaged_actions.label': '0d86b11df972dff3', 'apps.setup.navigation.nav_marketplace_browse.label': '2afbe021f5419fdd', 'apps.setup.navigation.nav_marketplace_installed.label': '93bd5d458e05db60', diff --git a/packages/platform-objects/src/apps/translations/zh-CN.ts b/packages/platform-objects/src/apps/translations/zh-CN.ts index ca51232285d..d748ede03be 100644 --- a/packages/platform-objects/src/apps/translations/zh-CN.ts +++ b/packages/platform-objects/src/apps/translations/zh-CN.ts @@ -36,12 +36,6 @@ export const zhCN: TranslationData = { navigation: { group_overview: { label: '总览' }, group_apps: { label: '应用' }, - // The Setup app's own `nav_packages`. A translation for this id already - // existed under `apps.studio.navigation` and did NOT answer here — the - // lookup is per app — so Setup fell back to the author's 'Packages' - // literal in an otherwise Chinese menu (#5750). Both entries are - // legitimate; keep them both. - nav_packages: { label: '软件包' }, nav_packaged_actions: { label: '打包动作' }, nav_marketplace_browse: { label: '浏览应用市场' }, nav_marketplace_installed: { label: '已安装应用' },