Skip to content

chore(ui-i18n): the call-site key gate refuses a lazy-namespace key read outside its namespace's frame - #30853

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sw8poh
Nov 6, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-sw8poh

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #30645

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:i18n 调用点门禁(check:i18n-keys)新增第 9 类 namespace-key-outside-frame:只存在于懒加载命名空间(目前是 marketplace)里的 key,如果在该命名空间的「框」目录之外被 t() 读取,门禁直接变红。
  • 为什么改:perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640 把大部分 marketplace.* 文案移进了懒加载包,只有两个市场页面会拉取它;别的页面(比如 Studio、Setup)一旦复用其中某个 key,用户会看到原始 key,而原有的各道 i18n 门禁都把命名空间当成常驻包,全部是绿的。
  • 风险与回滚:只改门禁脚本和它的测试,不发布任何包、不改运行时;当前树上 0 条新发现。回滚即 revert 本 PR。
  • 先看:ui/scripts/check-i18n-call-site-keys.mjs 的 PACK_NAMESPACE_FRAMES(命名空间与它的框声明在同一张表里)、analyze() 里的 judgeFrame 三处调用点,以及测试文件里新的 describe。
  • 两分钟验证:pnpm --dir ui check:i18n-keys,预期退出 0,并打印一行 Namespace frames: 1 pack-shaped namespace(s) — packages/i18n/src/namespaces/marketplace/ read 183 time(s) inside its frame。
  • 需要你做的:无,席位落地。

What changes

ui/scripts/check-i18n-call-site-keys.mjs:

  • The one list of pack-shaped namespaces now carries each namespace's frame: PACK_NAMESPACE_FRAMES maps the namespace directory to the source directories that render under the gate fetching it (marketplace: packages/app-shell/src/console/marketplace/). PACK_NAMESPACE_DIRS is derived from its keys, so every existing reader (collectEnKeys, check-i18n-en-drift.mjs, check-i18n-dead-keys.mjs) reads the same list, and a namespace cannot be added without its frame. analyze() throws on a namespace declared with an empty frame.
  • collectEnKeys also returns namespaceOf: which namespace each namespace-defined leaf lives in.
  • New class 9, namespace-key-outside-frame, HARD from day one (no baseline). A call site outside a namespace's frame is refused when it asks for a key that only that namespace answers: a literal key (plural forms included: refused when every form that resolves is namespace-only), a returnObjects subtree, or a template head with no resident leaf under it. A key the resident pack answers is never refused.
  • A live control: the run prints Namespace frames: with how many call-site keys each frame reads, and fails as a collapsed scan when a declared namespace's frame reads none.
  • Header, HINTS entry and its own failure paragraph for the class.

Division of labour with marketplaceNamespaceBoundary.test.ts (unchanged): this gate judges every file OUTSIDE the frame directories; the boundary test keeps judging the files INSIDE console/marketplace/ that a page outside the frame imports.

Tests

