You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 4a5396b
Browse filesBrowse the repository at this point in the historyBrowse files
fix(i18n): a tenant's rewrite of an extension-contributed validation rule message is served as written, on the refusal and on /meta (#27932)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D4iKDikdsAexd39K77jgeJh)_
domain:server#4 · os-elon-musk · session_016WV4cytRisTLYYDef35A9G
Fixes#27559
Clause-②: yes (widening) — `SchemaRegistry` (`@objectstack/objectql`)
gains the public read `getPackagedObjectBase(name)`
Metadata-protocol: semantic
## 维护者速读
- 改了什么:租户在 overlay 里改写了“扩展包”(objectExtensions)带来的校验规则提示语后,非默认语言下拒收报错和
/meta 仍回扩展包译文;现在两处都拿“owner + 代码扩展包”这一套打包规则做比较,改写的照原样显示,没改的照常翻译。
- 为什么改:#27543 只拿 owner
的声明比较,扩展包的规则无从比较,租户为自己用户改的话一换语言就变回扩展包译文;折叠里还会出现两条同名规则,拒收同时报两句。
- 风险与回滚:只扩 validations 一项(label/description 仍只认 owner,#8037 不受影响);折叠里
overlay 带的同名规则顶替扩展包原规则(仅当 owner 未声明该名),owner 与扩展包同名规则仍并存。回滚即 revert 本
PR。
-
先看:`packages/objectql/src/registry.ts`(`getPackagedObjectBase`、`tenantCarriedRuleNames`)、`packages/objectql/src/engine-validation-messages.ts`(`packagedRuleLookup`)、`packages/metadata-protocol/src/protocol.ts`(`getPackagedObjectBase`)。
- 两分钟验证:`pnpm --filter @objectstack/objectql exec vitest run
src/engine-validation-tenant-rewrite.test.ts
src/protocol-packaged-object-base.test.ts`,预期 16 passed:zh-CN/zh/en 下
/meta 与拒收是同一句,扩展规则改写为租户原句、只出现一次。
- 需要你做的:无,席位落地(欠一次契约复审,席位安排)。
## What changes
ADR-0029 D9.2a compares a served rule message with the packaged rule of
the same name. Since #27543 the packaged side was the OWNER's
declaration only, so a rule a second package contributes through
`objectExtensions` had nothing to compare with, and the catalog won over
a tenant's rewrite on both readers.
- **One packaged rule set, both readers.**
`SchemaRegistry.getPackagedObjectBase(name)` (new, beside
`getPackagedObjectOwner`) answers the packaged owner's declaration with
each CODE `extend` contributor's `validations` appended in fold order.
By reference when no code extender ships a rule; `undefined` exactly
where `getPackagedObjectOwner` is.
`ObjectStackProtocolImplementation.getPackagedObjectBase` (what `/meta`
hands `translateObject` as `packagedBase`) and `packagedRuleLookup`
(what the rule evaluator judges the refusal by) both read it; a registry
double without it is still read through `getPackagedObjectOwner`, the
owner alone. The comparison stays `valueOverridesPackagedBase` /
`scalarOverridesPackagedBase`, unchanged.
- **⛔ The object scalars' base is not widened.** Only `validations`
gains the extenders' entries; `label` / `pluralLabel` / `description`
stay the owner's, so an extension's label still reads as diverged and
keeps winning over the owner's catalog (#8037). Pinned.
- **The fold (design choice the card left open).** An extender's
top-level rule yields to a rule of the same name that the fold's BASE
carries and the `own` contributor does NOT declare
(`tenantCarriedRuleNames`, computed once over the base, the rule-name
mirror of `tenantAuthoredScalars`). On an overlay base those are the
tenant's copies of extender rules (a Studio GET → edit → PUT stores the
folded object), so the rule is served and evaluated once, as the tenant
last wrote it. Conservative edges, as for the scalars: no `own`
contributor, or a base that IS the owner (every no-overlay read and
`resolveOwnerLayer`), returns `undefined` and the fold concatenates
exactly as before; a name the owner declares itself is not counted, so
an owner rule and an extender rule sharing a name stay two rules
(pinned). Composes with #8027's `subtractExtenderContributions` (that
runs first, on `foldObjectExtendersOnto`).
- **Prose** (seat-granted surface amendment, prose only):
`packages/spec/src/system/i18n-resolver.ts` docblocks of
`TranslateDocumentOptions.packagedBase` and
`translateObjectValidations`; `content/docs/ui/translations.mdx`, the
validation-rule paragraph of "An edit beats the packaged catalog". No
predicate change in `packages/spec`.
- ⛔ `packages/rest/src/meta-item-read-gate.ts` is not edited (held by
#27897): its `packagedObjectBaseOf` already calls the protocol's
`getPackagedObjectBase`, which now carries the extenders' rules.
## Why
A tenant who rewords a rule an add-on package ships, for their own
users, saw it revert to the add-on's translation whenever a user's
locale differed from the default, and the refusal also carried the
add-on's original rule beside the tenant's.
## Verification
Card probe re-run on `main` `784dc9ae` (real `ObjectQL` engine +
`SchemaRegistry`; owner `px_duty` with no rules, an `extend` contributor
adding `ext_blocked` "Say what is blocking the duty.", a zh-CN bundle
entry, a tenant overlay; not committed). Locale zh-CN:
| Case | Reading | Before | After |
|:--|:--|:--|:--|
| no overlay | fold / `/meta` / refusal | 1 rule / `请写明阻塞原因。` /
`请写明阻塞原因。` | unchanged |
| overlay rewords `ext_blocked` to "Tell us what blocks this duty." |
fold | 2 rules (rewrite + appended original) | 1 rule (the rewrite) |
| | `/meta` | `请写明阻塞原因。`, `请写明阻塞原因。` | `Tell us what blocks this duty.`
|
| | refusal | `请写明阻塞原因。 \| 请写明阻塞原因。` | `Tell us what blocks this duty.`
|
| overlay carries `ext_blocked` untouched | fold / `/meta` / refusal | 2
rules / translated twice / translated twice | 1 rule / `请写明阻塞原因。` /
`请写明阻塞原因。` |
Pins (red first on `main`, 6 red; all green after):
- `engine-validation-tenant-rewrite.test.ts`: an overlay rewording the
extension rule is served and refused as written on zh-CN / zh / en,
folded once; an overlay carrying it untouched is folded once and
translated on both readers; the #27543 owner-rule rows unchanged (the
packaged-base assertion now also lists the extension rule). The refusal
reader now joins every violation, so a duplicated rule shows.
- `protocol-packaged-object-base.test.ts`: the packaged base appends the
extender's rules while `label` stays the owner's (#8037); by reference
when no extender ships a rule; unmoved by an overlay rewording an
extender rule; owner + extender same-name rules both still folded.
Ablations (each restored, suite green after):
- A1 `getPackagedObjectBase` answers the owner only → 4 red (both
readers).
- A2 fold yield off → 3 red (fold count, both readers).
- A3 `packagedRuleLookup` reads the owner only → 1 red (refusal reader
alone).
Suites: `@objectstack/objectql` full vitest 492 files / 9264 tests pass;
`@objectstack/rest` i18n override files (4 files / 109 tests) pass
against the rebuilt dists; `objectql` + `metadata-protocol` typecheck
pass. `dispatch-gates --commands` (107 families, derived on a tree
identical to head `5697abaa`) and `pnpm lint`: see the os-dev report
comment on #27559.
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed (docblock-only on a non-schema interface;
`check:docs` / `check:api-surface` green).
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_016WV4cytRisTLYYDef35A9G
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(i18n): a tenant's rewrite of a validation rule that an object extension contributes is served as written in every locale, on the refusal and on `/meta` (#27559)
8
+
9
+
Clause-②: yes (widening) — `SchemaRegistry` (`@objectstack/objectql`) gains the public read `getPackagedObjectBase(name)`
10
+
Metadata-protocol: semantic
11
+
12
+
#27221 made a tenant's reworded rule message beat the package's translation, but only for rules the object's owning package declares. A rule a second package adds through `objectExtensions` was compared with nothing, so when a tenant overlaid the object and reworded that rule, a reader in any locale the extension's bundle carries still got the extension's translation, on the rejected write's error and on the rule `/meta` serves.
13
+
14
+
-**Now.** A rule's packaged message is the message of whichever code package shipped that rule name: the owner, or a package that extends the object. Both readers compare against that one packaged rule set, so a tenant's rewrite of an extension rule is served and returned as written, and an extension rule the tenant left alone is still translated.
15
+
-**The fold is unchanged.** Validations stay additive (ADR-0029 D9.2a): when the tenant's overlay carries its reworded copy of an extension rule, the extension's own rule still runs beside it, and that copy, unchanged, is still translated.
16
+
-**`SchemaRegistry.getPackagedObjectBase(name)`** answers the packaged owner's declaration with each code extender's `validations` appended (by reference when no extender ships a rule; `undefined` where `getPackagedObjectOwner` is). Only `validations` widens: `label`, `pluralLabel` and `description` stay the owner's, so an extension's label keeps winning over the owner's catalog. `ObjectStackProtocolImplementation.getPackagedObjectBase` and the rule evaluator's packaged lookup both read it.
17
+
-**`@objectstack/spec`**: the `TranslateDocumentOptions.packagedBase` docs say what an object's packaged base now carries. No type or behaviour changes.
0 commit comments