Skip to content

fix(metadata-protocol)!: an email template locale added in a .custom pair is listed and sent on the base's address; a dependent's added locale stays under its own package (#28802) - #28828

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-p5198q
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-p5198q

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Requested by Leehom · project thread

Fixes #28802
Fixes #28779

Clause-②: yes (narrowing)
Metadata-protocol: semantic
Skills-docs: follow-up #28829

维护者速读

  • 改了什么:邮件模板按「名称 + 语言」逐个成员判断基础包地址上该给谁。客户在 BASE.custom 里新增的基础包没有的语言(比如 showcase 只发 en-US,客户加 fr-FR),现在出现在全局列表、基础包列表和执行列表里,发 fr-FR 邮件真的用客户的模板;依赖包在基础包模板名下加的语言,只在依赖包自己的地址下可读,按基础包名或不带包名读取不再返回它(404)。
  • 为什么改: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 放行这种新增后,配对包加的语言能按名读到却从不进列表,邮件服务读的是执行列表,所以法语收件人收到英文原版;反过来,依赖包的语言却能冒用基础包的地址被按名读出。metadata-protocol: measure what the base-named read serves when only a dependent package holds an email-template locale under the base's name (#28746 follow-up) #28779 实测,席位选了方案 A(成员粒度共存)。
  • 风险与回滚:中低。只改读取侧,写入闸门不变,非成员类型(除 email_template 外的所有类型)字节不变。收窄:按基础包名读依赖包新增语言从 200 变 404。回滚即撤销本 PR。
  • 先看:packages/metadata-protocol/src/protocol.ts 的三处 [#28802](全局列表成员粒度收窄、基础包列表保留配对包新增成员、按名读取的配对包行与注册表兜底守卫);protocol-environment-rows.ts 的 baseAddressServesMember;dogfood 测试文件。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/email-template-dependent-unshipped-locale-read.dogfood.test.ts,预期 10/10 通过(main 上 4 红)。
  • 需要你做的:无,席位落地。

What changes

An email template is one item per (name, locale) (ITEM_KEY_DISCRIMINATORS), and since #28746 a locale the base does not ship is admitted as an addition, from the base's .custom pair (no overrides) and from a dependent package. The reads of a base's address decided at name grain. They now decide member by member (option A of the #28779 report, the seat's pick; ADR-0146 D2, ruling A on #28390):

on the base's address base's own member pair's declared override pair's own added member dependent's added member
by-name read naming the base / naming no package served served (as the base's) served, _packageId: PAIR (unchanged) 404 (was the dependent's body)
env-wide list, list scoped to the base, getMetaItemsForExecution listed listed listed (was absent) absent (unchanged)
renderTemplate / sendTemplate in that locale renders it renders it renders it (was the base's en-US) the base's canonical member (unchanged)

The dependent's member is still served by the read naming the dependent and by the list scoped to it; the list scoped to the pair is unchanged.

packages/metadata-protocol/src/protocol-environment-rows.ts. baseAddressServesMember(base, bound, baseShipsMember): the one member-grain answer. A member bound to the base or to no package is served; a member bound to the base's pair is served when the base does not ship that member; anything else (a dependent's member, a pair's undeclared row at a member the base ships, which the write gate refuses) is not.

packages/metadata-protocol/src/protocol.ts, bundle types only (read from ITEM_KEY_DISCRIMINATORS, never a literal; email_template is its only entry):

  • Env-wide list (readFlattenedMetaItems, the narrowing to the sole shipper): an item another package binds under a name one code package ships is kept when baseServesBundleMember says so. For every other type the expression is byte-identical (isBundle && … is false).
  • List scoped to the base: the pair's row at a member the base does not ship, which the merge held back as an unseated stand-in, is kept (unseated.delete). Only for a name the base ships.
  • By-name read (readMetaItem): when no served-row candidate holds a row and the base ships the name but not the member, the pair's own row at that member is read (findServedOverlayRow naming the pair) and served under its binding. The registry's last tier (the first composite match across packages, ADR-0048 §3.3) is then guarded: an answer the base's address does not serve is dropped (registryMemberServedAt); for a read naming no member, the base's own canonical-first member is asked instead.
  • Three private helpers (shipsBundleName, baseServesBundleMember, pairOfAddedMember) count shipped members through the existing member-aware shippedArtifactsOf.

Why the pair's row and not the registry. Measured: with the pair's and a dependent's fr-FR both stored, the registry holds one entry for that member (the last hydration wins), so the base's address served the dependent's body even with the pair present (red case in both test files below). The stored row is the pair's own, wherever the registry slot stands.

PR #28800's shape pin flipped. email-template-dependent-unshipped-locale-read.dogfood.test.ts now pins the behaviour: its SHAPE cases are renamed and assert the 404, and a second describe (its own boot) pins the pair-added locale and the pair-plus-dependent case.

Tests

  • packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts: 10 cases over the real showcase and its HTTP doors. RED on main c064d3a2 (4 failed / 6 passed): the base-named and package-less reads served the dependent's fr-FR (2 cases), the base's lists and executing list left the pair's fr-FR out and renderTemplate fr-FR rendered en-US, and with both stored the base's address served the dependent's. GREEN at f085057c: 10/10 (os-verify-lock VERDICT command-exit 0).
  • packages/objectql/src/protocol-bundle-member-base-address.test.ts (new): the real protocol over the real engine and an in-memory driver; 5 cases: pair addition on every read; dependent addition 404 on the base's address; pair and dependent at one locale; a bundle with no canonical member read with no member (the base's member, not a dependent's en-US); the pair's declared en-US override beside its added fr-FR. RED on the base's metadata-protocol src (5/5 failed); GREEN 5/5 at f085057c.
  • Package suites at f085057c: @objectstack/metadata-protocol typecheck + test (296 files passed, 4 skipped; 22068 tests passed), @objectstack/objectql typecheck + test (520 files, 9988 tests passed).
  • Ablations, one per changed behaviour, each leg mutated through scripts/ablation-replace.mjs (anchor 1 → 0, restore to the HEAD blob verified), metadata-protocol rebuilt and the mutation proved absent from dist/ by ablation-dist-preflight.mjs --absent, both files run:
    • A1, drop the member-grain list change (env-wide keep → isBundle && false, scoped-list unseated.delete(it) → void it): unit 3 red (the three pair-addition cases), dogfood 2 red (the pair's lists and send; pair beside dependent). Dependent-only cases stay green.
    • A2, drop the by-name registry guard: unit 2 red (dependent 404; no-member read), dogfood 2 red (the two dependent-on-base cases). Pair cases stay green.
    • A3, drop the pair's stored-row read: unit 1 red and dogfood 1 red, both the pair-beside-dependent case only.
    • The first A1 attempt did not run: if (false) replacements failed the DTS build (unused locals), so nothing was measured; re-run with type-neutral replacements as above. A restore build followed the last leg.

Acceptance notes

Verification

Gate sweep at f085057c: dispatch-gates --commands derived 66 families; 66 run, all exit 0 (dispatch-gates --ran reads "66 run, 0 NOT-MEASURED", a derived zero). One first ran exit 3 on a prerequisite and was re-run green: check:dual-build-cjs-loads (seven packages outside the dogfood closure were unbuilt). The record is pm-orders/server/28802-gates-ran.txt in the project files.

Full dogfood battery (the (narrowing) arm) at f085057c, under the lock as CI runs it (pnpm --filter @objectstack/dogfood test, no shard, no path): VERDICT command-exit 0, Test Files 380 passed, 3 skipped (383); Tests 3118 passed, 28 skipped (3146).

Changeset .changeset/28802-bundle-member-base-address.md: @objectstack/metadata-protocol minor, both declaration lines, the ADR-0087 not-required (no-migration-prescription) disposition. CI families are read from this PR's check-runs, not re-run here.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5m2Ryfx2Wubuz716mw

…s address, keep a dependent's under its own package

The env-wide list narrowed a name one code package ships at name grain, so the
pair's own added email template locale was read by name but never listed or
executed, and the by-name reads on the base's address fell to the registry's
cross-package tier and served a dependent's member under the base's name.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5m2Ryfx2Wubuz716mw
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 8 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 3cb8553c946905bf216dfaa6b84870380d112e25 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b3289e424a6b817c4f05a9dca104d99dedbb5c2d — the merge of head 47b1c37574444ec65bc0a544281952f2d5ff26af into base 3cb8553c946905bf216dfaa6b84870380d112e25, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin b3289e424a6b817c4f05a9dca104d99dedbb5c2d && git checkout b3289e424a6b817c4f05a9dca104d99dedbb5c2d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3cb8553c946905bf216dfaa6b84870380d112e25 47b1c37574444ec65bc0a544281952f2d5ff26af && git checkout -B drift-repro 3cb8553c946905bf216dfaa6b84870380d112e25 && git merge --no-ff 47b1c37574444ec65bc0a544281952f2d5ff26af

node scripts/docs-audit/affected-docs.mjs --json 3cb8553c946905bf216dfaa6b84870380d112e25

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 3cb8553c946905bf216dfaa6b84870380d112e25 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f085057cbe729cc8ecf3e0db867990302cb9e3f9
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-environment-rows.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/protocol-bundle-member-base-address.test.ts, packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts + packages/metadata-protocol/src/protocol-artifact-lookup.ts (shippedArtifactsOf, shippingPackagesOf), packages/objectql/src/registry.ts (registerItem, getItem), packages/metadata-core/src/item-key-discriminators.ts, packages/spec/src/kernel/customization-package.ts, packages/plugins/plugin-email/src/template-loader.ts, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts (read as the PR #28777 write-gate control)

Reviewed at 2026-11-01T22:25Z against the merge-base c9a9981dae1803fe5270c2171605c1c06508ee89 (PR #28800's merge commit) for PR #28828 (card #28802, closes #28779), order: option A, member-grain coexistence. Face: --paths decides no face; the committed changeset .changeset/28802-bundle-member-base-address.md carries Clause-②: yes (narrowing), so the narrowing face governs and the floor is every non-prose path of the diff. Read-only: the diff, the card, the claim, the os-dev-report, the evidence directories, PR #28800, ADR-0146 D2/D8, ADR-0048 §3.3, and the head's check-runs; nothing built, run or re-run.

① Derived judgments

1. The order's semantics, per read face (bundle types only, email_template today). Judged on the diff plus the cited code; each face names the case table: base's own member / pair override / pair-added member / dependent-added member / pair and dependent at one member.

  • Env-wide list (readFlattenedMetaItems, servedOverride && !packageId): the sole-shipper narrowing now keeps an item bound to another package when isBundle && baseServesBundleMember(...). The base's own member (bound = sole shipper) and the pair's declared override (seated in the base's slot, stamped as the base's) take the unchanged return true; the pair's added member (bound PAIR, base does not ship the member) is kept; a dependent's member (bound DEPENDENT) is dropped; a pair's undeclared row at a member the base ships stays dropped. Pair-beside-dependent: the stored rows reach this list from sys_metadata per (slot, package) (not from the registry's one bare slot), so both are present and the member-grain filter keeps the pair's and drops the dependent's. Correct.
  • ?package=BASE list: the pair's rows enter as overrideStandIns; an undeclared one was recorded in unseated and dropped. The new block deletes from unseated the pair's row of a name the base ships (shipsBundleName) at a member the base does not ship (baseServesBundleMember), stamped with the pair's id. A dependent's row never enters a base-scoped read (readActiveOverlayRows(request) is scoped to BASE; overrideRows are the pair's only). Correct.
  • Base-named by-name read (readMetaItem, request.packageId = BASE): step 1's served-row candidates (declared override, own row, package-less) are unchanged; the new 1b reads the pair's own row at the wanted member only when pairOfAddedMember says the base ships the name and not that member; the registry tier is then guarded by registryMemberServedAt. Base's own member: step 1 or the artifact. Pair override: step 1 (overrideOf), stamped as the base's. Pair-added member: 1b, stamped PAIR (as the registry tier stamped it before, so the stamp is unchanged). Dependent-added member: 1b finds no pair row; the registry's bare slot holds the dependent's hydrated row; the guard drops it (bound DEPENDENT, base does not serve) → 404. Correct.
  • Package-less by-name read: contentPackageId = packagelessContentPackage = the one code package that ships the name (BASE) whether the stored rows are bound to PAIR, to DEPENDENT or to both (candidates ≠ 1 ⇒ null ⇒ shippers.size === 1 ⇒ BASE), so 1b and the guard run exactly as on the base-named read. When several packages ship the name and a row is bound to one, contentPackageId is undefined and nothing changes. Correct.
  • ?package=DEPENDENT (list and by-name): the list reads the dependent's own rows; its pairOfList (DEPENDENT.custom) has no rows. By-name: step 1 serves the dependent's own row; registryMemberServedAt returns item unchanged because the dependent ships no bundle of that name. Unchanged, as ordered.
  • ?package=PAIR: customizationPackageId(PAIR) is not ok, so neither new list block runs; by-name step 1 serves the pair's own row. Unchanged, as ordered.
  • getMetaItemsForExecution: readFlattenedMetaItems(..., 'execution') with layer: 'effective' ⇒ servedOverride true and no packageId ⇒ the env-wide member-grain narrowing applies. Correct.
  • renderTemplate / sendTemplate: packages/plugins/plugin-email/src/template-loader.ts:142 reads getMetaItemsForExecution({ type: 'email_template' }); a listed pair-added fr-FR renders, a dependent's falls to the base's en-US. Correct.
  • The third change (pair's stored row on the by-name path) — correct, and minimal where it is needed. Verified in the cited code: the hydrator calls registry.registerItem(type, registered, 'name') with NO packageId, so every stored row hydrates into the one bare slot name@member of getItem's bare tier; with a pair row and a dependent row at one member the registry holds one entry, whichever hydrated last. The registry guard alone would therefore answer the dependent's body or nothing, depending on hydration order; reading the pair's own stored row before the registry tier is the only deterministic answer on the by-name face. It is not needed on the list faces, which never consult the registry for stored rows (the overlay merge reads sys_metadata per (slot, package)), and the dev did not add it there. The A3 ablation reddening only the pair-beside-dependent cases is consistent with this reading.
  • No-member read (member undefined, wantedMember = canonical): for a bundle with no canonical member the guard asks the registry for the base's own member (itemDiscriminator of the base's artifact) instead of a dependent's canonical one; a bundle that ships the canonical member is unchanged (pairOfAddedMember returns undefined, the registry answers the base's). The unit test's fourth case pins it. Correct.

2. Bundle-type derivation. Every new branch is gated on ITEM_KEY_DISCRIMINATORS[request.type] !== undefined (scoped list), isBundle from the same table (env-wide list), or wantedMember !== undefined (bundleMemberDiscriminator returns undefined for a type with no table entry). No 'email_template' literal in the diff. For a non-bundle type the env-wide expression reduces to the old soleShipper === undefined || soleShipper === bound (isBundle && … is false), the scoped-list block is skipped, and neither by-name addition runs. Byte-identical behaviour for non-bundle types by construction; the control is the package suites (metadata-protocol 296 files, objectql 520 files) and the full dogfood battery, not a dedicated non-bundle case — see should-fix ③ S1.

3. Write gate. The diff touches neither protocol-packaged-base-refusal.ts nor packaged-base-regime.ts (PR #28777's files, merge 5bb7834a is an ancestor of this head). The dogfood keeps the 403 NOT_OVERRIDABLE control for a dependent's en-US. Untouched, as ordered.

4. Evidence.

5. Skills-docs. The PR body carries Skills-docs: follow-up owed — skills/objectstack-platform/SKILL.md, content/docs/concepts/metadata-lifecycle.mdx. Under pm-dispatch SKILL.md (the Metadata-protocol line's third-line rule) follow-up owed — FILES is a legal value and the seat files the owed card and rewrites it to follow-up #N before ACCEPT; the review checklist makes the missing rewrite a REWORK. So a follow-up is enough under the repo rules; the rewrite is owed by the seat (③ S2). Open #28778 / PR #28814 already edits exactly these two files and is the natural carrier.

② Semver level

  • Clause-②: yes (narrowing) — correct. Widening: a pair-added member of a bundle is served on the base's address (lists, execution list, by-name reads, send). Narrowing: a by-name read naming the base or no package no longer answers a member the base's address does not own through the registry's cross-package tier (200 → 404). The widening is stated in the PR body and changeset prose; the title carries !; the BREAKING banner and the ADR-0087 not-required (no-migration-prescription) marker are in the changeset body.
  • Metadata-protocol: semantic — correct; the claim's expected narrowed is not. AGENTS.md Post-Task Checklist step 3 defines the line as the change to what a metadata author can write: narrowed = a key removed or tombstoned, a value refused or a requirement tightened; semantic = a changed meaning or default. Nothing an author writes is refused here — the metadata-protocol: the override write gate judges an email template by (type, name), so a <base>.custom pair can declare and publish an override of a locale the base does not ship #28746/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 write gate is untouched, a dependent's locale under a base name is still accepted and still served under its own package. What changed is the meaning of a written locale on the base's address: a pair-added locale now means "listed and sent for the base"; a dependent's means "the dependent's own, served under its package only". That is semantic; the breaking read-side narrowing is carried by the (narrowing) arm, the banner and the ADR-0087 marker, which is exactly how check-changeset-no-major.mjs separates the two (narrowed must sit beside a (narrowing) arm; semantic beside one is not a contradiction). Open question answered: option A, accept semantic.
  • Changeset level: @objectstack/metadata-protocol: minor — satisfied under either arm. The level follows the kind alone: narrowed and semantic each take at least minor on a package the diff moves (protocol.ts, protocol-environment-rows.ts are its published source); none would take patch. The gate's level axis therefore demands minor whichever kind is declared; the committed changeset declares minor, the local sweep records check-changeset-no-major.mjs --base origin/main :: exit 0, and the head's Check Changeset check-run concluded success. Both declaration lines are in the PR body and the changeset body, line-initial.
  • Dogfood battery owed by the (narrowing) arm: reported run at the head under the lock, no shard, no path — Test Files 380 passed, 3 skipped; Tests 3118 passed, 28 skipped; VERDICT exit 0. As reported.

③ Boundary flags

Author objectstack-fleet[bot]; no relay login to name.

  • Open question (Metadata-protocol kind): answered above — semantic. No change to the PR.
  • S1 (should-fix): no explicit non-bundle control. The order's control list named "a non-bundle type's lists byte-identical"; the diff's gating makes it true by construction and the suites cover it, but no case in either new test file lists a non-bundle type beside the bundle cases. Acceptable as is; a one-case control in protocol-bundle-member-base-address.test.ts would make the ablation legs prove it.
  • S2 (seat, before ACCEPT): rewrite Skills-docs: to follow-up #N — fold into skills(objectstack-platform) + docs: an email-template override names a locale its base ships; a new locale carries no overrides (#28746 follow-up) #28778 / PR docs(skills): an email-template override names a locale its base ships; a new locale carries no overrides #28814 (which already edits both files) or file a card; the body currently reads owed — FILES.
  • S3 (should-fix, prose): the narrowed set is wider than "a dependent's". registryMemberServedAt drops ANY other package's member the registry's first-match tier answered on a base's address — a dependent's, and a same-named package outside the closure (ADR-0048 §3.3 coexistence on a bare name; ADR-0146 D8 item 2 says such an item is not a candidate for the base's address at all, so the behaviour is right). The changeset's "Narrowed" bullet names only the dependent; say "another package's" so the CHANGELOG reader is not surprised.
  • Dormant, noted by the dev, agreed: previewDrafts of a pair-added member on the base's address falls through to the active read (serves the pair's published row); a package-unbound row at the same unshipped member is served first by the by-name read while the scoped list's stand-in is the unbound row (pre-existing ordering). Neither is reached by a documented door today.
  • Flag (follow-up card owed, not this PR): the layered read. /layers?package=BASE&member=fr-FR still answers 404 for a pair-added member (noted on PR test(dogfood): pin what the reads serve for a dependent's email template locale the base does not ship (#28779) #28800 and here). ADR-0146 D8 lists the layered read among the five that must agree; the order's face list did not include it. The seat should file it as a D8 card.
  • Flag (not measured): the version token the base-named read returns for a pair-added member (the lock is selected from the read's address, where no base row exists). No door edits a pair-added member through the base's address, so dormant; worth one line in the D8 card above.
  • Landing precheck ②: CI on the head is not complete at review time (11 in progress, 0 failures); the flipped dogfood file first runs in the merge group. The record stands for this head only.

Implemented-by: claude/project-thread-p5198q
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

…hangeset names any other package's member

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5m2Ryfx2Wubuz716mw
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 47b1c37574444ec65bc0a544281952f2d5ff26af
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-environment-rows.ts, packages/metadata-protocol/src/protocol.ts, packages/objectql/src/protocol-bundle-member-base-address.test.ts, packages/qa/dogfood/test/email-template-dependent-unshipped-locale-read.dogfood.test.ts + packages/metadata-protocol/src/protocol-artifact-lookup.ts (lookupArtifactItem, shippedArtifactsOf), packages/objectql/src/registry.ts (registerItem, getItem, getArtifactItem), packages/metadata-core/src/item-key-discriminators.ts, packages/spec/src/kernel/customization-package.ts, packages/plugins/plugin-email/src/template-loader.ts, packages/metadata-protocol/src/protocol-packaged-base-refusal.ts (the PR #28777 write-gate control), packages/rest/src/form-routes.ts and packages/runtime/src/domains/packages.ts (the per-package layer's readers, for the side reading)

Re-pin at 2026-11-01T22:40Z of the record on f085057cbe729cc8ecf3e0db867990302cb9e3f9 (PR #28828, card #28802, closes #28779; merge-base c9a9981dae1803fe5270c2171605c1c06508ee89). The new head is one commit on the reviewed head, 47b1c3757 test(objectql): a non-bundle control beside the member-grain cases; changeset names any other package's member, touching two files: the objectql test (+56) and the changeset (one bullet). No published source moved since the reviewed head, so ① items 1–3 of the prior record stand unchanged; this record re-judges what moved and answers the side reading. Read-only: the diff between the two heads, the PR body, the two new cards, the head's check-runs; nothing built, run or re-run (the working tree was read at the head commit, not a local branch).

① Derived judgments

② Semver level

Unchanged from the prior record: Clause-②: yes (narrowing) (widening stated in prose; ! in the title), Metadata-protocol: semantic (nothing authorable is refused; the meaning of a written locale on the base's address changed — option A of the dev's open question), @objectstack/metadata-protocol: minor (required by either kind). Both lines are line-initial in the PR body and the changeset body; Check Changeset and Governed Surface Queue Guard concluded success on this head. The (narrowing) arm's full dogfood battery was reported on f085057c; the one commit since touches an objectql unit test and changeset prose only, which moves no dogfood input.

③ Boundary flags

Author objectstack-fleet[bot]; no relay login to name.

Implemented-by: claude/project-thread-p5198q
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-01T22:41Z · head 47b1c375 · #28802 (closes #28779)

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
    • A draft against main with Fixes #28802 and Fixes #28779, and a ! title. It declares Clause-②: yes (narrowing), with the widening stated in prose, and Metadata-protocol: semantic, with a minor changeset for @objectstack/metadata-protocol. It also carries Skills-docs: follow-up #28829.
    • check-governed-merges --pr 28828 reads NOT governed (0 of 5 paths), +649 / -104. Landing tier S.
    • The claim (6563657135) names this PR's branch, claude/project-thread-p5198q.
  2. Against the order (option A, member-grain coexistence):
  3. Contract review at CONTRACT_REVIEW_TIER:
  4. Follow-ups filed:
  5. Gates: CI on this head has 0 red and 13 pending, and auto-merge waits for them.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 22:42
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 22:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 1920e48 Nov 1, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-p5198q branch November 1, 2026 23:17
os-project-manager pushed a commit that referenced this pull request Nov 2, 2026
Main's #28828 lists and sends a locale the pair adds on the base's
address, so the second locale no longer needs a row stored before the
close: `en-US` declares the override in the pair, `zh-CN` is the pair's
own member, and the member reset names the pair.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5ni5yjiTswP6crgAkX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant