Skip to content

fix(lint): translation-target-unknown 读取视图容器的默认 form.sections (#5415) - #5422

Merged
baozhoutao merged 3 commits into
mainfrom
claude/issue-5415-lint-container-form-sections
Aug 5, 2026
Merged

fix(lint): translation-target-unknown 读取视图容器的默认 form.sections (#5415)#5422
baozhoutao merged 3 commits into
mainfrom
claude/issue-5415-lint-container-form-sections

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5415

前提复核(改之前先证伪)

按正文在 origin/main(切 worktree 时 9fad07f66,issue 钉的是 b4872a868,两者之间未触 packages/lint)重跑了 showcase 复现:直接 import 真实的 examples/app-showcase/src/ui/views/contact.view.ts,把正确翻译 showcase_contact._sections.contact 的 bundle 喂给 validateTranslationReferences,输出与 issue 正文逐字一致:

rule:     translation-target-unknown
severity: warning
where:    locale "zh-CN" · object "showcase_contact" · section "contact"
message:  Translations are keyed to section "contact", which nothing on object
          "showcase_contact" declares — no `fieldGroups[].key`, no named form-view
          section, no named `record:details` section. ...
hint:     ... Object "showcase_contact" declares no named section at all.

前提成立。

改了什么

collectViewRecord 原本从四个锚点收集对象的合法 _sections 名:fieldGroups[].keylistViews.* / formViews.* 的具名段落、页面 record:details 的具名段落、以及视图记录自身的 sections。容器的默认 form(defineView({ list, form, formViews }) 里那个、没有指定具名表单视图时 ObjectForm 渲染的那个)哪一条都不是 —— 循环写死 ['listViews', 'formViews'],而它既不在其中、也不是记录自身的 sections,于是 view.form.sections[].name 对事实集零贡献

现在默认 form 走与 formViews.* 完全相同的段落收集路径,绑定取 bindingOf(view.form) ?? listBinding —— 即先 form.data.object、再记录级对象、最后旁边那个 list 的绑定 —— 与 CLI i18n walker 的 viewObjectNameview.form.data.object 的解析对齐。这正是 #5405 要建立的集合关系(覆盖走查器要求的 key ⊆ 引用校验器接受的 key)在 lint 侧的那一半。

顺带把四处段落收集收敛成一个 addSections 收集器:锚点清单从此是「一串调用点」,而不是四份会各自漂移的循环副本。

没有任何收紧:无名段落依旧不可翻译(它没有可查的稳定 key),任何锚点都没声明的 _sections key 依旧上报 —— 只是 hint 现在会列出真实存在的锚点,而不再断言「declares no named section at all」。

未触 _views 族(#5164 领地),未触 packages/lint/src/index.ts(PR #5416 在队列中触该文件)。

测试

packages/lint/src/validate-translation-references.test.ts 新增 4 例:

  1. 默认 form 的具名段落可解析(合成最小形状)。
  2. 默认 form 按自己的 data 绑定,而不是旁边的 list —— 一个 stack 里同时断言两个方向。只断言「crm_lead 仍被上报」是不可证伪的:在修复前它同样通过,因为默认 form 什么都没贡献(空集合让断言"通过"的经典陷阱);是 crm_contact 那一半让这一对有了方向。
      1. 真实 showcase 元数据(import { Contact } + import { ContactViews })。选真实 import 而不是手工缩写:缺陷本身就是「锚点清单漏了一项」,手写 fixture 只能钉住作者记得的锚点。而这个面恰好是暴露它的形状 —— 对象只声明 field.group、没有 fieldGroups[],所以默认 form 是它唯一的段落锚点。一例钉四个具名段落全部被接受;一例是防过度放宽的反例:contract(contact 的错拼)与 who_is_this(formViews.create 那个无名段落的 label)两个 key 仍各上报一条,且 hint 改口列出 Declared sections: contact, notes, status, work

按车道口径,两条真实元数据 import 的用例显式 }, 60_000)

反向验证(方向先预判,再跑)

预判:删掉新加的这一条 limb,4 条新用例应全红(这是纯放宽,不存在 #5018 那种反转,也不存在 #5046 那种「计数从 1 变 0 反而多出诊断」)。实测一致:

× resolves a section named on the container default `form` 12ms
× binds the default `form` by its OWN data, not by the list beside it 2ms
× accepts every section the default form names 3ms
× still reports a section name nothing declares, and names the real ones 5ms
AssertionError: expected [ { severity: 'warning', …(5) } ] to deeply equal []
AssertionError: expected 'Sections are translatable only throug…' to contain 'Declared sections: contact, notes, st…'
Tests  4 failed | 24 passed (28)

恢复 limb 后:

pnpm --filter @objectstack/lint test        →  Test Files 57 passed | Tests 1202 passed (1202)
pnpm --filter @objectstack/lint typecheck   →  tsc --noEmit (clean)
pnpm --filter @objectstack/lint build       →  DTS Build success
npx eslint <两个改动文件> --no-inline-config  →  clean
node scripts/check-nul-bytes.mjs            →  OK (5429 files)

消费半径已按规则的调用点(而非改动包)扫过:translation-target-unknown / validateTranslationReferences 仅由 packages/lint 内的 reference-integrity-suite + index.ts 消费,全仓 _sections 的 fixture 只出现在本规则的测试与 packages/spec 的 translation schema 测试里 —— 没有 #5046 那种跨包 fixture 需要同步。

关于 #5416

切 worktree 时 PR #5416(#5405)尚未合入 main,故按分诊口径跳过「走查器要求 ⊆ 校验器接受」的集合断言,不为它引入跨分支依赖;本修复不依赖 #5416#5416 落地后可在其上补该断言。

越界发现(已另开 issue,未在本 PR 修)

🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

claude added 2 commits August 5, 2026 10:57
…ult `form.sections` (#5415)

`collectViewRecord` derived an object's legal `_sections` names from
`fieldGroups[].key`, the named sections on `listViews.*` / `formViews.*`, a
page's named `record:details` sections, and the view record's own `sections`.
The view CONTAINER's default `form` — what `defineView({ list, form })`
declares and `ObjectForm` renders when no named form view is asked for — was in
none of those, so `view.form.sections[].name` contributed nothing: a bundle
correctly translating a heading that DOES render was reported as keyed to a
section "nothing declares", with a hint advising the author to delete it.

The default form now feeds the same section collector as `formViews.*`, bound
by `bindingOf(view.form) ?? listBinding` — the resolution the CLI i18n walker
performs on `view.form.data.object` — so the rule that demands a key and the
rule that accepts one agree on which object a heading belongs to. Every anchor
is now a call into one collector instead of its own copy of the loop.

Tests import the real showcase contact metadata (`Contact` +`ContactViews`):
its object declares `field.group` and no `fieldGroups[]`, so the default form
is its ONLY section anchor, which is exactly what a hand-reduced fixture could
not have pinned. Over-widening controls kept: an unnamed section is still
untranslatable, an undeclared key is still reported, and the default form is
bound by its own `data`, not by the list beside it.

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

vercel Bot commented Aug 5, 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)
objectstack Ignored Ignored Aug 5, 2026 11:03am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/lint.

3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/automation/hook-bodies.mdx (via @objectstack/lint)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint)
  • content/docs/releases/v17.mdx (via @objectstack/lint)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 11:08
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 11:08
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 20963e7 Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5415-lint-container-form-sections branch August 5, 2026 11:17
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…ck-ai#5420) (objectstack-ai#5438)

`src/ui/views/contact.view.ts` declares the whole "create form != edit form"
reference implementation — a default grouped `form` with four named sections,
a sparse `formViews.create`, and the list's
`addRecord: { mode: 'form', formView: 'create' }` binding — and
`src/ui/views/index.ts` exports it. `objectstack.config.ts` named the other
four containers on line 23 and line 196 and never named this one.

There is no directory scan behind `views:`; the CLI reads exactly that array.
So the metadata compiled, type-checked and linted clean while reaching
nothing: `nav_contacts` rendered a derived default form instead of the
authored one, `addRecord.formView` never bound, and no static pass
(`os validate` / `os lint` / `os i18n extract` / the coverage ratchet) could
see it either — which is also why objectstack-ai#5405's new `i18n/missing-section` gate
found 10 headings in the showcase and zero of them under `showcase_contact`.
`content/docs/ui/create-vs-edit-form.mdx` cites this file as the live
reference implementation the whole time.

Registration makes exactly five zh-CN coverage keys newly reachable, measured
on the real config rather than predicted:

  os lint examples/app-showcase/objectstack.config.ts
    483 warnings (before) -> 488 (registered, untranslated) -> 484 (translated)
    + i18n/missing-view     objects.showcase_contact._views.list.label
    + i18n/missing-section  objects.showcase_contact._sections.{contact,work,status,notes}.label

All five are translated in this commit, following PR objectstack-ai#5416's rework: zh-CN
takes its words from the vocabulary this bundle already uses, and `en` gains
nothing (the default locale is satisfied by the inline labels; echoing the
source string only fakes coverage). The ratchet baseline
`scripts/i18n-coverage-baseline.json` and `i18n.supportedLocales` are
untouched — `check-i18n-coverage: OK (12 config(s), 660 baselined
untranslated string(s), none new)`, showcase back to exactly 451.

Two guards in `test/seed.test.ts`:

  * every container the barrel exports reaches `stack.views`, matched by
    target object (`defineStack` parses the config, so a registered container
    is a structural copy and never `===` the export). Reverting the config
    hunk fails it naming `ContactViews`.
  * the four section names and their zh-CN `_sections` keys are asserted as
    set EQUALITY, because the two i18n gates read that set in opposite
    directions — `i18n/missing-section` fails on a declared section with no
    entry, `translation-target-unknown` on an entry no section declares.
    With objectstack-ai#5422 on main this is the first time the relationship is testable on
    the real config: the four correctly-translated sections are NOT reported
    as orphans.

The remaining `translation-target-unknown` on `_views.list` is objectstack-ai#5164's
open contradiction between the three `_views` producers, not new here — the
showcase already carried four identical instances (project / task / inquiry /
business_unit) before this change.


Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…e heading no key can address (objectstack-ai#5417) (objectstack-ai#5455)

`_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 (objectstack-ai#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 objectstack-ai#5416's walker and objectstack-ai#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 objectstack-ai#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.


Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

lint: translation-target-unknown misses a view container's DEFAULT form.sections — correctly translating a rendered heading is reported as a stale key

2 participants