Repository navigation
Commit fdad178
fix(metadata-protocol,rest): a .custom pair's declared override is labelled in the ruled order on every /meta read (#30353)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wg6HN9TS7GFoRJmsKPyPbpwW)_
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
## 维护者速读
- 改了什么:客户在 `<基础包>.custom`
配对包里覆盖了基础包的页面、数据集、应用或书(仪表盘、视图、报表同样适用),改过的标签现在在所有 `/meta`
读上按裁定顺序显示:配对包自己的译文优先,其次是覆盖项自己写的文字,最后才是基础包的译文。
- 为什么改:以前页面、数据集、应用、书的覆盖项在非源语言下被基础包的译文盖住,客户改的名字看不到;七类覆盖项都不读配对包自己发布的译文。
- 风险与回滚:只有声明覆盖项多一次读,读的是配对包的 active translation
行,列表按基础包每次请求只读一次;不是覆盖项的条目没有新增读。回滚即撤本 PR,没有数据迁移。
- 先看:`packages/rest/src/meta-item-read-gate.ts` 的
`pairTranslationBundleOf` 和 `PACKAGED_BASE_ACCESSORS` 新加的四行,再看
`packages/metadata-protocol/src/protocol-discovery-and-bases.ts` 的
`getPairTranslationBundle`。
- 两分钟验证:`pnpm --filter @objectstack/rest exec vitest run
src/meta-dashboard-view-i18n-explicit-override.test.ts`,预期 108
条全绿;修复前的代码上有 19 条是红的。
- 需要你做的:无,席位落地。
Fixes #30062
Clause-②: yes (widening) — `ObjectStackProtocolImplementation` gains six
public methods, `getPackagedPageBase`, `getPackagedDatasetBase`,
`getPackagedAppBase`, `getPackagedBookBase`, `servedOverrideBaseOf` and
`getPairTranslationBundle` (measured on the built
`packages/metadata-protocol/dist/index.d.ts`); `@objectstack/rest`'s
exported `MetaBookTreeSources` accepts one more optional key,
`resolveProtocol` (measured on the built
`packages/rest/dist/index.d.ts`). REST's new `pairTranslationBundleOf`
is module-internal: it is not in the package's export list. Nothing
accepted is taken away.
Metadata-protocol: semantic
Skills-docs: none — the "Customizing an installed package (ADR-0146)"
paragraph of `skills/objectstack-platform/SKILL.md` says nothing about
how an override's labels are translated, so no sentence there turns
false; its stale served-type list is #30229's.
## What changes
The spec half (PR #30236, #30193) gave every document translator
`packagedBase` and `pairBundle`. Its "Server-half contract for #30062",
quoted:
> For a served item that is a declared `.custom` override, on item, list
and container reads alike, for all eight types:
>
> - `packagedBase` = the BASE package's item (pre-override: the document
the override replaces), never the override itself;
> - `pairBundle` = `{ locale: data }` built from that `.custom`
package's own translations only: its code-shipped translation bundle and
its authored `translation` rows, both of which are the pair's own;
> - keep passing `packagedBase` for object, dashboard, view and report
exactly as today, on every served item (`PACKAGED_BASE_ACCESSORS`,
`form-routes.ts` :385 :421): the object reads it for extension-folded
field labels too (ADR-0029 D9.2a, #27559), so it is not override-only.
Add the same accessor for page, dataset, app and book;
> - omit `pairBundle` for an item that is not a declared override.
This PR is that server half, with the seat's rulings on where the
contract does not fit the code (Deviations below).
**`packagedBase` for page, dataset, app and book.** Four protocol
accessors, `getPackagedPageBase` / `getPackagedDatasetBase` /
`getPackagedAppBase` / `getPackagedBookBase`, answer the base package's
own item through the same shadow-immune lookup
`getPackagedDashboardBase` / `ViewBase` / `ReportBase` use
(`packagedArtifactBase`, now typed over the seven artifact types). They
answer the base's item while the override is served, right after it is
published and after a restart, and `undefined` for an unknown name.
`PACKAGED_BASE_ACCESSORS` in the REST read gate gains the four rows, so
`GET /meta/:type/:name` and `GET /meta/:type` hand them to the
translators. `action` is now the one translatable type without a row.
**`pairBundle` for a declared override.** `servedOverrideBaseOf(type,
document)` names the base a served document overrides: an overridable
type, an `overrides.package`, and a served `_packageId` equal to it (the
envelope `protocol-meta-item-reads.ts` stamps on a served override).
Anything else, a base's own item included, answers `undefined`.
`getPairTranslationBundle(base)` reads that base's pair's own active
`translation` rows (`package_id` = `customizationPackageId(base)`)
through `readAuthoredTranslationLayer`, the same reader the i18n
service's authored layer uses (conversions, locale resolution and
bookkeeping stripped), and answers `{ locale: data }`, or `undefined`
when the pair has none, the id is empty or invalid, or the read fails.
The REST gate's `pairTranslationBundleOf` joins the two,
feature-detected on the protocol, and passes the result as `pairBundle`
on the item read and on every listed item. A list reads each base's pair
once per request.
**The book tree.** `createMetaBookTreeAnswer` forced `resolveProtocol`
to `undefined`, so `GET /meta/book/:name/tree` translated a book with no
packaged base. `MetaBookTreeSources` now takes the transport's
`resolveProtocol`, and the RestServer route passes it. The runtime
dispatcher's tree sources already spread it. The docs sidebar and the
book list now serve the same label (#26690).
## Deviations
- **`packages/rest/src/meta-item-read-routes.ts`** (+2): the book tree
route's sources, outside the dispatched surface; widened by the seat for
the tree fix above.
-
**`packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts`**
(+93, test only): the real-boot pins reuse #29613's sealed-boot fixture
there. They cannot live in `packages/metadata-protocol`: it does not
depend on `@objectstack/objectql` (objectql depends on it), so the
import would be a cycle. Widened by the seat.
- **`scripts/engine-double-contract.pinned.json`** (+5 / -1, generated):
`check:engine-double-contract` ratchets the two new `findOne` doubles
the new pins assert (`protocol.pair-translation-bundle.test.ts` 0 → 1,
`meta-dashboard-view-i18n-explicit-override.test.ts` 1 → 2), written by
`node scripts/check-engine-double-contract.mjs --write`. Coverage grew;
nothing else in the ledger moved.
- **No code translation source.** The contract names the pair's
"code-shipped translation bundle". A `.custom` package is authored,
never a code package, so it has none; and code bundles reach the i18n
service's static layer with no package provenance. `pairBundle` is built
from the pair's active `translation` rows only (seat ruling).
- **Seven types, not eight.** An object is extended, never replaced
(ADR-0146 D10): it carries no declared override, so
`servedOverrideBaseOf` answers `undefined` for it and no object read
takes a `pairBundle`. Its `packagedBase` is passed exactly as before.
- **Not covered here.** The view container read builds its slots without
the served-override envelope, so `servedOverrideBaseOf` answers
`undefined` there and the slots keep today's `packagedBase` order.
`form-routes.ts` (public form views) is outside the surface and is
unchanged: it still passes `packagedBase`, with no `pairBundle`.
- **A package-less authored translation** is in the deployment catalog
but is not the pair's, so it no longer outranks a changed label on a
declared override. That is the ruled order (6631250894: only the pair's
translation ranks first).
## Tests
- `packages/rest/src/meta-dashboard-view-i18n-explicit-override.test.ts`
(real `SchemaRegistry` and protocol over fake engine rows, through
`RestServer`): §1 now asks each of the eight accessor rows, the four new
ones included, and asks no accessor for `action`, `flow`, a plural and a
prototype key. New §5 over page, dataset, app, book and view: an
override's own label beats the base's translation (`zh-CN` and `en`); a
translation the pair ships wins in `zh-CN` while `en` reads the
override's label, on the item and the list read; no override reads the
base's translation; a member reads what an admin reads; the book tree
serves the label the book list serves for three seeds. 19 red on the
pre-fix code, 108/108 after.
-
`packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts`:
a real boot, page / app / dataset / book, with and without a metadata
service: each accessor answers the base's item before the override,
right after it and after a restart; `servedOverrideBaseOf` answers the
base on the served and listed items and nothing on the base's own;
`getPairTranslationBundle` is `undefined` before, then exactly the
pair's saved translation beside a package-less row of the same key, and
`undefined` for the pair's own id; fed to `translateMetadataDocument`
the three give pair → own → base. 16 new, file 87/87.
- `packages/metadata-protocol/src/served-override.test.ts`
(`servedOverrideBaseOf` table) and the new
`protocol.pair-translation-bundle.test.ts` (pair rows only, every read
naming `package_id`, bookkeeping stripped, `undefined` with no read for
an empty, invalid or pair id, `undefined` on a read failure): 15/15. Its
fake engine refuses any operator or combinator, holds the caller's
`limit` after the filter, and opens `findOne` with
`assertEngineFindOnePredicate`.
## Ablations
Through `scripts/ablation-replace.mjs`, each restored to blob == HEAD
with `git diff HEAD` empty. A metadata-protocol mutation was rebuilt and
proved in `dist/` with `ablation-dist-preflight.mjs` (exit 0) before the
objectql and rest legs, which read its `dist/`; after the last one a
clean rebuild was proved free of all three markers (`--absent`, exit 0).
| | Mutation | Red |
|:--|:--|:--|
| A1 | the four new `PACKAGED_BASE_ACCESSORS` rows deleted | rest 18/108
|
| A2 | `pairTranslationBundleOf` never fetches | rest 11/108 |
| A3 | the book tree route's `resolveProtocol` deleted | rest 1/108
(tree == list) |
| A4 | `servedOverrideBaseOf` always `undefined` | mp 1/15, objectql
8/87, rest 11/108 |
| A5 | the pair's `package_id` filter dropped | mp 2/15, objectql 8/87 |
| A6 | `getPackagedBookBase` answers `undefined` | objectql 4/87, rest
4/108 |
A5's first run left objectql green: the layer merges rows in name order,
and the package-less row sorted before the pair's, so its text never
showed. The row was renamed to sort after the pair's, and the pin now
goes red.
## Docs
`content/docs/ui/translations.mdx`, "An edit beats the packaged
catalog": the bullet "An edited string is served in the language it was
written in, to every reader" is false for a declared override whose pair
translates the string. It now names that exception, and a new paragraph
states the order for a declared override (page, dataset, app, book,
dashboard, view, report; the pair's translations first; only
`translation` items saved in the pair; the item, list and book tree
reads agree). `content/docs/concepts/metadata-lifecycle.mdx` and
`content/docs/api/metadata-api.mdx` were read sentence by sentence:
neither says how an override's labels are translated, so neither
changes.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CajcU31aZRQuQzwWj6)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent f672e0d commit fdad178
12 files changed
Lines changed: 868 additions & 35 deletions
File tree
- .changeset
- content/docs/ui
- packages
- metadata-protocol/src
- objectql/src
- rest/src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
| 221 | + | |
221 | 222 | | |
222 | 223 | | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
223 | 235 | | |
224 | 236 | | |
225 | 237 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
13 | 19 | | |
14 | 20 | | |
15 | 21 | | |
| |||
29 | 35 | | |
30 | 36 | | |
31 | 37 | | |
| 38 | + | |
32 | 39 | | |
33 | 40 | | |
34 | 41 | | |
35 | 42 | | |
| 43 | + | |
36 | 44 | | |
37 | 45 | | |
38 | 46 | | |
| |||
47 | 55 | | |
48 | 56 | | |
49 | 57 | | |
| 58 | + | |
50 | 59 | | |
51 | 60 | | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
52 | 68 | | |
53 | 69 | | |
54 | 70 | | |
| |||
247 | 263 | | |
248 | 264 | | |
249 | 265 | | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
250 | 320 | | |
251 | 321 | | |
252 | | - | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
253 | 325 | | |
254 | 326 | | |
255 | 327 | | |
256 | 328 | | |
257 | | - | |
| 329 | + | |
258 | 330 | | |
259 | 331 | | |
260 | 332 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
0 commit comments