Skip to content

fix(metadata-admin): 审计面板锁状态列补齐中文,词表键绑 lockState union (#5004) - #5023

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-5004-lock-vocab
Aug 17, 2026
Merged

fix(metadata-admin): 审计面板锁状态列补齐中文,词表键绑 lockState union (#5004)#5023
yinlianghui merged 1 commit into
mainfrom
claude/issue-5004-lock-vocab

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#5004

症状

zh-CN 管理员打开元数据编辑页的「审计」面板,凡是带锁的审计行,标题为「锁状态」的那一列显示裸英文 no-overlay / no-delete / full

根因:不是漏译,是整张词表串了行

CONSOLE_VALUE_ZH.lock 里装的是一套草稿状态词表(draft / locked / published / none),而它服务的字段是 ADR-0010 §3.6 的四态锁MetadataAuditEntry.lockState。三者对齐后是完全错位,不是部分缺失:

词表条目能否被命中
draft / locked / published对不上 lockState任何可能值
none唯一对得上的,却被调用点自己的守卫排除 —— AuditPanel.tsx:191ev.lockState && ev.lockState !== 'none' 为假时走 支路,永远不会问它
no-overlay / no-delete / full实际能到 translateConsoleValue 的三个值,一条条目都没有

命中率 0/3。translateConsoleValuevalue 声明为 string,所以类型检查对此毫无意见 —— 与 #4982overlayScope 被声明成 string 的同一个原因。

改动

  1. 补三条中文no-overlay → 禁止编辑、no-delete → 禁止删除、full → 完全锁定。用词跟随读者在同一屏上会读到的锁定横幅句子(engine.edit.lockNoOverlay 不可编辑 / lockNoDelete 不可删除 / lockFull 不可编辑或删除),压缩成适合窄列的状态名。

  2. 键绑生产者的 union(仿 objectui: MetadataLayered.overlayScope 词表与 spec 漂移(注释声称 organization/environment/package,生产者只发 org/env),LayeredDiff 把该原值当徽标直出 #4982 / PR fix(metadata-admin): overlayScope 收窄为 spec 派生词表,层徽标过 i18n #5005 落地的 LAYER_SCOPE_ZH):新表 LOCK_STATE_ZH 的键类型是 Record< NonNullable< MetadataAuditEntry['lockState'] >, string >。今后 union 加第五态时,这条记录编译不过并点名缺失的那个标签,而不是让界面静默漏出裸英文。

    键集选的是完整的 NonNullable(含 none,不是按调用点现实剔除 none 后的 Exclude< …, 'none' >。理由两条:一是与 PR fix(metadata-admin): overlayScope 收窄为 spec 派生词表,层徽标过 i18n #5005 已落地的形状一致;二是词表的完备性应当对着生产者的类型,而不是对着另一个文件里 JSX 三元表达式中的一个 !== —— 那个守卫是「未锁定显示破折号而非文字」的展示选择,它哪天改了,词表不该需要跟着改。none 因此保留一条,并在 docblock 里写明它目前经唯一调用点不可达。

  3. 删掉三条死条目(先量后删,这正是本卡记录的陷阱):

    • 全仓扫 translateConsoleValue('lock'仅 1 个调用点AuditPanel.tsx:193);
    • CONSOLE_VALUE_ZH 是模块私有 const,全仓只有两处引用:定义处与 translateConsoleValue 内那一次查表;
    • 6 个 translateConsoleValue 调用点的 group 实参全是字面量'op' / 'outcome' / 'lock' / 'layer'),没有变量 group 能间接读到;
    • MetadataLayered['lock'](同一套四态锁的另一个字段)由 ResourceEditPage.tsx:1935-1937 渲染,走的是 t('engine.edit.lockFull'…) 整句字符串,不读这张词表

    结论:确无第二调用点,三条删除。键绑定本身也强制了这一点 —— 多余键会触发 excess property check。

刻意未改

  • translateConsoleValue 仍是 zh-only(其余九个 locale pack 返回原值是既有设计);是否扩出去是独立决定,不搭车。
  • none / null 支路渲染与改前逐字一致。
  • 是否该由 @objectstack/spec 拥有这套锁词表:本仓今天手写这个 union,按分诊围栏不伸手 spec,问题留给维护者(见下)。

测试

新增 AuditPanel.lockState.test.tsx(9 例)。状态清单不是自由手写的,而是用 satisfies Record< LockState, 0 > 校验过的键集反推 —— 双向穷尽(少一个键报缺失,改名报多余),所以清单无法与 union 漂移。这里没有 @objectstack/specz.enum 可在运行时读 .options(union 是本仓手写的),故用 satisfies 技巧代替 #4982.options 数组。

pnpm exec vitest run packages/app-shell/src/views/metadata-admin/
Test Files 175 passed (175)
Tests 1735 passed | 1 skipped (1736)
pnpm exec turbo run type-check --concurrency=2
Tasks: 81 successful, 81 total
node scripts/check-control-bytes.mjs
OK (scanned 4479 tracked text file(s))

反向验证(先书面预判,再变异)

变异①:从 LOCK_STATE_ZH 删掉 'no-delete' 一条。 预判 tsc 红 + vitest 红(2 例)。这里刻意不同于 PR #5005 变异① 的「tsc 红、vitest 绿」先例:本 PR 的测试里写了那个编译期钉的运行时孪生(穷尽循环 + 逐状态渲染例),所以两侧都该动。实测与预判一致:

i18n.ts(4162,7): error TS2741: Property '"no-delete"' is missing in type
'{ none: string; 'no-overlay': string; full: string; }' but required in type
'Record< NonNullable< "none" | "no-overlay" | "no-delete" | "full" | null >, string >'.
AssertionError: CONSOLE_VALUE_ZH.lock has no entry for lock state 'no-delete'
AssertionError: expected span to be null // 屏幕上出现裸 no-delete
Tests 2 failed | 7 passed (9)

变异②:把 i18n.ts 整体退回 origin/main(保留测试)。 预判 vitest 红 5 例、tsc 绿。tsc 绿是这里最有信息量的一半:改前那个状态编译得干干净净,这正是缺陷能出厂的原因。实测与预判一致,红的是穷尽例 + 三个渲染例 + 死条目例,绿的 4 例恰好是三条不回归钉(none 破折号、null 破折号、override 星号)与 en-US 原值钉:

TSC_EXIT=0
AssertionError: CONSOLE_VALUE_ZH.lock has no entry for lock state 'no-overlay'
AssertionError: expected span to be null // no-overlay / no-delete / full 三例
AssertionError: expected '草稿' to be 'draft' // 死条目复活即红
Tests 5 failed | 4 passed (9)

留给维护者的开放问题(未自行决定)

这套四态锁词表是否应归 @objectstack/spec 拥有?现状是 MetadataLayered['lock']MetadataAuditEntry['lockState'] 在本仓 packages/data-objectstack/src/metadata-client.ts 手写两遍同一个 union(第 298 行与第 340 行,字面重复)。若 spec 出 z.enum,两处可派生、词表键可像 LAYER_SCOPE_ZH 一样绑到 spec,跨仓漂移也一并关闭。本 PR 按围栏不伸手 spec,绑到本仓 union;如维护者认可归属 spec,这里的键类型换一行即可。


Generated by Claude Code

…ckState (#5004)
`CONSOLE_VALUE_ZH.lock` held a draft-status vocabulary (`draft`/`locked`/
`published`/`none`) rather than the ADR-0010 §3.6 four-state lock, so the
alignment with `MetadataAuditEntry.lockState` was total rather than partial:
the three entries matched no value the field can hold; `none` — the only
match — is excluded by the call site's own guard, which renders an em dash
for an unlocked row; and the three values that do reach the helper
(`no-overlay`/`no-delete`/`full`) had no entry. Hit rate 0/3, so a zh-CN
admin read bare English in a column headed 锁状态.
Adds the three missing labels and, following #4982's `LAYER_SCOPE_ZH`, binds
the key set to the producer's own union via
`Record<NonNullable<MetadataAuditEntry['lockState']>, string>` — a fifth lock
state now fails `type-check` naming the missing label instead of shipping a
raw token. The union is this repo's hand-written one in
`@object-ui/data-objectstack`; whether `@objectstack/spec` should own this
vocabulary is left open rather than answered here.
The three draft-status entries are deleted after measuring: repo-wide,
`translateConsoleValue('lock', …)` has exactly one call site and
`CONSOLE_VALUE_ZH` is module-private, so nothing else could read them.
`none` is kept so the record stays complete over the producer's type instead
of tracking a guard in another file.
Unchanged on purpose: `translateConsoleValue` stays zh-only, and the em-dash
branch for `none`/`null` renders exactly as before.
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-I-haR3l1.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)500.23KB111.77KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.12KB60.43KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.22KB26.98KB
plugin-map (index.js)18.72KB6.09KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收 ✅ ACCEPT(#5004,批次 21 首单)

实物核验(merge-base b46cf11d6,已核含 PR5005 范式):3 files,+240/−4 —— LOCK_STATE_ZH 用词跟随同屏锁定横幅句子压缩成状态名;三条死条目按六点扫描证据删除(唯一 lock 组调用点 / 表私有无导出 / 六个调用点 group 全字面量 / lockState 全仓两文件 / 姊妹字段走整句 t() 不读此表 / fixture 零引用),且键绑定的 excess property check 本身强制删除。模型标识 msg/diff 双 0;releases/ 零触碰;changeset app-shell patch。

CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。

键集选型采信:选含 'none' 的完整 NonNullable 四键而非按 AuditPanel 三元剔除后的三键 —— 「词表完备性对着生产者类型,不对着另一个文件里 JSX 三元的一个 !==」这条理由成立(那个守卫是展示选择,改了不该连累词表),与 PR5005 落地形状一致,且 'none' 目前不可达已在 docblock 与测试显式写明。直接赋值比展开更严(少键报缺失、改名报多余)—— 比围栏要求的更好。

反向验证读数:变异①刻意不套用 PR5005 的「tsc 红 vitest 绿」先例并说明了为什么(本 PR 写了编译期钉的运行时孪生,两侧都该动)—— 同族先例被引用而不被机械照搬,实测 tsc TS2741 + vitest 2 红精确命中;变异②的「tsc 绿是最有信息量的一半 —— 正是缺陷能出厂的原因」读数到位,死条目复活即红的钉子在位。

open question 处置:四态锁词表的 spec 归属(A 长期 / B 中途站)已按围栏立 #5024(finding,observation-class,两份手写副本 + ResourceEditPage 无穷尽兜底横幅全部 file:line 在案)交维护者,未自行实施 —— 边界守住。undraft + auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 16:33
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit e71c854Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-5004-lock-vocab branch August 17, 2026 16:34
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@yinlianghui@claude