Skip to content

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

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

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

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 #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.
Ablation (one-off, from committed HEAD c5b0757)
  • 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.

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 feat(objectql): an object extension's fieldPatches relabels the vendor field on every read, refused at install and os validate when it names nothing (#28565) #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).

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. 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).
Full dogfood battery (Clause-② narrowing arm)
  • 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).
Gate sweep
  • 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.

Generated by Claude Code

…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>
…at its two lookup sites

Claude-Session: https://claude.ai/code/session_01FEkQ5oDNN263d7ffSwgjsY
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cloud-connection, @objectstack/metadata-protocol, touching 10 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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
  • 1 changed file(s) yielded no anchor (packages/metadata-protocol/src/index.ts) — pages documenting those are invisible to this run
  • 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 — 14 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 8afaf12d7c302072871f7e975e9dff5be5541d90 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0bf4ae56423c6e2461becec9b33af063489d3f0d — the merge of head e30f9fc8a703d04c4952a0ecae3a7c90f1d56fc1 into base 8afaf12d7c302072871f7e975e9dff5be5541d90, 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 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

⚠️ 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 8afaf12d7c302072871f7e975e9dff5be5541d90 → 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: e30f9fc8a703d04c4952a0ecae3a7c90f1d56fc1
Local-runs: none
Scope: packages/metadata-protocol/src/protocol-packaged-base-refusal.ts, packages/metadata-protocol/src/protocol-artifact-lookup.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/index.ts, packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts, packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-override-gate.test.ts, packages/cli/src/utils/override-declarations.test.ts, packages/qa/dogfood/test/email-template-override-unshipped-locale.dogfood.test.ts + packages/cli/src/utils/override-declarations.ts (the os validate walk, source unchanged), packages/metadata-core/src/item-key-discriminators.ts (ITEM_KEY_DISCRIMINATORS, itemDiscriminator), packages/objectql/src/registry.ts (getArtifactItem's member narrowing), the verdict's call sites and mergeArtifactProtection in protocol.ts, skills/objectstack-platform/SKILL.md (the paragraph the follow-up names)

Face: narrowing — Clause-②: yes (narrowing) · Metadata-protocol: narrowed on the PR body and on .changeset/28746-override-gate-bundle-member.md; record-recognisers.mjs --paths derives no schema or rules face from the ten changed paths (no packages/spec/src/**, no governed rule text), so the narrowing face is the whole of what is owed and its floor is every non-prose path of the diff. Reviewed against merge-base 5493b8842995fabb0979ae535301d3f3cb9fc65e (origin/main at the dev's merge). Card #28746, triage#1, claim 6561473652, order-28746.md, os-dev-report 6562551938, epic #28236 N-09 and ADR-0146 D1/D2 read. PR author objectstack-fleet[bot], so no login note.

① Derived judgments

  1. The identity comes from the registry's table, not a literal — correct. overrideWriteMemberOf(type, item) is itemDiscriminator(PLURAL_TO_SINGULAR[type] ?? type, item) over ITEM_KEY_DISCRIMINATORS (@objectstack/metadata-core, the table the registry keys bundles on and bundleMemberOf reads for mergeArtifactProtection). At this head the table holds one key, email_template: { field: 'locale', canonical: 'en-US' } (item-key-discriminators.ts:68–70), so the dev's "only entry today" is measured true; a second bundle type added to the table is member-aware at every door with no edit here. A body naming no locale is the canonical member, which is how the read side and the save door's own packagelessPairLanding call (itemDiscriminator, protocol.ts:21168/22397) already read it.
  2. One spelling, four doors — holds. overrideWriteIdentityOf(type, identity, item) = identity joined with the member by a NUL for a pair-identified type, identity unchanged otherwise. Its consumers: (a) overrideWriteItemsOf stamps OverrideWriteItem.identity through it (refusal.ts:528), which is what os validate / os build keys its shipped set and walked key on (${type}\u0000${identity}, override-declarations.ts:109–115 — source untouched, so the CLI door moved by composition alone) and what the install door's delivered packages carry; (b) the install door's listedIdentity (install-local-plugin.ts:586–593) returns it for an installed base's listed items — and that listing carries every member of a bundle with its locale (SchemaRegistry listing since email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730, slot-keyed merge since GET /meta/email_template can collapse an i18n bundle back to one row — the list merge dedups by (package, name), no locale #7774, protocol.ts:3064–3078/10034–10044), so an installed base shipping en-US and zh-CN lists two identities, not one; (c) overrideWriteVerdict looks the registry up at the body's member and keeps only artifacts of that member. The save door (protocol.ts:21162, 22381, 22401), the per-item and package publish (:24001, judged on the draft body) and the flow-lock exception (:16911) all pass through overrideWriteVerdict, so the save door, ?member= publish, package publish, os validate/os build and install-local compute shippers at one identity. No second spelling of the pair exists in the diff or in the three doors; no door was found that lacks the member, since every caller hands the gate the body.
  3. A declared override of an unshipped locale is refused declared_package_ships_no_such_item at each door — pinned. The rule is judgeOverrideWrite's existing branch (!closure.shippers.includes(base)), unchanged in order and code; only the sentence gains with locale 'fr-FR' for a pair-identified type (the ofMember string is empty for every other type, so their sentences are byte-identical). Pinned at the save door and the ?member=fr-FR publish on the real showcase (dogfood, 403 NOT_OVERRIDABLE, no pair row, fr-FR 404), at os validate (CLI test, the one row) and at install-local (403, nothing registered, no ledger entry). No new refusal code, no second gate — the order's ruling held.
  4. A shipped-member override still passes — pinned at the canonical member; the non-canonical case rests on the registry's own pin. All three files pin en-US declared → admitted. A base shipping a non-canonical locale (zh-CN only, or beside en-US) is answered by getArtifactItem(type, name, pkg, member)'s entriesOfMember narrowing (objectql registry.ts:6463–6479), pinned at environment-overlay-email-locale-restart.test.ts:738 (zh-CN shipped member → the package's artifact); the verdict's .filter(a => overrideWriteMemberOf(type, a) === member) is a guard for a member-blind registry double, the same guard mergeArtifactProtection applies with bundleMemberOf. Correct; a non-canonical shipped-member control in this PR's own pins would have been tighter (observation, not a must-fix).
  5. The widening is real, correct under D2, and declared honestly. On main shippers counted the NAME, so the pair's undeclared fr-FR beside a base en-US met undeclared_same_name and a sentence telling the author to declare an override of a member that does not exist. Under the ruled identity it collides with no base item: D2's declaration is the mechanism for replacing an existing item, and D2's undeclared-same-name refusal exists so that nothing is "served as a silent replacement" — the read side serves per member (findServedOverlayRow, the layered read, the list's slot-keyed merge), so a fr-FR the base never shipped replaces nothing. Admitting it is the order's "unaffected: an addition without overrides" ruling made true (it was the order's premise that was wrong, not its intent). The PR body and the changeset each carry an explicit Widened paragraph and the 维护者速读 says 另有一处放宽; the Acceptance notes name the falsified premise. Honest.
  6. The Clause-② line is right as spelled. AGENTS.md step 3 admits yes|no plus at most one arm from the closed pair, (narrowing) being the BREAKING arm; a change that both narrows and widens cannot name both arms, and the yes already carries the surface additions (the widening, the new export). yes (narrowing) with the widening in prose is the only well-formed declaration of this diff; check-changeset-no-major reads narrowed beside a (narrowing) arm as consistent.
  7. Surface beyond the claim — each in scope and correct. protocol-artifact-lookup.ts: an optional fourth member threaded to both lookupArtifactItem calls; omitted, byte-identical, and every other caller (artifactLockLayerAt :94 — a lock binds the NAME —, protocol-lock-version-gates.ts:445, protocol-packageless-overlay-migration.ts:301, protocol.ts:10533/11386) passes none. It is the only way the gate asks per member without a second lookup. protocol.ts: the private shippedArtifactsOf gains the optional parameter and forwards it; TypeScript emits a private member without its signature, so the published .d.ts does not move. index.ts: overrideWriteIdentityOf is a new public export of @objectstack/metadata-protocol's entry — correctly a Clause-②: yes item, and the changeset's last paragraph names it with its contract; overrideWriteMemberOf stays module-level (not in the entry), which is right, since cloud-connection needs the identity only. The call-site pin records the two sites' literal arguments with the reason — the pin's purpose, moved honestly.
  8. Unchanged set verified. A declared override of a shipped locale admitted; an undeclared item at a shipped locale still undeclared_same_name (shippers at en-US contain the base); every non-bundle type's identity is its name (overrideWriteMemberOf is undefined, the NUL join never happens, the sentence is unchanged); the read side untouched.

② Semver level

minor on @objectstack/metadata-protocol and @objectstack/cloud-connection — the two packages whose published source the diff moves; @objectstack/cli moves tests only, so no entry is owed. The title carries !, the changeset the BREAKING banner, both declaration lines at line start, and one ADR-0087 marker not-required (no-migration-prescription). Consistent with AGENTS.md step 3 (a kind other than none ⇒ at least minor on a moved package; launch-window breaking ships minor) and with check-changeset-no-major (narrowed beside (narrowing)); the dev's sweep has check-changeset-no-major.mjs --base origin/main and check-adr-0087-registration.mjs exit 0, and CI Check Changeset is green on this head. ADR-0087 disposition judged honest: no key of any schema changes shape; a stored pair row declaring an override of an unshipped locale is (at rest) served as before under the base's _packageId with layered code: null; (at upgrade) nothing re-judges it at boot — if the base later ships that locale it becomes a real override; (next save) with overrides it meets the gate's own sentence naming the locale and the remedy, saved without overrides it becomes the pair's own member. Stated in the changeset's At rest paragraph and measured on the showcase; a migration would have nothing to convert.

③ Boundary flags

④ Design judgment

The narrowing against the published spec on main: OverrideDeclarationSchema (ADR-0146 D2) grants a <base>.custom item the capability to replace an item its base ships; the bundle identity (ITEM_KEY_DISCRIMINATORS, mirrored from the spec and pinned against it by registry-i18n-bundle-key.test.ts) says an email_template item is (name, locale). A declaration naming a locale the base never shipped is not a capability the spec grants — it declares a replacement of nothing — so this change takes nothing away from a consumer and makes the spec's own refusal (declared_package_ships_no_such_item) real at the grain the identity table already fixes for every read. Per file: protocol-packaged-base-refusal.ts holds against the code it cites (the registry lookup narrows by member, the read side's bundleMemberOf guard is mirrored, judgeOverrideWrite's order is untouched); marketplace-install-local-plugin.ts holds against the listing semantics it relies on (every member listed, locale on each); the CLI walk holds unchanged by keying on identity; the dogfood test accepts the change on the real showcase doors against both arms and both controls. The SKILL.md paragraph is silent rather than wrong, and its follow-up is declared. ADR-0146 is not amended by this PR and needs no amendment for the narrowing arm; whether D2's "under its own names" should be restated at member grain is the ③ follow-up's question, not this diff's.

Implemented-by: session_01FEkQ5oDNN263d7ffSwgjsY
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS

Must-fixes: none. Seat actions before ACCEPT / queue: file the Skills-docs card and rewrite the Skills-docs: line to #N; file the two p3 follow-ups in ③; read every check green on e30f9fc8a703d04c4952a0ecae3a7c90f1d56fc1.


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-01T19:23Z · head e30f9fc8 · #28746

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

  1. Shape:
    • A draft against main with Fixes #28746; minor for @objectstack/metadata-protocol and @objectstack/cloud-connection; Clause-②: yes (narrowing), Metadata-protocol: narrowed, one ADR-0087 marker.
    • check-governed-merges --pr 28777 reads NOT governed (0 of 10 paths), +310 / -19. Landing tier S.
    • The claim (6561473652) names this PR's branch, claude/project-thread-4dpgpx. The file surface grew to protocol-artifact-lookup.ts, protocol.ts, the entry export and a call-site pin, all inside the one gate's lookup; the review read each.
  2. Against the order:
    • One identity, (type, name, member), with the member read from the registry's bundle-key table (email_template is its only entry), is shared by the save and publish door, os validate / os build and install-local. A declared override of a locale the base does not ship is refused declared_package_ships_no_such_item at each; an override of a shipped locale passes.
    • The dev measured that the order's "unaffected" addition (a new locale in the pair without overrides) was refused on main; it is now admitted, as ADR-0146 D2 reads at member grain. The PR body and changeset state that widening beside the narrowing.
    • Red first 7 of 8 on base; the ablation reds the refused and addition cases only.
  3. Contract review: owed (narrowing). PASS on this head, no must-fix (6562711312). The dev's open question was decided A (keep).
  4. Seat actions the review named, done: Skills-docs card skills(objectstack-platform) + docs: an email-template override names a locale its base ships; a new locale carries no overrides (#28746 follow-up) #28778 filed (pm:blocked until this lands) and the PR's Skills-docs: line rewritten to it; p3 follow-ups 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 (measure the base-named read for a dependent's locale) and metadata-protocol: an email-template override of an unshipped locale stored before PR #28777 is served under the base's package id (#28746 follow-up) #28780 (a pre-gate stored row at rest) filed.
  5. Gates: the dev reports 69 of 69 derived gate families exit 0 and the full dogfood green; CI on this head was in progress with 0 red at review, and auto-merge waits for it.
  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 19:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 19:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 5bb7834 Nov 1, 2026
54 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-4dpgpx branch November 1, 2026 19:49
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/m tests tooling

Projects

None yet

1 participant