Skip to content

fix(metadata-protocol): os migrate packageless-overlays moves an email template row of a locale the base does not ship as the pair's own member - #29258

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7l0dag
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-7l0dag

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #28904

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:os migrate packageless-overlays 现在能迁移「基础包不发运该语言」的旧版无包邮件模板行(例如只发运 en-US 的模板的 zh-CN 翻译):把它作为配对包 BASE.custom 自己的该语言成员写入,不带 overrides 声明,回读确认后再删除无包行,结果报 moved 并注明 addedMember。
  • 为什么改:以前仪式给这类行加上 overrides 再存入配对包,被 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 的写入闸门拒绝(基础包不发运该语言),结果 failed、行原地不动;客户已有的翻译邮件迁不走。
  • 风险与回滚:低。只改迁移仪式的分支和 CLI 的展示文字;写入闸门、读取侧、关闭无包写入的判定都不变。基础包发运的语言照旧按声明式覆盖迁移。回滚即撤销本 PR。
  • 先看:packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts 的 addedMemberOf 与迁移循环第 2、3 步的 [#28904] 分支;packages/cli/src/commands/migrate/packageless-overlays.ts 的预览与报告两行。
  • 两分钟验证:pnpm --filter @objectstack/metadata-protocol build && pnpm --filter @objectstack/objectql exec vitest run src/environment-overlay-email-locale-restart.test.ts -t 28904,预期 5/5 通过(main 上 2 红)。
  • 需要你做的:无,席位落地。

What changes

An email_template is one item per (name, locale) (ITEM_KEY_DISCRIMINATORS). The ceremony classified a package-less row against the packages that ship its NAME, so a zh-CN row of a template the base ships in en-US only was movable, and the move saved it into the pair declaring overrides: { package: BASE, basedOn }. The override write gate counts shippers at the bundle member and refuses that declaration (declared_package_ships_no_such_item, #28777), so the outcome was failed and the row stayed.

packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts:

  • addedMemberOf (private): the bundle member a row's body is, when the base ships the name but not that member, counted as overrideWriteVerdict counts a shipper (the artifacts at that member, less any another member answers, bound to the base). undefined for a name-identified type and for a shipped member.
  • The census sets addedMember on such a movable row (new optional field of PackagelessOverlayRow). The disposition vocabulary is unchanged.
  • The move, for a row with addedMember: step 2 saves the body into the pair with NO overrides (a stored overrides naming the base is dropped), so no basedOn is needed or recorded and a base without a version does not fail it; step 3 reads back the pair's row at that member and requires it to declare nothing (isOwnMemberRow); step 4 deletes the unbound row as before, its history message naming the member. Outcome: { outcome: 'moved', packageId: PAIR, addedMember } (a new arm of PackagelessOverlayOutcome, no new outcome word).
  • Every other row moves exactly as before.
  • The close (packagelessCloseSubject) is unchanged: it judges the declared-override move only, so an unshipped member's package-less write stays open, as on main. Its docblock now says why.

packages/cli/src/commands/migrate/packageless-overlays.ts: the preview lists such a row as → PAIR (its own member; BASE ships none at zh-CN) instead of (overrides BASE), the --apply line reads moved into PAIR (as its own member zh-CN) instead of (basedOn …), and the --json row carries addedMember and omits basedOn for it. This file is in the claim's surface conditionally ("only if the preview needs a new outcome word"); no word was added, but without this edit the preview and report would print an overrides / basedOn the move does not record.

Tests

packages/objectql/src/environment-overlay-email-locale-restart.test.ts, new describe [#28904] … (5 cases, the file's real engine, protocol and in-memory driver; the package ships acme.digest in en-US only):

  1. an unshipped locale moves into the pair as its own member with no overrides; subject, bodyHtml, bodyText, replyTo and fromOverride are unchanged on the execution list the send path reads and on the by-name read, at once and after a restart; the base's en-US member is untouched; a re-run moves 0;
  2. an unshipped-locale row whose stored body declares the base: the declaration is dropped on the move;
  3. READ BACK FIRST: a save that writes nothing keeps the unbound row and reports failed;
  4. CONTROL: a shipped locale (acme.welcome zh-CN) still moves as the declared override with basedOn;
  5. the 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 refusal is unchanged: after the move, a save into the pair DECLARING an override of the unshipped locale answers 403 NOT_OVERRIDABLE.

Forced test edits: none. Two docblocks in the same file that said the ceremony cannot move an unshipped member (the file header and resetZh) now say it moves it as the pair's own member and its package-less reset stays open; comment-only.

Red on main, green on the fix

Red: metadata-protocol built from main's source (the fix held out of the tree), cases 1 and 2 red (os-verify-lock VERDICT command-exit 1, 2 failed / 2 passed): the census had no addedMember, and the move answered failed where moved was expected.

Green at 505a5ab7 and again at c62af5d2: 5/5; the three ceremony suites (environment-overlay-email-locale-restart, protocol-packageless-overlay-migration, environment-overlay-five-types) 87/87.

Ablations (one per pinned behaviour; each leg through ablation-replace.mjs, metadata-protocol rebuilt, ablation-dist-preflight.mjs marker present, then restored to the HEAD blob and preflight --absent)
  • A1, census addedMember ignored by the move (the old declared-override save): cases 1, 2, 3 red; case 1's outcome became failed with … declares an override of 'com.acme.mail', which ships no email_template named 'acme.digest' with locale 'zh-CN' ….
  • A2, keep a stored overrides on the member save: case 2 red (failed where moved).
  • A3, member read-back always accepts: case 3 red (moved where failed).
  • A4, the gate's declared_package_ships_no_such_item branch disabled (protocol-packaged-base-refusal.ts, mutated and restored, not in the diff): case 5 red (expected undefined to match object { code, status }).

Every restore: git hash-object equals the HEAD blob, git diff HEAD empty, marker absent from dist/.

Acceptance notes

Verification

All at head 2ebd0841 unless noted (the last commit after the package runs is comment-only in a test file).

  • @objectstack/metadata-protocol typecheck exit 0; test 300 files passed, 4 skipped, 22100 tests passed (at d485bebc).
  • @objectstack/objectql typecheck exit 0; test 527 files, 10143 tests passed (at d485bebc).
  • @objectstack/cli typecheck exit 0; vitest run --project unit 372 files, 5250 tests passed (at d485bebc). The integration tier is declared to CI.
  • eslint --no-inline-config over packages/metadata-protocol/src, packages/cli/src/commands/migrate and the test file: exit 0, no findings.
  • dispatch-gates --commands derived 62 families; 62 run; 59 exit 0 at first run, 3 exit 3 on prerequisites and re-run exit 0 on the same head (check-plugin-teardown-shape --self-test after fetching its pinned fixture commit; check:dual-build-cjs-loads and check:i18n-coverage once the unbuilt packages had dist). --ran with exit codes: "62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN", a derived zero.
  • Full dogfood battery: n/a (no ! in the title, no (narrowing) arm).
  • origin/main merged once before opening (79774af3).

Changeset .changeset/28904-packageless-overlays-added-member.md: @objectstack/metadata-protocol and @objectstack/cli patch, both declaration lines.


Generated by Claude Code

…il template locale the base does not ship (red)

Claude-Session: https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W
Co-authored-by: Claude <noreply@anthropic.com>
…l template row of a locale the base does not ship as the pair's own member

A package-less row of a bundle member the base does not ship (a zh-CN
translation of a template shipped en-US only) was saved into the pair
declaring an override, which the write gate refuses, so the move failed
and the row was kept. The census now names that member (addedMember) and
the move saves the body into the pair with no overrides declaration, reads
back the pair's own member row, and reports it moved with the added member.

Claude-Session: https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W
Co-authored-by: Claude <noreply@anthropic.com>
…eletes the unbound row

Claude-Session: https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W
Co-authored-by: Claude <noreply@anthropic.com>
…locale email template row as the pair's own member

Claude-Session: https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W
Co-authored-by: Claude <noreply@anthropic.com>
…s own-member move

Claude-Session: https://claude.ai/code/session_01ADHkmS3jJf5LgjHTU7Cs6W
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

11 anchor(s) derived from 2 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 42 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 f6a04e3af7756446bb7a2e73973f09e011090312 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f4edaab1cdfeb86174ea869309d8ae1ce2fabfee — the merge of head 2ebd0841c847da408d5fe49beae79a579a031112 into base f6a04e3af7756446bb7a2e73973f09e011090312, 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 f4edaab1cdfeb86174ea869309d8ae1ce2fabfee && git checkout f4edaab1cdfeb86174ea869309d8ae1ce2fabfee
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f6a04e3af7756446bb7a2e73973f09e011090312 2ebd0841c847da408d5fe49beae79a579a031112 && git checkout -B drift-repro f6a04e3af7756446bb7a2e73973f09e011090312 && git merge --no-ff 2ebd0841c847da408d5fe49beae79a579a031112

node scripts/docs-audit/affected-docs.mjs --json f6a04e3af7756446bb7a2e73973f09e011090312

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

维护者速读

席位意见:与 diff 相符。CLI 文件的仅渲染改动接受(否则预览显示错误的 overrides/basedOn)。开放问题取 A:本 PR 不关这类成员的无包写,B 另立卡,排在 #28905 之后。注册表条目文案过时的发现转 spec 车道。

席位核对

ACCEPT · domain:server seat 5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 · 2026-11-02T10:34Z · head 2ebd0841 · #28904

  1. Shape: draft against main, Fixes #28904, no other card number beside a closing keyword; patch changeset for @objectstack/metadata-protocol and @objectstack/cli; Clause-②: no, Metadata-protocol: none. Not governed, +240 / -22, 4 files. No packages/spec/**, and protocol-packaged-base-refusal.ts is only imported from (overrideWriteMemberOf), not edited.
  2. Against the order: the census names addedMember on a movable row when the base ships the name but not that member, counted through the write gate's own member reading (overrideWriteMemberOf + shippingPackageIds, no second reading). The move saves the body without overrides, reads it back as the pair's own undeclared row, then deletes the unbound row; outcome stays moved with addedMember in place of basedOn (no new outcome word, as the delta preferred). Read in the diff.
  3. Accepted deviations: packages/cli/src/commands/migrate/packageless-overlays.ts render-only edit (preview and report lines, --json addedMember), inside the claim's conditional surface; without it the listing would print a wrong overrides/basedOn.
  4. Gates: dev reports 62/62 derived gates exit 0 (0 NOT-MEASURED; 3 exit-3 prerequisites cleared and re-run), metadata-protocol 22100, objectql 10143 and cli 5250 tests green, three typechecks and lint clean; red on main first (2/4), four ablations each red then restored. Check-runs on this head were still running at ACCEPT; the seat readies it only after Lint & Repo Gates and TypeScript Type Check read success and no check is red.
  5. Out of scope: closing the package-less write of an unshipped member (option B) → a separate card after metadata-protocol: a save naming a read-only code package itself (?package=<base>) for an overlay type is still accepted and lands an overlay bound to the base #28905 lands; the unreleased prose of the spec migration-registry entry 18.packageless-overlay-write-closed still describing these rows as failing → handed to the domain:spec lane.
  6. Landing: pr_ready + automerge_enable through the relay once CI is green.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 10:54
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 10:54
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit a19759a Nov 2, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7l0dag branch November 2, 2026 11:41
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