ui/scripts/__tests__/check-i18n-call-site-keys.test.ts, new describe a key only a namespace defines is refused outside its frame (objectstack#30645), 10 cases on synthetic repos and the real tree:

  • positive control: a namespace-only key spelled in packages/studio/src/page.tsx turns the class red, naming the namespace;
  • the same key inside the frame is silent and counted in framedReads; a resident key outside the frame is silent;
  • plural base, template head (namespace-only vs one a resident leaf answers), returnObjects subtree;
  • a second namespace (billing) declared with its own frame is judged with no other change;
  • a namespace declared with an empty frame throws;
  • one list read both ways (PACK_NAMESPACE_DIRS equals the frame map's keys, every frame non-empty);
  • on this tree: 0 findings of the class, and every frame reads at least one key.

Real-tree probe (one-off, file removed afterwards, not committed): a file packages/app-shell/src/console/Probe30645.tsx calling t('marketplace.title') made node scripts/check-i18n-call-site-keys.mjs exit 1 with [namespace-key-outside-frame] marketplace.title; with the file removed it exits 0.

Acceptance notes

  • Not judged by the new class, by construction: a key built by a helper or spelled in a table and passed to t() as a variable. That is the same blind spot class 1 (missing-key) has; the boundary test's literal scan still reads such spellings inside console/marketplace/.
  • A declared dynamic family whose head is resident but some of whose members are namespace-only is not judged member by member; the head is passed because a resident leaf answers it.

Verification

Head f4a22f9b, tree objectstack-ai/objectstack.

Derived gates: 20 of 20 exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 20 lines from the 2 changed files. All 20 exited 0, among them pnpm --dir ui exec vitest run scripts/ (under the verify lock: Test Files 170 passed, 2 skipped; Tests 4989 passed, 2 skipped; VERDICT command-exit 0). dispatch-gates --ran printed 20 derived famil(ies) accounted for — 20 run, 0 NOT-MEASURED.

Run beyond the derivation, all exit 0 on the final head: node scripts/check-sdui-manifest.mjs, pnpm --dir ui check:i18n-keys, pnpm --dir ui check:new-line-citations, pnpm --dir ui run type-check:scripts. The scripts type-check was red once on an earlier head (the new return field and the frame map were untyped in JSDoc) and is green on f4a22f9b.

Not run locally

marketplaceNamespaceBoundary.test.ts is unchanged. Locally it stops at import (@objectstack/spec/ui has no dist in this worktree), so it is NOT MEASURED here and Test UI runs it.

No published package changes (ui/scripts/ belongs to the private object-ui-monorepo root), hence skip-changeset.


Generated by Claude Code

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Nov 6, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#3 · os-tesla · session_01L9qibxW6qraU2Z3mwupUGp

维护者速读

  • 改了什么:i18n 调用点门禁 ui/scripts/check-i18n-call-site-keys.mjs 加第 9 类 namespace-key-outside-frame:一个只在懒加载命名空间(目前是 marketplace)里定义的键,如果在该命名空间的「框」目录之外被读,门禁直接红。
  • 为什么改:perf(i18n, app-shell)!: the marketplace pages' strings leave the eager language packs for a lazy marketplace namespace #30640 把市场页文案搬进懒加载命名空间后,框外读这类键会在界面上显示原始键名,而原有门禁把命名空间当成常驻包的一部分,看不出来。
  • 风险与回滚:只改门禁脚本和它的测试,不改产品代码;main 上 0 命中。每个命名空间必须连同它的框一起登记(PACK_NAMESPACE_FRAMES),所以以后再加命名空间会自动受管。回滚即撤本 PR。
  • 先看:PACK_NAMESPACE_FRAMES 的定义,和测试里那个让门禁变红的合成仓库正控。
  • 两分钟验证:cd ui && node scripts/check-i18n-call-site-keys.mjs 预期 exit 0,并打印 Namespace frames: 一行(marketplace 框读到 183 个键)。
  • 需要你做的:无,席位落地。
  • 席位意见:可合。

席位核对 — ACCEPT · head f4a22f9

  • 形态:2 个文件,+264/−18(门禁脚本与其测试)。check-governed-merges --branch origin/claude/project-thread-sw8poh:0/2 路径在受管面,282 行 ≤ 3000。Fixes #30645,目标 main,skip-changeset(ui/scripts/** 不随任何包发布)。与认领 6678221203 及修订 6678261512 一致。本 PR 由本席派出的卡线程实现,席位不写码。
  • 契约:Clause-②: no,Metadata-protocol: none。门禁收紧不是发布面,四个契约面都不碰,按 contract-review.md 由席位自读,不起第二个 agent。
  • 席位自己核对(读 diff):PACK_NAMESPACE_DIRS 现由 PACK_NAMESPACE_FRAMES 的键派生,analyze() 对缺框或框项不以 / 结尾的命名空间直接抛错,所以命名空间无法不带框登记;第 9 类在字面键(含复数形式,全部形式都只在命名空间时才算)、returnObjects 子树和模板前缀三处判,各自只在没有常驻叶子能答时才拒;框内读数按命名空间计数,某命名空间框内读数为 0 时整次运行失败(防止空集假绿)。提示文案明确禁止靠扩大框来消红。
  • 前提实测:dev 报告 6678939064(status done)。合成仓库正控变红;真实树上的临时探针 exit 1 后撤回为 0;ui scripts 测试套件 4989 过;派生门禁 20/20 exit 0,--ran 0 NOT-MEASURED。偏差:marketplaceNamespaceBoundary.test.ts(未改)本地缺 spec dist 未测,CI 的 Test UI 已跑并绿。
  • 门禁 check-runs:本 head 60 个,44 success、16 skipped、0 红;mergeable_state clean。
  • 落地条款:维护者常设令 「按 pm-dispatch 审过的pr你可以自主落地」(2026-10-30T14:20Z),本场 「ui 车道跟进一下 3小时内没更新的pr和已派发的issue,接手处理。」。中继 pr_ready,约一分钟后 automerge_enable。合并即关卡(Fixes),同笔摘 pm:dispatched。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 6, 2026 07:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 6, 2026
Merged via the queue into main with commit de0f692 Nov 6, 2026
61 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-sw8poh branch November 6, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

i18n gates: a marketplace.* namespace key read outside the marketplace pages' frame passes every gate and renders as a raw key

1 participant