Repository navigation
Commit 0837fdc
fix(metadata-protocol): an unshipped email-template locale keeps its write before the pair is ensured; the named-base refusal names os migrate packageless-overlays (#30322)
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgBNPpAVVeFjdqPfxDbdEPKj)_
domain:server#5 · os-bill · session_01Utp6CYVV6fcAQrNTDQRvPb
## 维护者速读
- **改了什么:** 给系统自带的邮件模板加一个基础包没有的语种(比如 showcase
只带英文,管理员加中文),在新装环境里现在能直接保存;指名基础包的写入(`?package=BASE`)同理。基础包已有的语种照旧被拒,并指向
`.custom` 配对包。
- **为什么改:**
代码注释写明这种写入应保持开放,但新环境里配对包还没建出来,判断「能否写进配对包」时拿到「不归我管」,被当成「能」,于是先拒一次;照报错去配对包写,又因基础包没有这个语种被拒第二次,翻译加不进去。拒绝句另补上
`os migrate packageless-overlays`,告诉运维这条命令能把老的基础包绑定行挪进配对包(#29479)。
- **风险与回滚:** 不加参数、不改错误码和状态;只改配对包尚未安装时的判断依据(按 ensure
门将要给它的依赖闭包)。指名基础包的拒绝句措辞变了(为塞进 500 字上限而收短),dogfood 钉住的 rollback
片段原样保留。回滚即撤 PR。
- **先看:**
`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(`overlayPairFixOf`
新增的 `pendingPairScope`、`overlayPairFixAdmits`
的调用、`closedOverlayWriteClauses` 的
`boundRowServed`),`packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts`(新
describe「a fresh install, the pair not yet ensured」)。
- **两分钟验证:** `pnpm --filter @objectstack/metadata-protocol exec vitest
run src/protocol.named-base-overlay-write.test.ts -t "fresh
install"`:三条全绿,两条无包和指名基础包的 fr-FR 写入成功,en-US 对照仍是 403 `NOT_OVERRIDABLE`。
- **需要你做的:** 无,席位落地。
Fixes #30168
Fixes #29479
Clause-②: no — the fix restores the write `packagelessCloseSubject`'s
docblock documents as open ("An `email_template` member at a locale the
base does not ship is refused there (#28777) and stays open"); a pair
that is installed was already answered that way, and nothing outside
that documented set is admitted.
Metadata-protocol: semantic — an author's package-less (or named-base)
save of an unshipped email-template locale, refused `403
NOT_OVERRIDABLE` on an install whose pair was not yet ensured, now
succeeds.
Skills-docs: none — `skills/objectstack-platform/SKILL.md` line 128 (PR
#29924) already states the package-less write of an unshipped locale is
kept ("an email-template row at a locale the base does not ship moves as
the pair's own member and keeps that write"); this PR makes the runtime
match it.
## What changes
**#30168 — the close read "no such package" as "admitted".** Both
overlay-write closes (`packagelessCloseSubject`, and
`namedBaseCloseSubject` for `?package=BASE`) close a write only where
the fix they name can be taken, and ask the override write gate about
the declared body written into the pair (`overlayPairFixAdmits`). The
gate reads the written package's closure from the registry
(`resolveWritePackageScope`). On a fresh install the pair `BASE.custom`
is not installed yet, so that read answers `undefined` and
`overrideWriteVerdict` answers `not_concerned`, which
`overlayPairFixAdmits` treats as "not refused". The close then closed a
write whose fix is refused once the pair exists
(`declared_package_ships_no_such_item`): the card's double refusal.
Every existing pin booted with the pair installed, so none saw it.
- `overlayPairFixOf` now carries `pendingPairScope` when the pair is not
installed: the closure the ensure door gives it (one dependency, the
base, `manifest.dependencies: { [base]: range }`), so the base plus what
the base reaches, read with `resolvePackageClosure`, the gate's own
walk.
- `overrideWriteVerdict` (module function and the class method) takes an
optional `writtenScope` used in place of the registry read for that
package. Only `overlayPairFixAdmits` passes one; every door's own write
passes none, so no door's verdict moves.
- `OverrideWriteVerdictAsk` types the gate as both closes' hosts ask it
(`NamedBaseCloseHost`, `PackagelessCloseHost`).
**The first act's measurement (step 1 both ways).** On a fresh showcase
boot (`pnpm dev -- --fresh`, this branch), `PUT
/api/v1/meta/email_template/showcase_task_done_email`:
- with `?member=zh-CN`: `400 VALIDATION_ERROR` "The "member" query
parameter is not supported by this endpoint … Supported parameters:
force, mode, package, surface." The save door is body-addressed
(`META_ITEM_SAVE_PARAMS`), so this spelling never reaches the close.
- without it, with `locale: 'zh-CN'` in the body: the close reads the
body's member (`overlayMemberOfBody` → `zh-CN`), the right one. The
defect was the gate half above, not the member reading; the docblock is
right and the code was wrong.
**#29479 — the named-base sentence names the ceremony.** Since PR
#29477, `os migrate packageless-overlays` moves a row stored bound to
the read-only base into the pair with its declaration; the refusal did
not say so. `closedOverlayWriteClauses` gains two clauses, one spelling
each: `ceremony` (the command, now used by both sentences,
byte-identical in the package-less one) and `boundRowServed`, used by
the named-base save, publish and rollback sentences: "… is served until
moved by `os migrate packageless-overlays`, overridden by the pair, or
deleted naming the base." The previous tail ("served until the pair
declares the override, or a delete naming the base removes it") was
shortened to fit: with the ceremony added in the long form, the file's
30-character-base / 40-character-name pin measured 525 (save) and 511
(rollback) characters against the REST door's 500
(`CLIENT_MESSAGE_MAX`). The phrase the dogfood suite pins ("a rollback
of its row bound to the read-only base is closed") is unchanged.
Docblocks that said the ceremony never moves a base-bound row
(`NamedBaseWriteOperation`, `namedBaseOverlayWriteRefusal` in both
files) are corrected.
## Tests
`packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts`:
`makeProtocol({ pairInstalled: false })` boots the base with no pair,
and a new top-level describe pins:
- the package-less save of `crm_welcome` at `fr-FR` (the base ships the
canonical member only) is accepted and stored package-less;
- the save naming the base at `fr-FR` is accepted, as it is with the
pair installed;
- CONTROL: the canonical member is still closed onto the pair at both
addresses, with the pair not yet ensured, and nothing is written.
`expectClosedSentence` now also asserts the `boundRowServed` clause on
every named-base sentence; the 500-character pin covers the new length.
<details><summary>Red first, on origin/main 04f36b6</summary>
```
× the package-less save is accepted and served as the environment's own member
× the save naming the base is accepted, as it is with the pair installed
Error: 'email_template/crm_welcome' ships in 'com.acme.crm': save it into 'com.acme.crm.custom' (?package=com.acme.crm.custom), declaring overrides: { package: 'com.acme.crm', basedOn }. A write naming no package is closed where `os migrate packageless-overlays` can move its row (ADR-0146 D11); …
Tests 2 failed | 32 passed (34)
```
The CONTROL case was green on main too.
</details>
<details><summary>Ablation (one, the behaviour the new pins
hold)</summary>
`node scripts/ablation-replace.mjs --file
packages/metadata-protocol/src/protocol-packaged-base-refusal.ts
--anchor "item: declaredBody }, fix.pendingPairScope);" --replacement
"item: declaredBody } /* ABLATED pendingPairScope */);" -- … vitest run
src/protocol.named-base-overlay-write.test.ts -t 'fresh install'` on
`b2fc8526`. The suite imports the source (`./protocol.js`), so no
`dist/` leg applies.
```
ablation-replace: ok mutation landed: anchor 1 -> 0, blob 6d46188 -> 56db37f9ff9d
× the package-less save is accepted and served as the environment's own member
× the save naming the base is accepted, as it is with the pair installed
Tests 2 failed | 1 passed | 34 skipped (37)
ablation-replace: ok restored: blob == HEAD (6d46188) and `git diff HEAD` is empty
```
</details>
## Signature notes (for the next PR on these functions)
`classifyPackagelessRow` is unchanged. `overlayPairFixAdmits` keeps its
parameters and result; its host's `overrideWriteVerdict` is typed
`OverrideWriteVerdictAsk` (one optional parameter, `writtenScope`), and
it passes `fix.pendingPairScope`. `OverlayPairFix` gains the optional
`pendingPairScope`, present only while the pair is not installed.
## Acceptance notes
- Live readings (fresh showcase boot, this branch): package-less zh-CN
save `200`, served at `?member=zh-CN` as the environment's own member
while en-US stays the package's; `?package=com.example.showcase` fr-FR
save `200`; both en-US controls `403 NOT_OVERRIDABLE` naming the pair,
the named-base one with the new clause.
- `content/docs/concepts/metadata-lifecycle.mdx`'s D11 callout says a
package-less write onto an item one read-only package ships is refused,
without the unshipped-locale exception. This PR does not make it false
(the exception was documented in code and skills, not there); left as
is.
## Verification
All on head `b2fc8526` (after `git merge origin/main`, `1a06be80`, then
a rebuild: `turbo run build --filter='!@objectstack/docs'`, 119/119).
- `pnpm --filter @objectstack/metadata-protocol test`: Test Files 312
passed, 4 skipped; Tests 22382 passed, 26 skipped.
- `pnpm --filter @objectstack/metadata-protocol typecheck`: exit 0.
- `packages/objectql`:
`environment-overlay-email-locale-restart.test.ts`,
`protocol-packageless-overlay-migration.test.ts`,
`environment-overlay-five-types.test.ts`: 3 files, 106 tests passed
(they read the rebuilt `dist/`).
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack`: 60 derived families, each run, all `exit
0`; `--ran` reconciliation: "60 derived famil(ies) accounted for — 60
run, 0 NOT-MEASURED".
- `pnpm lint`: exit 0.
- Not run locally, left to CI: the dogfood battery (no `!`, no
`(narrowing)` arm), `test:repo`, the UI lanes.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CYeyrEJGAte4R8B7un)_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Utp6CYeyrEJGAte4R8B7un
<!-- signed: domain:server#5 · os-bill ·
session_01Utp6CYVV6fcAQrNTDQRvPb -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent cfb602c commit 0837fdc
5 files changed
Lines changed: 159 additions & 40 deletions
File tree
- .changeset
- packages/metadata-protocol/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
46 | 46 | | |
47 | 47 | | |
48 | 48 | | |
| 49 | + | |
49 | 50 | | |
50 | 51 | | |
51 | 52 | | |
| |||
254 | 255 | | |
255 | 256 | | |
256 | 257 | | |
257 | | - | |
258 | | - | |
| 258 | + | |
| 259 | + | |
259 | 260 | | |
260 | 261 | | |
261 | 262 | | |
262 | | - | |
| 263 | + | |
263 | 264 | | |
264 | 265 | | |
265 | 266 | | |
| |||
274 | 275 | | |
275 | 276 | | |
276 | 277 | | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
277 | 285 | | |
278 | 286 | | |
279 | 287 | | |
| |||
286 | 294 | | |
287 | 295 | | |
288 | 296 | | |
289 | | - | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
290 | 300 | | |
291 | 301 | | |
292 | 302 | | |
| |||
306 | 316 | | |
307 | 317 | | |
308 | 318 | | |
309 | | - | |
310 | | - | |
311 | | - | |
312 | | - | |
313 | | - | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
314 | 324 | | |
315 | 325 | | |
316 | 326 | | |
| |||
322 | 332 | | |
323 | 333 | | |
324 | 334 | | |
325 | | - | |
326 | | - | |
327 | | - | |
328 | | - | |
329 | | - | |
330 | | - | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
331 | 343 | | |
332 | 344 | | |
333 | 345 | | |
| |||
337 | 349 | | |
338 | 350 | | |
339 | 351 | | |
340 | | - | |
341 | | - | |
| 352 | + | |
342 | 353 | | |
343 | 354 | | |
344 | | - | |
| 355 | + | |
345 | 356 | | |
346 | | - | |
| 357 | + | |
347 | 358 | | |
348 | 359 | | |
349 | 360 | | |
| |||
356 | 367 | | |
357 | 368 | | |
358 | 369 | | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
359 | 386 | | |
360 | 387 | | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
361 | 395 | | |
362 | 396 | | |
363 | 397 | | |
| |||
390 | 424 | | |
391 | 425 | | |
392 | 426 | | |
393 | | - | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
394 | 432 | | |
395 | 433 | | |
396 | 434 | | |
| |||
400 | 438 | | |
401 | 439 | | |
402 | 440 | | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
403 | 446 | | |
404 | 447 | | |
405 | | - | |
| 448 | + | |
406 | 449 | | |
407 | 450 | | |
408 | 451 | | |
| |||
419 | 462 | | |
420 | 463 | | |
421 | 464 | | |
422 | | - | |
| 465 | + | |
423 | 466 | | |
424 | 467 | | |
425 | 468 | | |
| |||
429 | 472 | | |
430 | 473 | | |
431 | 474 | | |
432 | | - | |
| 475 | + | |
433 | 476 | | |
434 | 477 | | |
435 | 478 | | |
| |||
953 | 996 | | |
954 | 997 | | |
955 | 998 | | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
956 | 1004 | | |
957 | 1005 | | |
958 | 1006 | | |
959 | 1007 | | |
| 1008 | + | |
960 | 1009 | | |
961 | 1010 | | |
962 | 1011 | | |
963 | 1012 | | |
964 | 1013 | | |
965 | 1014 | | |
966 | | - | |
| 1015 | + | |
| 1016 | + | |
| 1017 | + | |
967 | 1018 | | |
968 | 1019 | | |
969 | 1020 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
141 | | - | |
| 140 | + | |
142 | 141 | | |
143 | 142 | | |
144 | 143 | | |
| |||
689 | 688 | | |
690 | 689 | | |
691 | 690 | | |
692 | | - | |
693 | | - | |
| 691 | + | |
| 692 | + | |
| 693 | + | |
| 694 | + | |
| 695 | + | |
| 696 | + | |
694 | 697 | | |
695 | 698 | | |
696 | 699 | | |
| |||
0 commit comments