Repository navigation
Commit de0f692
chore(ui-i18n): the call-site key gate refuses a lazy-namespace key read outside its namespace's frame (#30853)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} -->
_Requested by **Tesla** · [project
thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao4D7g8Jw4AoTpqTHJgB4GVG)_
Fixes #30645
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:i18n 调用点门禁(`check:i18n-keys`)新增第 9 类
`namespace-key-outside-frame`:只存在于懒加载命名空间(目前是 marketplace)里的
key,如果在该命名空间的「框」目录之外被 `t()` 读取,门禁直接变红。
- 为什么改:#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`.
<details><summary>Derived gates: 20 of 20 exit 0</summary>
`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`.
</details>
<details><summary>Not run locally</summary>
`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.
</details>
No published package changes (`ui/scripts/` belongs to the private
`object-ui-monorepo` root), hence `skip-changeset`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01L9qibxDqYaHT7GBjAY9FzB)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 2cf269f commit de0f692
2 files changed
Lines changed: 264 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
| 20 | + | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
| |||
302 | 303 | | |
303 | 304 | | |
304 | 305 | | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
| 402 | + | |
| 403 | + | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
305 | 420 | | |
306 | 421 | | |
307 | 422 | | |
| |||
0 commit comments