Skip to content

fix(metadata-admin): overlayScope 收窄为 spec 派生词表,层徽标过 i18n - #5005

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

fix(metadata-admin): overlayScope 收窄为 spec 派生词表,层徽标过 i18n#5005
yinlianghui merged 1 commit into
mainfrom
claude/issue-4982-overlayscope-vocab

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes#4982

前提复核(锚点在合并后 ref 上重定位)

基线 origin/main = 85fdb0612,#4016 的 PR #4988(cf4f8a6e4)已在其中。分诊要求的硬串行门已满足,三处锚点复核如下:

卡面锚点合并后 ref 坐标说明
metadata-client.tsMetadataLayered.overlayScope255(错注释)/ 256(声明)#4988 之前在 249,被该 PR 下推 7 行
LayeredDiff.tsx:196196,未移动
i18n.ts:4125(CONSOLE_VALUE_ZH.layer)4125,未移动

卡面三条事实逐条复核为真:

  1. 声明是 string | null,紧邻注释把词表写成 organization | environment | package;
  2. 装到本仓的 @objectstack/spec@17.0.0 里,GetMetaItemLayeredResponseSchemaoverlayScopez.enum(['org', 'env']).nullable() —— 实测该 schema 对上面三种拼法逐个拒绝,package 这个 scope 在此字段上从来没有过;
  3. 全仓 overlayScope 的取值面只有 null(23 处)与 'org'(4 处),没有任何一处写那三种拼法 —— 所以收窄不会打断任何现存调用点。

改动

契约侧(@object-ui/data-objectstack) —— 词表从 spec 派生,不在消费侧复述:

exporttypeMetadataOverlayScope=GetMetaItemLayeredResponse['overlayScope'];

以索引 spec 已发布响应类型的方式绑定,spec 日后加一个 scope 本仓自动跟随。复述一份局部 union 正是 check:spec-symbol-derivation 要拒绝的 fork 的镜像类,所以没有走那条路;别名随包导出(MetadataOverlayScope),字段与那条错注释一并改正。

因为原声明是 string,编译器对这个字段从来没有意见,那条错注释就成了它唯一的说明 —— 不是过时文档,而是给下一位读者(或 agent)埋的前提。

用户可见侧(@object-ui/app-shell) —— LayeredDiff 的 overlay 徽标此前把原值直出,而同一列表另外三个层徽标(artifact / none / merged)全部过 translateConsoleValue,且 CONSOLE_VALUE_ZH.layer 对该字段能取的两个值都没有条目。结果是同一个徽标的语言取决于数据:zh-CN 管理员打开任一 overlay 的元数据项读到裸英文 org / env,没有 overlay 的走 set 支路才读到「已设」。现在它与三个同伴一致过翻译,层词表补入「组织」/「环境」。

补的那两条不是手写键,而是以 spec union 为键:

constLAYER_SCOPE_ZH: Record<NonNullable<MetadataOverlayScope>,string>={org: '组织',env: '环境',};

spec 日后新增 scope 会在 type-check 处失败并点名缺失的标签,而不是让英文原值悄悄漏到界面上 —— 也就是让这个 bug 的形状在编写期就无法再成立。

禁区已守:translateConsoleValue 仍然只对 zh 生效(所有分组一致,其余语言恒返回原值),这是它的现有设计;是否扩到十个语言包是独立决定,本 PR 不搭车,并且专门留了一条 en-US 用例把「没有顺手改」钉住。

测试

  • pnpm exec vitest run packages/data-objectstack/ packages/app-shell/src/views/metadata-admin/ --maxWorkers=2214 files / 2274 passed, 1 skipped
  • 仓根 pnpm exec turbo run type-check --concurrency=281 successful, 81 total,零 TS 错误
  • node scripts/check-control-bytes.mjs(4465 个文件)、node scripts/check-spec-symbol-derivation.mjsnode scripts/check-changeset-no-major.mjs 全绿
  • 改动文件 eslint:0 error(仅既有 no-explicit-any / react-refresh warning,均不在本 diff 触及的行上)

新增两个文件,共 8 条用例。

关于「引用同一性 toBe」这一钉:此处不适用,如实记录

本仓最强的派生钉形态是 expect(local).toBe(spec)(见 packages/types/src/__tests__/spec-subschema-parity.test.ts —— 忠实的拷贝也会红,因为拷贝就是 fork)。这里没有可比的运行期见证:绑定是类型别名,产品代码只 import type,不引入任何 spec 。为了让一条 toBe 有东西可比而从本包导出一份 enum options 拷贝,恰好就是那个 guard 要拒绝的 fork,所以刻意不做。同等强度的替代是三条:

  1. 双向 satisfies(spec 成员全部流入别名 / 别名不比 spec 更宽)。本包 tsconfig.jsoninclude 覆盖整个 src(含测试),所以 type-check 真的编译它们 —— 是 enforcement,不是装饰(objectui#3009 的区分);
  2. 一条 @ts-expect-error:把收窄退回 string | null,'organization' 重新可赋值,该指令失去抑制对象,tsc 以 TS2578 报红。这是让本修复的回退变红的那一行;
  3. app-shell 的 LAYER_SCOPE_ZH 键类型 —— 编译期覆盖,见上。

app-shell 一侧的用例不写死 scope 清单,而是从 GetMetaItemLayeredResponseSchema 读 options 迭代,spec 新增的 scope 一存在就自动进入覆盖。

反向验证(先书面预判,commit 后变异,git checkout -- 还原,⛔ stash)

三条变异全部与预判一致,无一条需要改写预判:

变异预判实测
overlayScope 退回 string | null(保留别名)type-check 红:TS2578(@ts-expect-error 失效)+ _fieldIsAliassatisfies 失败;vitest 仍绿✅ 一致。tsc 恰好两条错误:metadata-client.overlayScope.test.ts(54,74): error TS1360: Type 'string | null' does not satisfy the expected type '"org" | "env" | null'(58,1): error TS2578: Unused '@ts-expect-error' directive;同一文件 vitest 2 passed
② 去掉 CONSOLE_VALUE_ZH.layer 里的 ...LAYER_SCOPE_ZHapp-shell 套件红:覆盖用例报 no entry for overlay scope 'org',两条渲染用例读到裸英文徽标✅ 一致。3 failed / 3 passed,首条错误逐字为 AssertionError: CONSOLE_VALUE_ZH.layer has no entry for overlay scope 'org': expected 'org' not to be 'org';两条渲染用例栽在 queryByText(scope) 上 —— 裸值确实在屏幕上
③ 徽标退回 layered.overlayScope ?? … 原值直出两条 zh 渲染用例红、覆盖用例仍绿✅ 一致。2 failed / 4 passed,红的正是 overlayScope=org / =env 两条

① 的预判方向值得单独说明:它不是「钉子变红」的常规方向。派生钉是编译期的,所以退回收窄后 vitest 会一直绿 —— 只有 type-check 会红,实测也正是如此(2 passed)。把它写成「测试变红」会是一份符合模板、读起来却像已验证的伪造,所以这里照实分开写。

②③ 合起来把这个 bug 的两半分开钉住:③ 只动渲染(词表在,徽标不查表)→ 覆盖用例仍绿;② 只动词表(徽标查表,表里没有)→ 覆盖用例也红。两半各有各的红。

还原后三个文件回到 HEAD,git status 干净,重跑两文件 8 passed

顺手发现(已另立卡,未在本 PR 修复)

同一张 CONSOLE_VALUE_ZH 表的邻组 lockMetadataAuditEntry.lockState完全错位(命中率 0/3:表里 4 条有 3 条对不上任何可能值,唯一对得上的 none 被调用点显式排除,而实际能送到的 no-overlay / no-delete / full 一条都没有):已按 Prime Directive #10 立为 #5004,未认领,未在本 PR 修复。它的修法与本卡不同(那套 4 态锁在本仓是手写 union,spec 没有可派生的 enum),所以不是本卡的子任务。


Generated by Claude Code

`MetadataLayered.overlayScope` 原本声明为 `string | null`,紧邻注释把词表写成
`organization | environment | package` —— 三种拼法生产者一个都不发。真实词表只在
`@objectstack/spec` 的 `GetMetaItemLayeredResponseSchema` 里:
`z.enum(['org','env']).nullable()`;framework `metadata-protocol` 两处赋值也只写
`'org'` / `'env'`。因为声明是 `string`,编译器对此毫无意见,那条错注释就成了字段
唯一的说明 —— 不是过时文档,而是给下一位读者(或 agent)埋的前提。
现在按仓内惯例从 spec **派生**而非消费侧复述:新增 `MetadataOverlayScope`,以索引
spec 已发布响应类型的方式绑定,spec 加一个 scope 本仓自动跟随。复述局部 union 正是
`check:spec-symbol-derivation` 要拒绝的 fork 的镜像类。
用户可见的那一半:`LayeredDiff` 的 overlay 徽标把该值原样直出,而同一列表里另外三个
层徽标(artifact / none / merged)全部过 `translateConsoleValue`,且
`CONSOLE_VALUE_ZH.layer` 对该字段能取的两个值都没有条目。于是同一个徽标的语言取决于
数据:zh-CN 管理员打开任一有 overlay 的元数据项读到裸的 `org` / `env`,没有 overlay
的走 `set` 支路才读到「已设」。徽标现在与三个同伴一致过翻译,层词表补入「组织」/「环境」。
该词表的 overlay-scope 部分以 spec union 为键(`LAYER_SCOPE_ZH`),所以日后 spec 新增
scope 会在 `type-check` 处失败并点名缺失的标签,而不是让英文原值悄悄漏到界面上。
`translateConsoleValue` 仍然只对 zh 生效(所有分组一致)—— 是否扩到其余语言包是独立
决定,不搭这趟车。
测试:词表派生钉(双向 `satisfies` + 一条 `@ts-expect-error`,把收窄退回
`string | null` 即以 TS2578 变红)、spec schema 对三种旧注释拼法的拒绝、LayeredDiff
在 zh-CN 下按 spec 选项逐个渲染中文徽标、en-US 保持原值、无 overlay 与无 scope 两条
支路不回归。
Fixes#4982
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-VAEW3OFl.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)498.86KB111.27KB
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)240.05KB60.05KB
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(#4982,批次 20)

实物核验(merge-base 85fdb0612 与报告基线一致):7 files,+282/−5 —— 类型经 GetMetaItemLayeredResponse['overlayScope'] 索引访问派生(零字面量复述,spec 动则自动跟);第二级派生把 Record<NonNullable<MetadataOverlayScope>, string> 作 zh 词表的,「spec 新增 scope 漏配标签」从界面事故变成编写期编译错;错注释改真实词表;徽标过 translateConsoleValue 与同组件三个层标签一致,?? 的 null 语义逐字保留。禁区守住(zh-only 未扩,en-US 反向钉在位)。模型标识 msg/diff 双 0;releases/ 零触碰;changeset patch×2。

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

反向验证读数:变异①的如实分类是本单最值得记的 —— 派生钉是编译期断言,回退后 vitest 永远绿、红在 tsc(恰好两条错误,TS1360 + TS2578),dev 明确拒绝把它写成「测试变红」的模板化伪装;②③把 bug 的两半(词表缺失 / 渲染直出)分开钉住各有各的红。「引用同一性 toBe 无运行期见证」一节的处理正确:type-only import 下为了凑 toBe 而导出 enum 拷贝恰是派生门要拒绝的 fork,双向 satisfies + @ts-expect-error 哨 + 编译期键覆盖是同强度替代,如实记录而非硬凑。

premise 复核更强:spec 对注释声称的三种拼法逐个拒绝、全仓取值面仅 null/'org',收窄零断点(仓根 type-check 81/81 亲证)。draft 先开、反向验证三格标 ⏳ 后回填实测的时序已披露且无残留占位,记账成立。finding #5004(CONSOLE_VALUE_ZH.locklockState 完全错位 0/3,同表邻组同失败类,zh-CN 审计面板今天可见)已由 PM 定级入队 —— 其键类型来源需独立判断、不照抄本卡形状的边界划分正确。

undraft + auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 14:53
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit 3d053bbAug 17, 2026
20 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4982-overlayscope-vocab branch August 17, 2026 14:54
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants

@yinlianghui@claude