Repository navigation
fix(metadata-protocol)!: the override write gate judges an email template by its locale, so a pair's override of a locale the base does not ship is refused (#28746) - #28777
Conversation
…ed email-template locale A declared override of an email template locale the base does not ship, at the save door, the per-item publish, os validate and install-local, with the shipped-locale and undeclared-addition controls. Claude-Session: https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY Co-authored-by: Claude <noreply@anthropic.com>
The override write gate counted the packages that ship an email template by name across every locale, so a <base>.custom pair could declare and publish an override of a locale its base never shipped. The gate now asks about the member the body addresses: the save door and the publish look the registry up at that member, and os validate and install-local compare items on one shared identity (overrideWriteIdentityOf) that joins the bundle member for a pair-identified type. Claude-Session: https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY Co-authored-by: Claude <noreply@anthropic.com>
…changeset Claude-Session: https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY Co-authored-by: Claude <noreply@anthropic.com>
…at its two lookup sites Claude-Session: https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0bf4ae56423c6e2461becec9b33af063489d3f0d && git checkout 0bf4ae56423c6e2461becec9b33af063489d3f0d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8afaf12d7c302072871f7e975e9dff5be5541d90 e30f9fc8a703d04c4952a0ecae3a7c90f1d56fc1 && git checkout -B drift-repro 8afaf12d7c302072871f7e975e9dff5be5541d90 && git merge --no-ff e30f9fc8a703d04c4952a0ecae3a7c90f1d56fc1
node scripts/docs-audit/affected-docs.mjs --json 8afaf12d7c302072871f7e975e9dff5be5541d90
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Face: narrowing — ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe narrowing against the published spec on main: Implemented-by: VERDICT: PASS Must-fixes: none. Seat actions before ACCEPT / queue: file the Skills-docs card and rewrite the Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo
Requested by Leehom · project thread
Fixes #28746
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #28778
维护者速读
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_DISCRIMINATORSin@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):itemDiscriminatorover the type's singular, so the canonical member for a body that names none, andundefinedfor every type whose identity is its name. Read from the identity table, never a type literal;email_templateis the only entry today (measured:ITEM_KEY_DISCRIMINATORSat7a48bf73holds one key).overrideWriteIdentityOf(type, identity, item)(exported): an item's identity (its name; an action'sobjectName:name) joined with its member for a pair-identified type, unchanged otherwise.overrideWriteItemsOfspellsOverrideWriteItem.identitythrough it, soos 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 guardmergeArtifactProtectionapplies on the read side for a registry that ignoresmember.judgeOverrideWrite: unchanged rules and order; thedeclared_package_ships_no_such_itemsentence 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.tsandprotocol.ts. The privateshippedArtifactsOftakes an optionalmemberand passes it to both of itslookupArtifactItemcalls. Omitted, the lookup is what it was, so the lock's artifact layer (a lock binds the NAME) is untouched. The published.d.tslists 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) returnsoverrideWriteIdentityOf(...), so an installed base'sen-UStemplate and an incomingfr-FRone are two items at this door as at the publish doors.?member=), package publish (promoteDraftForPublishjudges the draft body),os validate/os build, install-local: each computes itsshippersat 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/28746-override-gate-bundle-member.md:minoron@objectstack/metadata-protocoland@objectstack/cloud-connection, both declaration lines, ADR-0087 dispositionnot-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; onmainit was refusedundeclared_same_name. Unchanged: a declared override of a shipped locale; an undeclared item at a shipped locale (stillundeclared_same_name); every non-bundle type.Tests
7a48bf73(dists built from base): the dogfood file failed 3 of 4 (thefr-FRoverride saved200and its draft published200, served as an override; thefr-FRaddition was refused403undeclared_same_name); the CLI cases failed 2 of 2 (no row for the declaredfr-FR; anundeclared_same_namerow for thefr-FRaddition); the install-local cases failed 2 of 2 (thefr-FRoverride installed200; the addition was refused403). Theen-USshipped-locale control was green on base.packages/qa/dogfood/test/email-template-override-unshipped-locale.dogfood.test.ts(showcase, HTTP):PUT ?mode=draft&package=com.example.showcase.customofshowcase_task_done_emailatfr-FRwithoverrides→403 NOT_OVERRIDABLE, no pair row,GET ?member=fr-FR404; a draft of that shape stored before the gate →POST …/publish?package=…&member=fr-FR403, the row stays a draft,fr-FRstill 404; controls:en-USdeclared → 200, published, served;fr-FRwithoutoverrides→ 200, published, served.packages/cli/src/utils/override-declarations.test.ts: the pair'sfr-FRdeclaration →declared_package_ships_no_such_item; controls:en-USdeclared plusfr-FRundeclared → no row; a dependent's template collides only aten-US.packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts: the pair'sfr-FRdeclaration →403, nothing registered, no ledger entry; control:en-USdeclared plusfr-FRundeclared →200.view/dashboard/page(metadata-protocolprotocol.packaged-base-refusal.test.ts, objectqlprotocol-custom-override-effective-read.test.ts, the CLI and install-local files) is unchanged and green.protocol.lookup-artifact-item-call-sites.test.tsrecords the literal arguments of eachlookupArtifactItemcall; the two rows in the seam module now readtype, name, undefined, memberandtype, name, packageId, member, with the reason.Ablation (one-off, from committed HEAD c5b0757)
node scripts/ablation-replace.mjsreplacedreturn itemDiscriminator(PLURAL_TO_SINGULAR[type] ?? type, item);inoverrideWriteMemberOfwith a guardedreturn undefined(markerABLATE_28746): anchor 1 → 0, blob5b2921c51bef→162d86a93c62. metadata-protocol and cloud-connection rebuilt;ablation-dist-preflight.mjs @objectstack/metadata-protocol ABLATE_28746: marker present in 2 built files, exit 0.fr-FRdeclaration and the combineden-US+fr-FRcontrol). install-local file: 2 failed / 6 passed (the same two). Dogfood file: 3 failed / 1 passed (the save refusal, the publish refusal and thefr-FRaddition; theen-USshipped-locale control stayed green). So dropping the member reds the refused cases AND the undeclared-addition controls, because onmainthe 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.git checkout HEAD --the file:git diff HEADempty, blob equals HEAD5b2921c51bef; both packages rebuilt; preflight--absent: marker absent from all 24 built files, tree clean, exit 0.Acceptance notes
overrides" as unaffected. Measured on base, it was refusedundeclared_same_nameat the save door,os validateand 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)).en-USonly is refused aten-USand admitted atfr-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.fr-FRoverride row laid down before the gate:GET ?member=fr-FRserves it with_packageId: com.example.showcaseand itsoverridesblock, and the layered read answerscode: null;en-USis unaffected. Its next save carryingoverridesis refused with the member-naming sentence; saved withoutoverridesit publishes as the pair's ownfr-FR, served undercom.example.showcase.custom. Nothing re-judges the row at boot or upgrade; if the base later shipsfr-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.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 withorigin/main5493b884, which carries #28412's flow-override edits toprotocol-packaged-base-refusal.tsandprotocol.ts; auto-merged), afterpnpm install --frozen-lockfileand a rebuild of the dogfood closure plus cli and cloud-connection (BUILD_EXIT=0).Package tests and typecheck
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. Theintegrationtier is declared to CI (the diff touches no spawn entry).typecheckfor metadata-protocol, cloud-connection, cli (withcheck:test-typecheck: OK, ledger held) and dogfood: exit 0.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 headc5b07573).Full dogfood battery (Clause-② narrowing arm)
pnpm --filter @objectstack/dogfood testunder the verify lock, no shard, no path: VERDICT command-exit 0; 375 files passed (3 skipped), 3038 tests passed (28 skipped).Gate sweep
node scripts/pm/dispatch-gates.mjs --commands: 69 families ate30f9fc8; every one exit 0. Three first answeredPREREQUISITE NOT MET(exit 3: the teardown self-test's pinned commit absent from the shallow clone;check:type-check-debtqueued behind the dogfood run;check:dual-build-cjs-loadsbefore a full build) and were re-run green: the teardown self-test after fetching its pinned commit,check:type-check-debtunder the lock,check:dual-build-cjs-loadsafter 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).Generated by Claude Code