fix(lint): 声明了 label 却没有 name 的表单段落 —— 永远无法被翻译的标题,现在会告警 (#5417) - #5455
Merged
Conversation
…e heading no key can address (#5417) `_sections` is keyed by the section's `name`, and every renderer resolves a section heading that way (`sectionLabel(objectName, section.name, authored)`), falling back to the authored label when there is no name. So a section authored with a `label` and no `name` is untranslatable by construction — and both existing gates are structurally blind to it: there is no orphan key for `validateTranslationReferences` to report, and the coverage walker (#5405) emits one expected key per `sections[].name`, so a nameless section demands nothing and the report reads 100%. Measured: 70/70 HotCRM form-view sections are in that state at full declared coverage; `os validate` over examples/app-showcase reports 14 (6 form views, 8 record:details pages) and still exits 0. `validateTranslatableSections` (`translation-section-name-missing`) joins the reference-integrity suite, so it runs on validate/lint/compile at once, and reads exactly the anchors #5416's walker and #5422's fact set already agree on — a container's `sections`, its DEFAULT `form.sections`, every `listViews.*`/`formViews.*` sub-container, the same three on object-embedded views, and `record:details` sections anywhere in a page's component tree via the shared `walkPageComponents`. fieldGroups-derived sections are out of range by construction (their key IS the name), as is a name-keyed `sections` map. Warning, and opt-in: a section warns only when the object it renders under carries some translation of its own, so a monolingual stack stays silent exactly as `computeI18nCoverage` does. The fix is a diagnostic at the producer — slugifying the label into a lookup key would fossilize a second de-facto contract next to the declared one (Prime Directive #12, and the renderer guard objectui#3373 pinned as correct). Per the PM ruling on the issue, requiring `name` on `FormSection` outright (the schema route) is a breaking authoring change left to the maintainer; packages/spec is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
baozhoutao
marked this pull request as ready for review
August 5, 2026 12:49
baozhoutao
enabled auto-merge
August 5, 2026 12:49
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5417
前提核验(先证后写)
按操作规程,先在
origin/main(5a45b9bf1)上用最小 fixture 实测,确认 issue 的前提今天仍然成立 —— 两侧都对无名段落零告警:crm_case有zh-CN翻译 + 三个{ label: 'Case' }/{ label: 'SLA' }/{ label: 'Resolution' }无name的 form section,validateReferenceIntegrity(stack)返回[]。collectExpectedEntries(fix(cli): i18n 覆盖走查收集objects.*._sections—— 段落标题不再是「声明了、渲染了、却零告警」的面 (#5405) #5416 落地后的走查),kind === 'section'的条目为[]—— 无名段落根本不产生ExpectedEntry,所以覆盖率读 100%。两个核验脚本是临时文件,已删除,未进入提交。前提成立,继续实现。
做了什么
新增规则
validateTranslatableSections(规则 idtranslation-section-name-missing),加入 reference-integrity 套件,因此os validate/os lint/os compile三个命令同时生效。判据即 PM 裁定的口径:声明了
label(或 detail 段落的title)而没有name的段落,在其所属对象有任何翻译时告警。为什么这是第三条腿:
_sections以段落name为键,渲染器也只按name查找(sectionLabel(objectName, section.name, authored),无name时回落到作者写的 label)。于是无名段落 —— 既不产生任何「孤儿键」让validateTranslationReferences报,也不产生任何「期望键」让覆盖率走查要 —— 是构造上不可翻译,而我们所有门禁对它结构性失明。锚集对齐
规则覆盖的 section 面与两处已落地实现严格一致(#5416 的 walker
section双源、#5422 的校验器锚集):sections、容器默认form.sections(lint:translation-target-unknownmisses a view container's DEFAULTform.sections— correctly translating a rendered heading is reported as a stale key #5415 补上的那个锚)、每个listViews.*/formViews.*子容器的sections;objects[].views、objects[].listViews);record:details段落 —— 复用共享的walkPageComponents,不自建副本(自建副本曾直接产出一条死规则,lint: no reference-integrity or option-key validation for app metadata #3583)。射程外且有理由:
fieldGroups派生段落的键就是fieldGroups[].key,天然有 name;sections写成按名索引的 map 时,map 的键就是 name。为什么是生产端诊断,而不是消费端容忍
诱人的「修法」是在渲染器把 label 转成 slug 去捞第二把键。那正是 Prime Directive #12 禁止的宽松回落:它会把
_sections.[label 的 slug]固化成与已声明契约并存的第二套事实契约,而且作者一改标题文案键就漂移。objectui#3373 已经把渲染器的name守卫钉为正确。所以缺陷在生产端,规则就放在生产端。提示里给出的 snake_case 名字只是建议作者写下来的名字,不是任何东西会去解析的键 —— 这句话写进了 hint 本身,免得 AI 作者反过来假设 label 就是键。严重级别与 opt-in
warning,与同族规则同档(ADR-0072 D1):不崩、不死,只是一个标题留在源语言。方案 2(收紧packages/spec的FormSection.name为必填)未搭车,按 PM 裁定属破坏性授权变更,留待维护者表态;本 PR 不动packages/spec、不动packages/cli。stack.translationsbundle 里有节点时才告警,与computeI18nCoverage的 opt-in 不变量一致。仓内真实锚(实测,如实报告)
PM 点名的 showcase
formViews.create无名段落确实命中。实跑pnpm --filter @objectstack/example-showcase validate:全配置共 14 条(6 条来自 form view,8 条来自
record:details页面),退出码 0 —— 只有error会 gate,warning走 advisory 通道,CI 不会因此变红。一处如实更正:
ContactViews(那个{ label: 'Who is this?' })从 views barrel 导出,但并未登记进 showcase config 的views:数组,所以它不在这 14 条里。测试里仍按 #5415 同款方式直接组合它(它在一个文件里同时给出「默认 form 四个段落都有 name」与「稀疏 create 覆写没有 name」的对照),另外补了一条用确实登记在 config 里的TaskViews的用例,免得「真实元数据」的说法被夸大。测试
27 条新用例,含:HotCRM 形状逐条告警断言、opt-in 门四态、8 个锚点各自双向断言(无名 → 本规则报 1 条;有名 + 已翻译 → 本规则静默且
validateTranslationReferences也接受,这就是「覆盖面必须一致」的共导断言,任一侧漂移就恰好红一半)、射程外四态、真实 showcase 两组。反向验证(先判方向后跑):预判为 红。删掉
collectViewSites里的默认form锚点分支 → 4 条用例转红,全部是expected [] to have a length of 3 but got +0/length of 1 but got +0,即该分支承重、断言不是靠「什么都没产生」而绿。已还原。消费半径扫查
规则跑在哪里,就扫哪里的 fixture(#5046 的教训:改在
packages/lint,坏的 fixture 在packages/cli的命令对等测试里)。packages/cli里authoring-rule-command-parity的formView用的是sections: []且无 translations;i18n-section-coverage走的是 fieldGroups 面。9 个消费半径内的 cli 测试文件全绿。Generated by Claude Code