Skip to content

fix(console): 把 system/permissions 与 Permissions 计数一并接到 sys_permission_set,收尾 #3655 第五腿 - #3728

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3655-permissions-leg
Aug 8, 2026
Merged

fix(console): 把 system/permissions 与 Permissions 计数一并接到 sys_permission_set,收尾 #3655 第五腿#3728
yinlianghui merged 1 commit into
mainfrom
claude/issue-3655-permissions-leg

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #3655

第五条也是最后一条腿。按 #3655 的 PM 裁决(维护者已授权代裁,裁决评论)落地 A —— sys_permission_set;C(卡片墙连同 hub 退场)保留为独立产品项,不阻塞,本 PR 未替它做任何事。

先复核前提(逐条对 origin/main @ 36bf20235 与框架 ../objectstack @ b4872a868)

裁决的结论成立,但它引用的一条论据不成立 —— 照实写在这里,并已写进代码注释,免得下一个读者继承它:

复核项 结果
sys_permission_set 是框架注册的对象 packages/plugins/plugin-security/src/objects/sys-permission-set.object.ts:16 name: 'sys_permission_set'
Setup 导航有对应条目 security-plugin.ts:445 { id: 'nav_permission_sets', type: 'object', label: 'Permission Sets', objectName: 'sys_permission_set', icon: 'lock' },contribute 进 app: 'setup' / group_access_control
sys_permission(不含 _set)仍然不是对象 ✅ 框架全仓精确 grep 零命中
文档称 permission set 为「the only capability container」 content/docs/permissions/permissions-matrix.mdx:64 等四处;administrator-guide.mdx:25 展开为「object CRUD、field security、access depth、system capabilities」
⚠️ 裁决理由 1 的「B 是 managedBy:'config' + no-overlay 的平台锁定注册表,A 是带管理员 CRUD 的授予容器」 不成立两个都是 managedBy: 'config' + protection.lock: 'no-overlay'(sys-permission-set.object.ts:21,24-25 / sys-capability.object.ts:32,35-36),且两个 docblock 都写明锁的是 schema、租户/管理员可以加行(capability 那句原文:「The platform/packages DEFINE capabilities; admins EXTEND them in Setup」)。「对 B 做管理员增删没有意义」不成立。

结论没有因此改变,换了一条更硬的论据:ADR-0066 的三层分离 —— capability(层 1,「能做什么」的定义目录)/ assignment(层 2)/ requirement(层 3)。sys_capability 是层 1,sys_permission_set 是层 2;卡片文案「Manage permission rules and assignments」正是层 2,而层 1 是 permission set 按 name 引用的东西,不是被授予的东西。这条写进了 AppContent.tsx 的路由块注释,连同上面那条订正。

当前 main 上 permissions 腿的三处现状也逐条复核成立:AppContent.tsx 的「absent on purpose」注释、systemHubRoutes.test.tsx 钉住的未变落点、counts.test.tsx 钉住的恒 0。

五腿终局表(实测落点,有应用分支)

URL 本 PR 前 本 PR 后 落地于
system/users RouteNotFound(5 字) 一跳 …/sys_user #3673
system/organizations 改写为 …/system/record/organizations,RecordDetailView 收到 objectName:"system" 一跳 …/sys_organization #3673
system/roles RouteNotFound(5 字) 一跳 …/sys_position #3673
system/positions 改写为 …/system/record/positions 一跳 …/sys_position #3673
system/permissions 改写为 …/system/record/permissions,记录页对象名 system 一跳 …/sys_permission_set 本 PR

计数侧同步:dataSource.find('sys_permission')'sys_permission_set'#3680 已把 Organizations 从 sys_org 改到 sys_organization,sys_permission 是当时唯一留着的错名;本 PR 之后五个名字全部是框架注册的名字,这一点由那条 audit 用例的一行断言机械保证(见下)。

