Repository navigation
Commit 5bb7834
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
<!-- claude-projects-attribution: {"github_login":"os-project-manager"}
-->
_Requested by **Leehom** · [project
thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XKpnR4hU1b8DuBmGsW5R8jA)_
Fixes #28746
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28778
## 维护者速读
- **改了什么**:覆盖写入闸门(ADR-0146 D2)对邮件模板按「名称 +
语言(locale)」判断,而不是只按名称。`BASE.custom` 声明覆盖一个基础包根本没发的语言(比如基础包只发
`en-US`,却声明覆盖 `fr-FR`)现在在保存、单项发布、`os validate` / `os
build`、install-local 四个入口一致拒绝 `403 NOT_OVERRIDABLE`,提示「去掉 `overrides`
作为自己的语言保留」。
-
**为什么改**:原来闸门只问「基础包发不发这个名字」,任何语言都算发了,于是一个其实是新增的语言冒充覆盖上线,升级时漂移检测和「恢复包原版」找不到可比的基础成员。
- **风险与回滚**:中低。另有一处放宽:在配对包里不带 `overrides`
新增一个基础包没发的语言,原来被误拒(`undeclared_same_name`,叫作者去声明一个不存在的覆盖),现在放行;依赖包里同名、不同语言的模板同理。已存的错标行照旧被读出,下次带
`overrides` 保存才会被拒。回滚即撤销本 PR。
-
**先看**:`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(新的
`overrideWriteMemberOf` / `overrideWriteIdentityOf`,以及
`overrideWriteVerdict` 按成员查
shippers);`packages/cloud-connection/src/marketplace-install-local-plugin.ts`(`listedIdentity`
改走同一个身份函数);`packages/qa/dogfood/test/email-template-override-unshipped-locale.dogfood.test.ts`。
- **两分钟验证**:`pnpm --filter @objectstack/dogfood exec vitest run
test/email-template-override-unshipped-locale.dogfood.test.ts`,预期 4/4
通过(main 上 3 红:`fr-FR` 覆盖保存和发布都答 200,`fr-FR` 新增被拒 403)。
- **需要你做的**:无,席位落地。
## What changes
**The seam.** An email template is one item per `(name, locale)`
(`ITEM_KEY_DISCRIMINATORS` in `@objectstack/metadata-core`, the table
the registry keys bundles on and the read side serves a member by). The
override write gate counted which closure packages ship the template's
NAME, across every locale, so "the base ships this item" held for any
locale. Every door now counts at the member the body addresses, through
one helper.
-
**`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`.**
- `overrideWriteMemberOf(type, item)`: `itemDiscriminator` over the
type's singular, so the canonical member for a body that names none, and
`undefined` for every type whose identity is its name. Read from the
identity table, never a type literal; `email_template` is the only entry
today (measured: `ITEM_KEY_DISCRIMINATORS` at `7a48bf73` holds one key).
- `overrideWriteIdentityOf(type, identity, item)` (exported): an item's
identity (its name; an action's `objectName:name`) joined with its
member for a pair-identified type, unchanged otherwise.
`overrideWriteItemsOf` spells `OverrideWriteItem.identity` through it,
so `os validate` (`packages/cli/src/utils/override-declarations.ts`,
unchanged source) and the install door's incoming items compare on it.
- `overrideWriteVerdict` (the save door, the per-item publish and the
package publish all ask it) looks the registry up at the body's member
and drops an artifact of another member, the guard
`mergeArtifactProtection` applies on the read side for a registry that
ignores `member`.
- `judgeOverrideWrite`: unchanged rules and order; the
`declared_package_ships_no_such_item` sentence names the member for a
pair-identified type ("ships no email_template named 'X' with locale
'fr-FR'"). Every other type's sentence is byte-identical.
- **`packages/metadata-protocol/src/protocol-artifact-lookup.ts` and
`protocol.ts`.** The private `shippedArtifactsOf` takes an optional
`member` and passes it to both of its `lookupArtifactItem` calls.
Omitted, the lookup is what it was, so the lock's artifact layer (a lock
binds the NAME) is untouched. The published `.d.ts` lists the private
member without a signature, so the surface does not move.
-
**`packages/cloud-connection/src/marketplace-install-local-plugin.ts`.**
`listedIdentity` (an installed package's listed items) returns
`overrideWriteIdentityOf(...)`, so an installed base's `en-US` template
and an incoming `fr-FR` one are two items at this door as at the publish
doors.
- **Every door that asks the gate.** Save door, per-item publish
(`?member=`), package publish (`promoteDraftForPublish` judges the draft
body), `os validate` / `os build`, install-local: each computes its
`shippers` at the same identity, so the four answers match. No door was
found that cannot know the member: the save is body-addressed, and every
caller hands the gate the body.
- **Changeset** `.changeset/28746-override-gate-bundle-member.md`:
`minor` on `@objectstack/metadata-protocol` and
`@objectstack/cloud-connection`, both declaration lines, ADR-0087
disposition `not-required (no-migration-prescription)`.
**Narrowed:** a pair's declared override of a locale the base does not
ship is refused `403 NOT_OVERRIDABLE`
(`declared_package_ships_no_such_item`). **Widened:** an undeclared
locale the base does not ship, in the pair or in a dependent, collides
with no base member and is admitted; on `main` it was refused
`undeclared_same_name`. **Unchanged:** a declared override of a shipped
locale; an undeclared item at a shipped locale (still
`undeclared_same_name`); every non-bundle type.
## Tests
- **Red first, at base `7a48bf73`** (dists built from base): the dogfood
file failed 3 of 4 (the `fr-FR` override saved `200` and its draft
published `200`, served as an override; the `fr-FR` addition was refused
`403` `undeclared_same_name`); the CLI cases failed 2 of 2 (no row for
the declared `fr-FR`; an `undeclared_same_name` row for the `fr-FR`
addition); the install-local cases failed 2 of 2 (the `fr-FR` override
installed `200`; the addition was refused `403`). The `en-US`
shipped-locale control was green on base.
- **Green** at the head below: dogfood 4/4, CLI file 10/10,
install-local file 8/8.
- **New pins.**
-
`packages/qa/dogfood/test/email-template-override-unshipped-locale.dogfood.test.ts`
(showcase, HTTP): `PUT ?mode=draft&package=com.example.showcase.custom`
of `showcase_task_done_email` at `fr-FR` with `overrides` → `403
NOT_OVERRIDABLE`, no pair row, `GET ?member=fr-FR` 404; a draft of that
shape stored before the gate → `POST …/publish?package=…&member=fr-FR`
`403`, the row stays a draft, `fr-FR` still 404; controls: `en-US`
declared → 200, published, served; `fr-FR` without `overrides` → 200,
published, served.
- `packages/cli/src/utils/override-declarations.test.ts`: the pair's
`fr-FR` declaration → `declared_package_ships_no_such_item`; controls:
`en-US` declared plus `fr-FR` undeclared → no row; a dependent's
template collides only at `en-US`.
-
`packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts`:
the pair's `fr-FR` declaration → `403`, nothing registered, no ledger
entry; control: `en-US` declared plus `fr-FR` undeclared → `200`.
- Non-bundle control: every existing pin of the gate for `view` /
`dashboard` / `page` (metadata-protocol
`protocol.packaged-base-refusal.test.ts`, objectql
`protocol-custom-override-effective-read.test.ts`, the CLI and
install-local files) is unchanged and green.
- **Pin moved.** `protocol.lookup-artifact-item-call-sites.test.ts`
records the literal arguments of each `lookupArtifactItem` call; the two
rows in the seam module now read `type, name, undefined, member` and
`type, name, packageId, member`, with the reason.
<details><summary>Ablation (one-off, from committed HEAD
c5b0757)</summary>
- **Member dropped from the gate's subject.** `node
scripts/ablation-replace.mjs` replaced `return
itemDiscriminator(PLURAL_TO_SINGULAR[type] ?? type, item);` in
`overrideWriteMemberOf` with a guarded `return undefined` (marker
`ABLATE_28746`): anchor 1 → 0, blob `5b2921c51bef` → `162d86a93c62`.
metadata-protocol and cloud-connection rebuilt;
`ablation-dist-preflight.mjs @objectstack/metadata-protocol
ABLATE_28746`: marker present in 2 built files, exit 0.
- **Readings.** CLI file: 2 failed / 7 passed (the `fr-FR` declaration
and the combined `en-US` + `fr-FR` control). install-local file: 2
failed / 6 passed (the same two). Dogfood file: 3 failed / 1 passed (the
save refusal, the publish refusal and the `fr-FR` addition; the `en-US`
shipped-locale control stayed green). So dropping the member reds the
refused cases AND the undeclared-addition controls, because on `main`
the addition was refused too (Acceptance notes); no other case moved.
The CLI file's dependent pin was added after this run and was not
ablated.
- **Restored.** `git checkout HEAD --` the file: `git diff HEAD` empty,
blob equals HEAD `5b2921c51bef`; both packages rebuilt; preflight
`--absent`: marker absent from all 24 built files, tree clean, exit 0.
</details>
## Acceptance notes
- **Premise half falsified: the addition was not unaffected.** The order
listed "a new member added in the pair without `overrides`" as
unaffected. Measured on base, it was refused `undeclared_same_name` at
the save door, `os validate` and install-local, with a sentence telling
the author to declare an override of a member the base does not ship.
Under the ruled identity `(type, name, member)` it collides with
nothing, so this PR admits it, and declares the widening (`Clause-②: yes
(narrowing)`).
- **Dependent packages follow the same identity.** A dependent's
template of a name its base ships in `en-US` only is refused at `en-US`
and admitted at `fr-FR`, as the identity table reads it (pinned in the
CLI file). Flagged for the contract review: it is the mechanical
consequence of the ruling, not a separate decision.
- **At rest (ADR-0087).** Measured on the showcase with a pair `fr-FR`
override row laid down before the gate: `GET ?member=fr-FR` serves it
with `_packageId: com.example.showcase` and its `overrides` block, and
the layered read answers `code: null`; `en-US` is unaffected. Its next
save carrying `overrides` is refused with the member-naming sentence;
saved without `overrides` it publishes as the pair's own `fr-FR`, served
under `com.example.showcase.custom`. Nothing re-judges the row at boot
or upgrade; if the base later ships `fr-FR`, the row becomes a real
override. No conversion entry: no key changes shape. The read serving
such a row under the base's package id is dormant once the gate holds
(no new row can be written) and is not filed.
- **Overlap.** Open PR #28728 edits
`marketplace-install-local-plugin.ts` (step 2e, the field-patch check);
its hunks are at the import block's metadata-core list and near `:2120`
/ `:3287`, none of the lines this PR touches (`:243`, `:579`).
## Verification
All readings at head `e30f9fc8` (this branch merged with `origin/main`
`5493b884`, which carries #28412's flow-override edits to
`protocol-packaged-base-refusal.ts` and `protocol.ts`; auto-merged),
after `pnpm install --frozen-lockfile` and a rebuild of the dogfood
closure plus cli and cloud-connection (`BUILD_EXIT=0`).
<details><summary>Package tests and typecheck</summary>
- `pnpm --filter @objectstack/metadata-protocol test`: 294 files passed
(4 skipped), 22022 tests passed.
- `pnpm --filter @objectstack/cloud-connection test`: 66 files, 805
tests passed.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: 365
files, 5178 tests passed. The `integration` tier is declared to CI (the
diff touches no spawn entry).
- `typecheck` for metadata-protocol, cloud-connection, cli (with
`check:test-typecheck: OK`, ledger held) and dogfood: exit 0.
- objectql consumers of the gate,
`protocol-custom-override-effective-read.test.ts`,
`protocol-packageless-overlay-migration.test.ts`,
`registry-i18n-bundle-key.test.ts`: 3 files, 42 tests passed (pre-merge
head `c5b07573`).
</details>
<details><summary>Full dogfood battery (Clause-② narrowing
arm)</summary>
- `pnpm --filter @objectstack/dogfood test` under the verify lock, no
shard, no path: VERDICT command-exit 0; 375 files passed (3 skipped),
3038 tests passed (28 skipped).
</details>
<details><summary>Gate sweep</summary>
- `node scripts/pm/dispatch-gates.mjs --commands`: 69 families at
`e30f9fc8`; every one exit 0. Three first answered `PREREQUISITE NOT
MET` (exit 3: the teardown self-test's pinned commit absent from the
shallow clone; `check:type-check-debt` queued behind the dogfood run;
`check:dual-build-cjs-loads` before a full build) and were re-run green:
the teardown self-test after fetching its pinned commit,
`check:type-check-debt` under the lock, `check:dual-build-cjs-loads`
after it (115 require entry points across 70 packages load).
- `dispatch-gates --ran`: 69 derived, 69 run, 0 NOT-MEASURED, 0 UNRUN.
The per-family exit codes are in the seat's project files
(`pm-orders/server/28746-gates-ran.txt`).
- Every family CI runs on this PR is read from its check-runs, not
re-run here.
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 56fdd56 commit 5bb7834
10 files changed
Lines changed: 310 additions & 19 deletions
File tree
- .changeset
- packages
- cli/src/utils
- cloud-connection/src
- metadata-protocol/src
- qa/dogfood/test
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
137 | 181 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
236 | 267 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
| |||
581 | 581 | | |
582 | 582 | | |
583 | 583 | | |
584 | | - | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
| 588 | + | |
| 589 | + | |
| 590 | + | |
585 | 591 | | |
586 | 592 | | |
587 | 593 | | |
588 | 594 | | |
589 | 595 | | |
590 | 596 | | |
591 | | - | |
| 597 | + | |
592 | 598 | | |
593 | 599 | | |
594 | 600 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
100 | | - | |
| 100 | + | |
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
105 | 105 | | |
106 | | - | |
| 106 | + | |
107 | 107 | | |
108 | 108 | | |
109 | 109 | | |
110 | | - | |
| 110 | + | |
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| 50 | + | |
50 | 51 | | |
51 | 52 | | |
52 | 53 | | |
| |||
266 | 267 | | |
267 | 268 | | |
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 | + | |
269 | 304 | | |
270 | 305 | | |
271 | 306 | | |
| |||
287 | 322 | | |
288 | 323 | | |
289 | 324 | | |
290 | | - | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
291 | 330 | | |
292 | 331 | | |
293 | 332 | | |
| |||
350 | 389 | | |
351 | 390 | | |
352 | 391 | | |
353 | | - | |
| 392 | + | |
| 393 | + | |
354 | 394 | | |
355 | 395 | | |
356 | 396 | | |
| |||
406 | 446 | | |
407 | 447 | | |
408 | 448 | | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
409 | 452 | | |
410 | | - | |
| 453 | + | |
411 | 454 | | |
412 | 455 | | |
413 | 456 | | |
| |||
438 | 481 | | |
439 | 482 | | |
440 | 483 | | |
441 | | - | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
442 | 487 | | |
443 | 488 | | |
444 | 489 | | |
| |||
480 | 525 | | |
481 | 526 | | |
482 | 527 | | |
483 | | - | |
| 528 | + | |
484 | 529 | | |
485 | 530 | | |
486 | 531 | | |
| |||
505 | 550 | | |
506 | 551 | | |
507 | 552 | | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
508 | 558 | | |
509 | 559 | | |
510 | 560 | | |
| |||
522 | 572 | | |
523 | 573 | | |
524 | 574 | | |
| 575 | + | |
525 | 576 | | |
526 | 577 | | |
527 | 578 | | |
528 | | - | |
| 579 | + | |
| 580 | + | |
529 | 581 | | |
530 | 582 | | |
531 | 583 | | |
| |||
0 commit comments