From c7e05e8d84d15c6f65eb97c3d7464445e4a210f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:20:37 +0000 Subject: [PATCH 1/2] fix(app-shell): permission set provenance badge reads packageId and the artifact only Spec 18 retired the authored `managedBy` on a permission set (objectstack#23891), so no served or saved body carries it. The badge's "Platform" arm could no longer fire, and a stray `managedBy` on an old stored row could still move the answer. Both badge sites (the routed page's badge and the Studio kind chip) now read one derivation, `shippedByPackage = !!packageId || codeIsArtifact`. No served datum names a platform set, so "Platform" goes, with its now-dead `perm.badge.platform` string (en and zh). The local `PermissionSetDraft` type drops its `managedBy` member. Claude-Session: https://claude.ai/code/session_0193EQMEE4tpjYNJen4LAhgR Co-authored-by: Claude --- .../12753-perm-provenance-no-managedby.md | 13 +++ .../PermissionMatrixEditor.badge.test.tsx | 90 +++++++++++++------ .../metadata-admin/PermissionMatrixEditor.tsx | 29 +++--- .../src/views/metadata-admin/i18n.ts | 2 - .../views/metadata-admin/permission-slice.ts | 3 +- 5 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 .changeset/12753-perm-provenance-no-managedby.md diff --git a/.changeset/12753-perm-provenance-no-managedby.md b/.changeset/12753-perm-provenance-no-managedby.md new file mode 100644 index 0000000000..98057109d1 --- /dev/null +++ b/.changeset/12753-perm-provenance-no-managedby.md @@ -0,0 +1,13 @@ +--- +'@object-ui/app-shell': patch +--- + +The permission set editor's provenance badge decides from `packageId` and the package artifact only (objectui#12753). Spec 18 retired the authored `managedBy` on a permission set (objectstack#23891), so no served or saved body carries it. The badge used to read it for a third answer, "Platform", and as one input to "Package". No served datum names a platform set: the layered read serves `_packageId` and `_provenance` (`package` or `org`), so a set the platform ships through a package reads "Package", like any other packaged set. + +- The badge now has two answers. "Package" means a package ships the set: it is opened under the package door's `packageId`, or its code layer is a package artifact. "Custom" means it was authored in this environment. +- The routed page's badge and the Studio Access pillar's kind chip read one derivation, so they cannot disagree. +- A draft that still carries a stray `managedBy` (an old stored row) no longer moves the answer. `managedBy: 'platform'` used to read "Platform", and `managedBy: 'package'` used to read "Package" for a set no package ships. Both now read "Custom". + +Clause-②: yes (narrowing) + +The metadata-admin string table loses `perm.badge.platform` (en and zh), the "Platform" label only that badge read. The table is module-local: it is not exported from the package entry, so no consumer can address the key. The local `PermissionSetDraft` type loses its `managedBy` member; that type is not exported from the package entry either. No export, prop or exported type member changes. diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx index 6a4c941c20..e726a3875f 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.badge.test.tsx @@ -1,20 +1,39 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * A4 (framework#2920) — the provenance badge in the Studio permission matrix is - * a TRI-STATE (platform / package / admin-custom), mirroring the unified sys_* - * `managed_by` vocabulary. This guards each branch of the ternary. + * The provenance badge in the permission set editor has two answers: "Package" + * (a package ships the set: the package door's `packageId`, or the layered + * envelope's code artifact) and "Custom" (authored in this environment). + * + * objectui#12753 — spec 18 retired the authored `managedBy` on a permission set + * (objectstack#23891). The badge used to read it for a third answer, + * "Platform", and for "Package" too. No served datum names a platform set, so + * the badge reads `packageId` and the artifact only. A draft that still carries + * a stray `managedBy` (an old stored row) must not move the answer. + * + * Both sites are pinned: the routed page's badge, and the Studio Access + * pillar's kind chip (`studio`). They read one derivation, so they agree. */ +import '@testing-library/jest-dom/vitest'; import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, cleanup } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; +/** The layered envelope's `code` slot; `null` = no code layer. */ +let codeLayer: Record | null = null; + function makeClient(set: Record) { return { - layered: async () => ({ effective: set, code: null, overlay: null, overlayScope: null }), + layered: async () => ({ + effective: set, + code: codeLayer, + overlay: null, + overlayScope: null, + ...(codeLayer ? { provenance: 'package' } : {}), + }), getDraft: async () => null, - list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account' } }] : []), + list: async (type: string) => (type === 'object' ? [{ item: { name: 'a_account', label: 'Account' } }] : []), get: async (type: string) => (type === 'object' ? { fields: [] } : null), save: async (_t: string, _n: string, payload: Record) => payload, } as any; @@ -34,44 +53,65 @@ vi.mock('./AssignedUsersSection', () => ({ AssignedUsersSection: () => null })); import { PermissionMatrixEditPage } from './PermissionMatrixEditor'; -afterEach(cleanup); +afterEach(() => { + cleanup(); + codeLayer = null; +}); function baseSet(extra: Record) { return { name: 'sales_perms', label: 'Sales', isProfile: false, objects: {}, fields: {}, ...extra }; } -async function renderWith(extra: Record) { +type Site = 'page' | 'studio'; + +async function renderWith(site: Site, extra: Record, packageId?: string) { clientImpl = makeClient(baseSet(extra)); render( - + {site === 'studio' ? ( + {} }} + /> + ) : ( + + )} , ); - // Wait for the load to settle. The identity strip is collapsed by default - // (objectui#2600 B1), so the label shows as summary text, not an input. - await screen.findByText('Sales'); + // Wait for the load to settle: the object list arrives with the set. + await screen.findByText('Account'); } -describe('PermissionMatrixEditPage — provenance tri-state badge (A4 framework#2920)', () => { - it('managedBy "platform" renders the Platform badge', async () => { - await renderWith({ managedBy: 'platform' }); - expect(screen.getByText('Platform')).toBeInTheDocument(); - expect(screen.queryByText('Custom')).not.toBeInTheDocument(); +const badgeText = () => screen.getByTestId('perm-provenance-badge').textContent; + +describe.each(['page', 'studio'])('the provenance badge (%s site)', (site) => { + it('a set opened under a packageId reads "Package"', async () => { + await renderWith(site, {}, 'com.example.sales'); + expect(badgeText()).toBe('Package'); + }); + + it('a set whose code layer is a package artifact reads "Package"', async () => { + codeLayer = { _packageId: 'com.example.sales', name: 'sales_perms' }; + await renderWith(site, {}); + expect(badgeText()).toBe('Package'); }); - it('managedBy "package" renders the Package badge', async () => { - await renderWith({ managedBy: 'package' }); - expect(screen.getByText('Package')).toBeInTheDocument(); + it('a set with neither reads "Custom"', async () => { + await renderWith(site, {}); + expect(badgeText()).toBe('Custom'); }); - it('managedBy "admin" renders the Custom badge', async () => { - await renderWith({ managedBy: 'admin' }); - expect(screen.getByText('Custom')).toBeInTheDocument(); + it('a stray managedBy "platform" on an old stored row does not read "Platform"', async () => { + await renderWith(site, { managedBy: 'platform' }); + expect(badgeText()).toBe('Custom'); expect(screen.queryByText('Platform')).not.toBeInTheDocument(); }); - it('legacy managedBy "user" still falls through to Custom (read compat)', async () => { - await renderWith({ managedBy: 'user' }); - expect(screen.getByText('Custom')).toBeInTheDocument(); + it('a stray managedBy "package" does not make an environment-authored set read "Package"', async () => { + await renderWith(site, { managedBy: 'package' }); + expect(badgeText()).toBe('Custom'); }); }); diff --git a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx index 6ce51094b1..bf97fcc377 100644 --- a/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx +++ b/packages/app-shell/src/views/metadata-admin/PermissionMatrixEditor.tsx @@ -459,6 +459,12 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, // opt-in would be a client copy of the seal rule. `undefined` keeps the // flag's answer, as before. const artifactTierApplies = !packageId && codeIsArtifact; + // The set's provenance, for the page badge and the Studio kind chip alike: + // a package ships it (the package door's `packageId`, or the layered + // envelope's code artifact), else it was authored in this environment. Spec + // 18 retired the authored `managedBy` (objectstack#23891), and no served + // datum names a "platform" set, so there are two answers (objectui#12753). + const shippedByPackage = !!packageId || codeIsArtifact; const artifactArmOpen = !!entry?.allowEnvironmentOverlay && servedEditable !== false; const typeTierOpen = !!(entry?.allowEnvironmentOverlay || entry?.allowRuntimeCreate); const canWriteByType = artifactTierApplies ? artifactArmOpen : typeTierOpen; @@ -1354,19 +1360,14 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, ); const provenanceBadgesEl = ( <> - {/* [A4 framework#2920] Provenance tri-state — platform / package / - admin(custom) — mirrors the unified sys_* `managed_by` vocab. + {/* Provenance: Package or Custom, from `shippedByPackage`. objectui#7611 (F2): "Package" also reads the layered envelope this editor already fetches (`codeIsArtifact`, the artifact - gate's own input). The registry serves no `managedBy`, and at - the environment scope there is no `packageId`, so a set a code - package ships read "Custom" beside its own lock. */} + gate's own input). At the environment scope there is no + `packageId`, so a set a code package ships read "Custom" beside + its own lock. */} - {draft.managedBy === 'platform' - ? t('perm.badge.platform') - : draft.managedBy === 'package' || packageId || codeIsArtifact - ? t('perm.badge.package') - : t('perm.badge.custom')} + {shippedByPackage ? t('perm.badge.package') : t('perm.badge.custom')} {!!draft.isDefault && ( {t('perm.badge.default')} @@ -1601,13 +1602,9 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved, chips={ <> {/* The provenance chip in the artboard's kind-chip look; same - test id and the same three answers as the page's badge. */} + test id and the same answer as the page's badge. */} - {draft.managedBy === 'platform' - ? t('perm.badge.platform') - : draft.managedBy === 'package' || packageId || codeIsArtifact - ? t('perm.badge.package') - : t('perm.badge.custom')} + {shippedByPackage ? t('perm.badge.package') : t('perm.badge.custom')} {!!draft.isDefault && {t('perm.badge.default')}} {/* A host-locked package says so in the pillar's note; the diff --git a/packages/app-shell/src/views/metadata-admin/i18n.ts b/packages/app-shell/src/views/metadata-admin/i18n.ts index d077dd4b87..31bd707829 100644 --- a/packages/app-shell/src/views/metadata-admin/i18n.ts +++ b/packages/app-shell/src/views/metadata-admin/i18n.ts @@ -2413,7 +2413,6 @@ const ENGINE_STRINGS_EN: Record = { 'perm.basics.editHint': 'Edit name / label', 'perm.cap.none': 'No system capabilities', 'perm.cap.add': 'Add capability', - 'perm.badge.platform': 'Platform', 'perm.badge.package': 'Package', 'perm.badge.custom': 'Custom', 'perm.badge.default': 'Default for new users', @@ -5812,7 +5811,6 @@ const ENGINE_STRINGS_ZH: Record = { 'perm.basics.editHint': '编辑名称 / 标签', 'perm.cap.none': '无附加系统能力', 'perm.cap.add': '添加能力', - 'perm.badge.platform': '平台内置', 'perm.badge.package': '包内置', 'perm.badge.custom': '本环境自定义', 'perm.badge.default': '新用户默认', diff --git a/packages/app-shell/src/views/metadata-admin/permission-slice.ts b/packages/app-shell/src/views/metadata-admin/permission-slice.ts index dd14740b58..1719413807 100644 --- a/packages/app-shell/src/views/metadata-admin/permission-slice.ts +++ b/packages/app-shell/src/views/metadata-admin/permission-slice.ts @@ -58,8 +58,7 @@ export interface FieldPerm { export interface PermissionSetDraft { name: string; label?: string; - isDefault?: boolean; // install-time suggestion (ADR-0090 D5); managedBy/packageId are provenance - managedBy?: string; + isDefault?: boolean; // install-time suggestion (ADR-0090 D5); packageId is provenance objects: Record; fields?: Record; systemPermissions?: string[]; From 44d065cbfed8b2273ebf28d2df8787bbc56a5f35 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 21:43:31 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(changeset):=2012753=20declares=20Clau?= =?UTF-8?q?se-=E2=91=A1=20no?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The removed `perm.badge.platform` lives in metadata-admin's module-local string table, which the package entry does not re-export, and `PermissionSetDraft` is not exported either: no published key, export or type member narrows. The paragraph after the line keeps the reason. Claude-Session: https://claude.ai/code/session_0193EQMEE4tpjYNJen4LAhgR Co-authored-by: Claude --- .changeset/12753-perm-provenance-no-managedby.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/12753-perm-provenance-no-managedby.md b/.changeset/12753-perm-provenance-no-managedby.md index 98057109d1..92db93c092 100644 --- a/.changeset/12753-perm-provenance-no-managedby.md +++ b/.changeset/12753-perm-provenance-no-managedby.md @@ -8,6 +8,6 @@ The permission set editor's provenance badge decides from `packageId` and the pa - The routed page's badge and the Studio Access pillar's kind chip read one derivation, so they cannot disagree. - A draft that still carries a stray `managedBy` (an old stored row) no longer moves the answer. `managedBy: 'platform'` used to read "Platform", and `managedBy: 'package'` used to read "Package" for a set no package ships. Both now read "Custom". -Clause-②: yes (narrowing) +Clause-②: no The metadata-admin string table loses `perm.badge.platform` (en and zh), the "Platform" label only that badge read. The table is module-local: it is not exported from the package entry, so no consumer can address the key. The local `PermissionSetDraft` type loses its `managedBy` member; that type is not exported from the package entry either. No export, prop or exported type member changes.