钉翻转对照(替换不并存,#3609 纪律)

派发单点了两处必翻。实测另有六处断言钉在同一条被改的肢体上 —— 逐条判定处置,不批量重拼:

# 位置 原钉 处置 为什么是这个处置
1 systemHubRoutes.test.tsx MEASUREMENT system/permissions still lands on a record of the object system 整条替换:并入主 it.each 的一跳直达断言(第 5 行) 原注释自述「expected to be REPLACED, not merely to keep passing」
2 counts.test.tsx MEASUREMENT Permissions still reads 0 while both candidate objects hold rows 整条替换:5 permissions 真实计数 + 不查 sys_permission / 不查 sys_capability 同上;fixture 里 permission_set 5 行、capability 7 行都非零,所以徽章上的数字本身指认选了哪一个 —— 只有一个有行的 fixture 会让两个名字都过
3 systemHubRoutes.test.tsx 零应用 it.each 四行 补第五行 permissions → sys_permission_set 本宿主把同一 fragment 同时传给 extraRoutesextraRoutesNoApp,新路由在两个分支都生效
4 systemHubRoutes.test.tsx 零应用 MEASUREMENT system/permissions 断言「无应用时落 Page not found、绝不落记录页」 改指 system/workgroups 它钉的事实不是那个 URL,而是「零应用分支压根没有 ShorthandRecordRedirect,长词也不会变记录页」。permissions 一旦声明就必红,而事实仍然成立 —— 需要一个仍未声明的长词
5 systemHubRoutes.test.tsx 长度分叉 MEASUREMENT permissions(11 字)被判成 record id」由 #1 那条兼任 新增 system/workgroups 一条接手 #1 被替换后,「长词 → 记录页」这一侧会失去唯一覆盖。teams(5)/workgroups(10) 是同一概念的两种长度、两块不同的失败屏 —— 比原来用真缺口 URL 当标本更稳
6 counts.test.tsx MEASUREMENT「未注册对象与真空对象徽章相同」+ #3679 的三行矩阵用例 都用 sys_permission(压根不存在的名字)当「未注册」标本 补声明式改写:改成 delete state.registry.sys_permission_set,即没装 plugin-security 的部署 五个名字全部变成真名后,页面再没有拼错的名字可以当标本;但「未注册」这一行事实仍属适配器契约(吞 404 → 与真空同形),需要一个真实的发生场合,而没装该插件就是
7 counts.test.tsx #3679 的邻卡隔离用例 邻卡 permissions 断言 0 permissions 改字面量 5 permissions 它断言的是「答复成功的邻卡保住真实数字」;permissions 现在就是一张会答复的卡
8 counts.test.tsx audit 用例 调用清单含 sys_permission;filter(不在框架里) 期望 ['sys_permission'] 改为 sys_permission_set / 期望 [] 这一行现在是整份审计最强的信号:零个查询名字缺席于框架

QUERIED_OBJECT_NAMES(#3679 的全失败用例用它装失败)同步改名,否则会给一个页面不再查询的名字装失败,而真实的 sys_permission_set 照常答复 —— 那条用例会以假红的方式报出来。

其余断言一行未改。

逆向验证(两个扰动,方向都先写死后运行)

扰动 A —— 删掉新增那行路由声明(保留计数改名)

预测:2 failed | 22 passed 红的只应是两条 permissions 落点(有应用一跳、零应用空状态),方向是回到旧落点;而 4/5 两条 workgroups MEASUREMENT、其余四腿、以及 counts 全 9 条都不该动 —— 它们不经过这行路由。这点照实预测,不假装 MEASUREMENT 会跟着红。

实测逐条吻合,失败现场直接打印出修前的屏幕:

× /apps/setup/system/permissions reaches /apps/setup/sys_permission_set in ONE hop
× /apps/setup/system/permissions still redirects, and the target is the no-apps empty state

TestingLibraryElementError: Unable to find an element by: [data-testid="object-view"]
  {"appName":"setup","*":"system/record/permissions","objectName":"system","recordId":"permissions"}
TestingLibraryElementError: Unable to find an element by: [data-testid="create-first-app-btn"]

Tests  2 failed | 22 passed (24)

扰动 B —— 计数名退回 sys_permission(保留路由)

方向与 A 不同,单独预测。预测:4 failed | 20 passed —— audit 用例(调用清单 diff)、新的 5 permissions 用例、#3679 邻卡隔离用例(permissions 邻卡回 0)、#3679 全失败用例(失败装在页面不再查询的名字上,sys_permission 照常 resolve 空 → 徽章 0 permissions 冒出来)。

并且明确预测两条 0 permissions 用例保持绿,而且是「因为什么都没产生所以绿」:上表 #6 的两条把 sys_permission_set 从 registry 里删掉当「未注册」标本,而退回后查的 sys_permission 同样未注册 —— 两种错法在这条 fixture 上同形。这不是覆盖,是这条钉子的已知盲区,写在这里而不是留给下一个读者。

实测逐条吻合:

× asks for exactly five names, every one of them registered by the framework
× counts Permissions through sys_permission_set, the container the card describes
× blanks only the card that failed and leaves its neighbours their real counts
× shows no counts at all when every lookup fails, and still renders the hub

AssertionError: expected [ Array(5) ] to deeply equal [ Array(5) ]
-   "sys_permission_set",
+   "sys_permission",
TestingLibraryElementError: Unable to find an element with the text: 5 permissions
AssertionError: expected div …(1)/div to be null
+ Received:  permissions

Tests  4 failed | 20 passed (24)

#3679 落的错误态断言不受两个扰动影响的部分(500 单卡致盲、三行矩阵)全程绿,回归无扰动。

测试

仓库根跑(AGENTS.md §怎么跑测试),重活走共享 flock + --max-old-space-size=4096 + --maxWorkers=2:

消费半径清扫:全仓 system/permissions 的产出者与断言者只有本 PR 这四个文件(SystemHubPage.tsx:246 的 href + 路由 + 两个测试);packages/** 零命中,两个 sidebar 不产出 permissions 条目(#3655 正文即如此:这一条只由 System Hub 卡片产出)。hub-card-permissions testid 全仓只有 counts.test.tsx 断言。sys_permission_set 在本仓此前零引用。

文件面

⛔ 未触碰:ROADMAP.md(#3704 在途)、doc-version-claims ledger(#3708 在途)、两个 sidebar 文件、ShorthandRecordRedirect / looksLikeRecordId 的判定逻辑、packages/app-shell 的任何路由声明、sys_capability 相关的任何东西。

遗留(只报不改)


🤖 Generated with Claude Code

https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt


Generated by Claude Code

…n_set (#3655)

Fifth and last of the five `system/*` navigation targets. `system/permissions`
and the hub's Permissions count were both held back (PR #3673 / #3680) because
the framework splits this console's "Permissions" into `sys_capability`
(ADR-0066 layer 1, the definition registry) and `sys_permission_set` (layer 2,
the grant container the permissions docs call "the only capability container").

objectui#3655 decided it as `sys_permission_set`: the card reads "Manage
permission rules and assignments", and rules-and-assignments is layer 2.

- route: `system/permissions` -> `SystemObjectRedirect objectName="sys_permission_set"`
- count: `dataSource.find('sys_permission')` -> `'sys_permission_set'`
- the two MEASUREMENT pins written to be replaced when this landed are replaced;
  the length-split fact they also carried is re-pinned on segments that are
  still undeclared in both route tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercel Bot commented Aug 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 8, 2026 9:09am

Request Review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 28.1 KB 350 KB
Entry file index-BEAgjw06.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.66KB 3.13KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
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) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 480.72KB 105.64KB
core (index.js) 2.96KB 1.13KB
create-plugin (index.js) 9.08KB 2.92KB
data-objectstack (index.js) 137.51KB 35.11KB
fields (index.js) 230.90KB 56.84KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.65KB 1.06KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 9.48KB 3.27KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 4.52KB 1.96KB
layout (index.js) 38.53KB 10.71KB
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.74KB
mobile (index.js) 1.50KB 0.62KB
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) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.98KB 12.37KB
plugin-charts (index.js) 61.04KB 17.31KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 115.50KB 29.96KB
plugin-designer (index.js) 210.51KB 42.51KB
plugin-detail (index.js) 232.79KB 57.42KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 112.10KB 27.10KB
plugin-gantt (index.js) 162.55KB 39.57KB
plugin-grid (index.js) 186.61KB 49.34KB
plugin-kanban (index.js) 48.30KB 13.28KB
plugin-list (index.js) 105.12KB 25.48KB
plugin-map (index.js) 16.81KB 5.24KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 40.58KB 10.58KB
plugin-timeline (index.js) 25.76KB 7.33KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.03KB 20.55KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.71KB 1.34KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants