Skip to content

check:i18n 应把 unknown-authoring-key lint 判为失败 —— 否则第十份 extract 配置还会照抄同一个错 #4804

Closed

Description

@os-zhuang

Blocked-by: #4736

由 #4736 的 dev 在实施中提出,PM 裁定采纳方案 A 并立单。#4803 合并后可派发。

背景

#4736 清掉了九份 scripts/i18n-extract.config.ts 里未声明的 name: 键。但那九份是同一个错被拷贝了九次 —— 说明后来者是照着第一份抄的,而没有任何东西拦住第一份。

现状:unknown-authoring-key lint 只打 warning(还带 dedupe,九个包只印一次),出现在一次全绿的 check:i18n 运行里。清掉现存九处之后,第十份新配置写错时,得到的仍然是同一条被人眼过滤掉的 warning。

采纳方案:A —— 在 scripts/check-i18n-bundles.mjs 里把 lint 签名判为失败

在该脚本里检测 CLI stdout 中 unknown-authoring-key lint 的签名(is not a declared stack key / is not a declared * key),命中则非零退出。约 5 行。

findConfigs 已经会自动发现 packages/ 下新增的配置,所以新包一落地就自动进保护,不需要维护清单。

为什么是 A,不是 B / C(PM 裁定,附两轴)

dev 给了四个选项,分析准确,我采纳其推荐并补充判据:

A(gate 脚本内判失败)

  • 长远合理性:✅ 唯一一个不扩大任何对外契约面的选项。它把「lint 已经看见的问题」变成「CI 会拦的问题」,落在 gate 脚本这一层,不碰 spec、不碰 CLI 的公开退出码。代价诚实说明:改变了 check:i18n 的 gate 语义(此前只判 bundle drift),需要在脚本的文档/错误信息里写清它现在还管什么。
  • 防 AI 写错:✅ 这正是「让错误在写的时候就被拒绝,而不是靠下游容忍」的形状。九份配置全写错、九次都只得到一条被过滤掉的 warning —— 这就是 AI 生成的 metadata 错误藏身并繁殖的地方。

B(让 os i18n extract 自己非零退出) —— 覆盖更广,但把一个内部卫生问题写进了公开 CLI 的退出码契约,会打断现有调用方,需要 changeset。这是面向用户的行为变更,不该顺手做;若将来真要做,应当单独立单并按公开契约变更处理。

C(ObjectStackDefinitionSchema 改 .strict()) —— 排除。爆炸半径覆盖全仓每一个 defineStack,而且 metadata-authoring-lint.ts 明确写了 schema 一旦 strict 这条 lint 就要自行静默 —— 等于用一次大迁移换一个小噪音,两头不讨好。

D(什么都不做) —— 排除。#4736 清的是症状;不装拦截器,第十份照抄是时间问题,而且下一次可能不是无害的 name:。

验收

  1. 一份含未声明 stack key 的 i18n-extract.config.ts 会让 pnpm check:i18n 非零退出,错误信息指名是哪个包、哪个键、以及"它在 load 时被丢弃"这个后果;
  2. 现有九个包(All nine i18n-extract configs author name: on defineStack — a key the lint drops at load, warning on every check:i18n run #4736 清理后)全绿,不产生任何新噪音;
  3. bundle drift 的原有判定不受影响(两类失败要能分辨,不要合并成一条模糊的错误);
  4. 反向验证必做:在 fix(i18n): 删掉九份 i18n-extract 配置里未声明的 name: 键 #4803 合并之前的 commit 上跑这个门禁,必须报出那九处;在 main 上必须干净。一个只在当前 main 上绿的门禁,无法证明它抓得到东西 —— 参见 check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690(check:react-declaration-parity 无 MANIFEST 时静默 skip 退出 0,永远不可能红)。

关联

Activity

  1. added
    toolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface
    pm:queueReady for the PM dispatch loop
    on Aug 3, 2026
  2. os-zhuang commented on Aug 3, 2026

    @os-zhuang
    ContributorAuthor

    Blocked-by: #4736 已解除 —— PR #4803 已合并,九处 name: 已从 main 清干净。本条现在可派发。

    两点落地时的提醒:

    1. 反向验证的目标 commit 现在是确定的:门禁必须在 fix(i18n): 删掉九份 i18n-extract 配置里未声明的 name: 键 #4803 合并前的 commit 上报出那九处(九个包各一处,packages/{platform-objects,plugins/plugin-approvals,plugins/plugin-audit,plugins/plugin-security,plugins/plugin-sharing,plugins/plugin-webhooks,services/service-messaging,services/service-realtime,services/service-storage}/scripts/i18n-extract.config.ts),在当前 main 上必须干净。这一步是本单的核心证据,不做等于交了一个无法证明有效的门禁。

    2. fix(i18n): 删掉九份 i18n-extract 配置里未声明的 name: 键 #4803 的 dev 已经确认过覆盖机制:findConfigs 是遍历 packages/ 发现配置的,所以新包一落地就自动进保护,不需要维护清单。这一点可以直接用,不必重新验证发现逻辑,但请确认你的失败判定挂在每一份被发现的配置上,而不是只看第一份(原 lint 有 dedupe,九个包只打印一次 —— 如果你的检测只看 stdout 有没有出现过一次这条签名,那是够的;但如果你想报出"哪几个包违规",要注意 dedupe 会吃掉后面八条)。

    第 2 点是我看这个 issue 时唯一担心的实现陷阱:原始噪音只出现一次,不代表只有一处违规。


    Generated by Claude Code

  3. added
    domain:devxDomain lane — seat card indexed by label:pm:seat
    on Aug 3, 2026
  4. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    认领:PM 循环第 9 轮
    会话:session_018iARDqtrhQgz6fVHDeDkbQ
    分支:claude/issue-4804-i18n-unknown-key-gate-fails
    Worktree:objectstack-issue-4804

    车道:domain:devx(维护者指定本会话独占,登记 #4604)。
    文件面声明(按新机制,供其他 PM 的全局在飞检查读取):scripts/check-i18n-bundles.mjs + 其旁测试。不碰 packages/cli。

    前置已解除 ✅

    Blocked-by: #4736 / 「#4803 合并后可派发」—— #4803 已合入 main(ffab8033b fix(i18n): drop the undeclared name: key from all nine i18n-extract configs)。九处现存违规已清,门禁现在上线不会即红。

    一处我先前判错、现已更正的记录

    我在 #4604 上曾把本条列为「落 packages/cli,伸进 os-zhuang 的文件车道,要压后派并等对方回复」。判错了:我从「unknown-authoring-key lint 的实现在 packages/cli/src/utils/i18n-extract.ts」推落点,没有读本 issue 采纳的修法。方案 A 落在 scripts/check-i18n-bundles.mjs,而且正文明确写了它刻意不碰 CLI 的公开退出码(那是被排除的 B)。按域表 scripts/(门禁类)= domain:devx,名实相符。

    这个错误恰是锚定规则要防的形状 —— 按「功能代码在哪」推,而不是按「本次修复落在哪」读。已在 #4604 更正,并撤回了对该条的压后承诺。

    全局在飞检查

    已列全仓 13 条 pm:dispatched 并比对文件面:无一触碰 scripts/check-i18n-bundles.mjs。本会话另两条在飞(#4777 → scripts/check-durability-degradation-log-level.mjs + baseline;#4731 → scripts/bump-objectui.sh)与之亦不相交,三者可并行。

    派发时会原样带上的两条

    1. 反向验证必做(issue 验收第 4 条):在 fix(i18n): 删掉九份 i18n-extract 配置里未声明的 name: 键 #4803 合并之前的 commit 上跑新门禁,必须报出那九处;在当前 main 上必须干净。要有真实命令输出。issue 自己点名了反面教材 check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 —— 那条门禁无 MANIFEST 时静默 skip 退出 0,永远不可能红,所以「只在 main 上绿」证明不了任何事。(顺带:check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 我刚按锚定规则改标为 domain:spec 交还 spec 车道。)
    2. 两类失败必须可分辨:unknown-authoring-key 与既有的 bundle drift 不能合并成一条模糊错误;错误信息要指名哪个包、哪个键,以及「它在 load 时被丢弃」这个后果 —— 否则又是一条被人眼过滤掉的输出,而本 issue 存在的全部理由就是那种输出没能拦住九次照抄。

    Generated by Claude Code

  5. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    PM 复核:通过(ACCEPT),等 CI 全绿后进合并队列

    PR:#4847(草稿,base main,body 首行 Fixes objectstack-ai/objectstack#4804)

    文件面(3 个,与派发声明一致):scripts/check-i18n-bundles.mjs(+318/-37)、根 package.json 一行、changeset。packages/** 零改动 —— 方案 A 的边界守住了,B(动 CLI 公开退出码)与 C(schema .strict())均未越入。

    反向验证,两个方向都有真实输出

    • 修复前语料红:把 ffab8033b^ 的九份配置恢复后跑新门禁 → exit 1,九个包逐一点名(包名 + 配置路径 + 键 + 「dropped at load — whatever it was meant to configure is NOT in effect, and never was」的后果 + did you mean 'pages'? 提示);
    • 基线证明这是新行为:同一九份语料跑未修改的门禁 → 九条 warning 照印、照样 exit 0 —— 这一条把「门禁此前确实拦不住」钉死了,不是嘴上说的;
    • 当前 main 干净:九个包零 UNDECLARED KEY 标记,exit 0。

    两类失败可分辨(验收第 3 条)

    单独 drift → 原有输出与 --write 修复指引不变;单独 unknown-key → 新 verdict 区块与「regenerating bundles will not fix it」的明确切割;两类同时发生 → 两个独立区块、两条独立修复路径,扫描行上还带 DRIFTED (1) <- UNDECLARED KEY 的并列标记。永不合并成一条模糊错误。

    实现中挖出的真根因,比 issue 的假设深一层

    issue 假设 lint 签名在 stdout。实际:lint 写的是 stderr,而旧门禁用 execFileSync —— 只有抛异常路径才附带 stderr,零退出的运行里那条流直通终端、没有任何代码读它。 也就是说旧门禁不只是「不判失败」,是结构上根本看不见那条警告。已换 spawnSync 双流捕获(非 lint 的 stderr 行原样转发,不吞诊断)。这解释了九次照抄为什么一次都没被拦 —— 与 #4690(审计脚本以为目录不存在)、docs-accuracy(路径全失效)同族:看的动作在发生,看的对象不在那里。

    反 #4690 保险做在了正确的位置

    签名正则失配时不是静默降级为绿:改写过措辞的行落进 unattributed 桶照样判红,且 self-test 里钉了 ffab8033b^ 的逐字 stderr 行 —— CLI 那边改了措辞,先红的是这条测试,而不是门禁悄悄失效。--self-test 已接在 check:i18n 的真跑之前,「这门禁能不能红」本身进了 CI。

    我派发时的 dedupe 警告,前提反了 —— dev 验证后如实纠正

    我警告「lint 带 dedupe,九个包只印一次,数行数会漏报」。实测:dedupe 是单进程内按路径去重,而门禁每个配置各起一个进程,九个包真的印九条。dev 没有按我的错误前提绕路,而是先验证再实现 —— verdict 本来就按发现的配置归属,两种前提下都正确。记这一笔:派发约束也该被验证,不该被盲从。

    CI 剩 4 项在跑、无红。全绿后取消草稿进合并队列。#4843(隔壁 objectui-range.mjs 同缺陷)在 #4842 落地后派发,修法将复用其导出的解析函数。


    Generated by Claude Code

  6. xuyushun441-sys commented on Aug 3, 2026

    @xuyushun441-sys
    Collaborator

    结案 — PR #4847 已合入 main

    审核通过并合入。记录几点这轮定下的判断,便于以后回看:

    根因不是正则写错,是 gate 结构上看不见它的判据。 check-i18n-bundles.mjs 用 execFileSync 取 lint 输出,而 lint 把未声明 key 的明细写在 stderr —— execFileSync 只在抛错路径上把 stderr 递出来,正常退出时那段文字压根没人读过。所以这个 gate 一直在"跑",一直是绿的,判据却从未被喂进去。已按方案 A 换成 spawnSync,stdout/stderr 一并解析。

    这与 #4690(没有 MANIFEST 也 exit 0)、.claude/workflows/docs-accuracy-audit.js(11 条路径指向已改名目录,静默跳过整个 protocol/kernel)是同一族缺陷:看似在运行、结构上却接触不到检查对象的检查。只观察到绿的 gate,和一个什么都匹配不上的 gate,从外部无法区分 —— 这也是为什么本 PR 要求了双向证明(在已知坏提交上必须红)。

    unattributed 桶是刻意留的反-#4690 保险。 凡是命中 UNDECLARED_KEY_SIGNATURE 却没能被行正则归因到具体文件的输出,不会被静默丢弃,而是进 unattributed 并让 gate 出声。将来 lint 改了输出格式,结果是 gate 报"有 N 条读不懂的违规",而不是悄悄变成永远绿。

    一处我自己的判断纠正:我最初把本 issue 归到 domain:cli,理由是"lint 在那边" —— 这是从议题措辞猜落点,正是 domain 锚定规则(落点 = 修复实际落在的那个包,triage 时读代码定,不从标题词汇猜)明令禁止的做法。实际落点是 scripts/check-i18n-bundles.mjs,即 domain:devx。已在 #4604 与本 issue 上更正。


    Generated by Claude Code

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

Metadata

Metadata

Labels

domain:devxDomain lane — seat card indexed by label:pm:seatpm:dispatchedDispatched to a dev agent by /pm-dispatchpm:queueReady for the PM dispatch looptoolingFix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions