Skip to content

fix(metadata-protocol): two locales of one email template survive a restart — sys_metadata overlay rows keyed by bundle member (#22877, stage 1) - #22954

Merged
objectstack-fleet[bot] merged 16 commits into
mainfrom
claude/issue-22877-locale-bundle-overlay
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 16 commits into
mainfrom
claude/issue-22877-locale-bundle-overlay

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Part of #22877
Clause-②: no

Stage 1 of 2 of ruling 6116165299 (letter A): the overlay store keys a bundled type's row by its member, so two locales of one email_template saved through the metadata door are two sys_metadata rows and both survive a restart. Stage 2 (the by-name member selector on the /meta doors, Clause-②: yes) is a separate PR; #22877 remains open for it.

The ruling, quoted:

A: the overlay store carries the bundled type's member as a key column; one table, one hydration path; each member addressable by name.

sys_metadata gains one nullable, type-neutral discriminator column. NULL means the canonical member, so every existing row and every type whose identity is name alone stays byte-identical. The value is taken from the body through itemDiscriminator (ITEM_KEY_DISCRIMINATORS), the one place the identity is declared.

What changed

  • Column (metadata-core sys-metadata.object.ts): bundle_member, nullable text (maxLength: 100). NULL is the canonical member and every row of a name-only type. The declared idx_sys_metadata_overlay_active gains it.
  • Index (metadata-protocol migrations/overlay-index.ts): the runtime key is (type, name, organization_id, COALESCE(package_id, ''), COALESCE(bundle_member, '')), active and draft. The change rides the existing ADR-0120 D4 probe-then-replace order unchanged: it drops and rebuilds the real name on every first write, so the next boot replaces the four-part index. A key part only relaxes uniqueness, so no rows a store already holds can block the rebuild. The duplicate-listing query, the dialect fallback and the os migrate duplicates probes all read the same builders.
  • Backfill (same module, backfillOverlayBundleMembers): through the engine, each email_template row with a NULL member whose body names a non-canonical locale gets that locale. It is run by boot hydration (loadMetaFromDb, after its own read succeeded) and by the first write. It is idempotent, never throws, and reports a failure at error with the consequence and the fix. A missing table counts as benign (isMissingTableError).
  • Repository (sys-metadata-repository.ts): put takes the member from the body (overlayMemberOfBody → itemDiscriminator), never from the caller. The write head (inherited draft base and orphan adoption included), the row payload, get, headAt, delete, promoteDraft (and its drain), restoreVersion and history all address one member. An omitted member means the canonical one. For a name-only type, no where and no row payload ever carries the column.
  • Protocol (protocol.ts): the save door addresses its stored head and its credential carry-forward by the body's member. The by-name reads that address rows directly (findServedOverlayRow, the publish door's draft read, the diff's active read and history rows) address the canonical member. A bare-name getMetaItem with no canonical stored row answers the code layer's canonical member, ahead of the registry's tier-first bare read.

Pins (the ruling's, on real code)

packages/objectql/src/environment-overlay-email-locale-restart.test.ts (real ObjectQL + real protocol, sys_metadata registered from SysMetadataObject, a restart is a new engine and protocol over the same stores):

  • two locales saved, then restart → each serves its own wording (the list the send path reads), and a re-save updates its own row;
  • CONTROL: a single-locale save is unchanged (one row, member NULL; the other locale keeps the package wording);
  • CONTROL: a package-shipped bundle is unchanged across a restart;
  • a bare-name read answers the canonical member, also when only zh-CN is customized;
  • a by-name reset removes the canonical member's overlay only;
  • a row stored before the column existed is backfilled at boot from its body, and that locale's next save updates it.

packages/metadata-protocol/src/migrations/overlay-index.test.ts (real SQLite, node:sqlite):

  • the four-part index refuses a second locale (the defect at the index level);
  • after the run the key is replaced, so two members coexist and a duplicate (type, name, member) is refused UNIQUE constraint failed;
  • the backfill writes exactly the non-canonical bodies, and every other type's rows are byte-identical (all columns) before and after the backfill and the index rebuild.

packages/metadata-protocol/src/sys-metadata-repository.bundle-member.test.ts: the repository by member. It covers two rows, canonical defaults for read, delete, history and promote, a revert refused across members, and a CONTROL that no name-only payload or where carries the column.

packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts: the showcase on a real SQLite file through PUT /meta, real sendTemplate per locale, and a cold boot.

Evidence

Every heavy run went through scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 unless noted). Each exit code was captured before any pipe.

  • Patch round 1, final head f21b6f9d24 (three commits on 288ef6fb69; no rebase and no force-push; os-dev-report 6119103318 on [finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877):
  • Head 288ef6fb69, before patch round 1 (this branch merged with origin/main e357a441c6; the incoming commits overlap none of the files here, and in metadata-core they add only meta-write-capability.ts):
    • turbo run build --filter=...@objectstack/objectql: exit 0.
    • metadata-core full suite: 17 passed, 394 passed.
    • metadata-protocol typecheck: exit 0.
    • metadata-protocol targeted (overlay-index, sys-metadata-repository.bundle-member, runtime-index-preflight, lookup-artifact-item-call-sites, i18n-bundle-list-merge, item-name-every-door): 6 passed, 72 passed.
    • objectql targeted (environment-overlay-email-locale-restart, environment-overlay-five-types, engine-security-catalog-package-door, registry-i18n-bundle-key): 4 passed, 65 passed.
    • The derived gate union, node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 288ef6fb69: 79 commands, 79 exit 0. Reconciled with --ran (exit codes recorded): 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN.
  • Before the merge:
    • metadata-protocol full suite (c7f399ba7f): Test Files 1 failed | 226 passed | 3 skipped (230), Tests 2 failed | 28183 passed. The one red file was the lookupArtifactItem call-site census, which needed the new site recorded (59841e1570); after that it is 4 passed.
    • objectql full suite, both projects (59841e1570): Test Files 404 passed (404), Tests 7864 passed.
    • At 139a2cbc9d:
      • metadata full suite: 58 passed, 871 passed.
      • Typecheck of metadata-core, metadata-protocol, objectql (with check:test-typecheck) and dogfood: all exit 0.
      • The dogfood closure build (turbo, 63 tasks): exit 0.
      • email-template-two-locales-survive-boot.dogfood.test.ts: 3 passed (3).
  • packages/cli is not touched. The repo-wide eslint sweep is CI's.

Ablation

The member was dropped from the repository's write head: const member = overlayMemberOfBody(ref.type, body); became an expression that is undefined at runtime, carrying the marker __ABLATION_22877_MEMBER_DROPPED. The change was made through scripts/ablation-replace.mjs on committed HEAD. The anchor count went 1 → 0, the marker count 0 → 1, and the blob 314afe10d8 → a28c0832a6. metadata-protocol was rebuilt (objectql resolves it through dist/), and ablation-dist-preflight confirmed the marker in dist.

  • Mutated: the restart pin file goes 3 failed | 3 passed (6). The two-locale restart pin, the bare-name canonical pin and the by-name reset pin are red (the second save updates the first locale's row). Both CONTROLs (single locale, package bundle) and the backfill case stay green, as predicted.
  • Restored: blob back to 314afe10d8, git diff HEAD empty, dist rebuilt, ablation-dist-preflight --absent exit 0, and the file goes 6 passed (6).

Deviations and residuals, stated

  • Lineage counter stays per (org, type, name), not per member. sys_metadata_history declares unique (organization_id, type, name, version), and a per-member counter would mint colliding versions under it, which would need a second index ceremony on a second table. With one counter per name, a version names exactly one snapshot. history and restoreVersion carry the member: history filters to one member, and a revert to another member's version answers 404 VERSION_NOT_FOUND.
  • History rows carry no member column. A history row's member is read off its own body (and a tombstone's off the row it removed, by previous_checksum) through the same overlayMemberOfBody. No second table and no history migration.
  • Registry heal on a by-name reset (removeRuntimeShadow) still sweeps the bundle's whole hydrated overlay group in the registry: the other member's REGISTRY entry drops until the next boot. Its row stays and is still served: the list and the send path read the store, as the reset pin shows. Member-aware registry heal belongs with stage 2's member-addressed DELETE.
  • Package-batch doors (publishPackageDrafts, discardPackageDrafts, revertCommit, the cluster mutation fan-out) address the canonical member. Before this PR two members' drafts could not coexist, so nothing they did before changes. A non-canonical member's draft is published by a save naming its locale, or by stage 2's selector.
  • os migrate duplicates on a store this version has not booted yet (a sys_metadata table without the member column): the overlay pre-flight first reads whether the column is present. When it is not, it lists duplicates with every row read as the empty member, so a live duplicate is still reported blocked and is never reported unreadable. Both statements live in overlay-index.ts. The choice between them is made in runtime-index-preflight.ts, which reports the statement that ran as the entry's duplicateSql. The report's keys are unchanged. On such a store, an email_template pair whose bodies name different locales can be reported and then not block once the serving boot's backfill has run. That errs in the strict direction. (Patch round 1, f21b6f9d24. This fixes the packages/cli duplicates integration red on 10f3b686f4.)
  • A driver without partial indexes (the ceremony's fallback arm, MySQL): the rebuild is refused there, as before, and is reported in the server log at the first metadata write. A store that booted before keeps its four-column declared UNIQUE under its name, because syncDeclaredIndexes skips by name. That index has never constrained an environment-wide overlay row: the metadata door stores no organization (ADR-0131 D6), and a UNIQUE key does not collide NULLs, in either the direct index or driver-sql's hash-shadow form. So two languages of one template coexist there too, and no save is refused. This was measured on SQLite (two members with organization_id NULL are both accepted; CONTROL: a non-NULL organization refuses the second member). It is not measured on a live MySQL. os migrate plan lists the declaration change as a destructive recreate_index, applied with os migrate apply --allow-destructive. A fresh store gets the five-column declaration. (Corrected after patch round 2: the earlier wording here said such a save is refused, which the dev's measurement 6119963033 falsified.)

Acceptance notes

  • i18n bundles: the regenerated zh-CN, ja-JP and es-ES bundles carry the English source text for the new bundle_member keys, which still need translating. check:i18n-coverage stays green.
  • Caller census: packages/types/src/driver-error-classification.callers.test.ts (a domain:cli file) gains one row for overlay-index.ts, the edit class the seat named. The sentinel pin sits in metadata-protocol.
  • Surface: metadata-protocol/src/migrations/runtime-index-preflight.ts (and its test) is outside the claim's listed files but inside its package and directory. It is the module that reads overlay-index.ts's statements. The edit is one function and a private table, and the seat accepted it as an in-lane deviation.
  • packages/drivers/driver-sql/src/sql-driver-overlay-index-drift.test.ts keeps its own four-part fixture. It tests the drift detector's reading of a COALESCE key part and imports nothing changed here, so it stays a valid detector fixture.
  • Two objectql harnesses (environment-overlay-five-types.test.ts, engine-security-catalog-package-door.test.ts) hand-declare sys_metadata. They gain the bundle_member field the platform object declares, because the engine refuses an undeclared field in a where.
  • protocol.lookup-artifact-item-call-sites.test.ts records the new lookupArtifactItem site with its disposition. scripts/engine-double-contract.pinned.json records the new test file's pinned doubles (--write, additions only).

Generated by Claude Code

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

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/platform-objects, touching 51 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 6a40a09ed952f83b1f765d47cd76aee4a41f6672.

⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 35 pages)
  • 8 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 15 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 6a40a09ed952f83b1f765d47cd76aee4a41f6672 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6a40a09ed952f83b1f765d47cd76aee4a41f6672

⚠️ 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 6a40a09ed952f83b1f765d47cd76aee4a41f6672 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… of the bundle-member backfill; enumerate its operatorFacingErrorText call

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
….bundle_member (node scripts/check-i18n-bundles.mjs --write)

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI red, not this PR's · domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T05:54Z.

…dates the member column, every row the empty member

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f21b6f9d24483ad9ff10b1b8bef00008347758ca
Local-runs: none

Reviewed: card #22877 (body and all 13 comments; ruling 6116165299 binding), PR #22954 (body, both comments, the 24-file list, the net diff against main at merge base e357a441c6, and patch round 1 alone, 288ef6fb69..f21b6f9d24), the head's check-runs, and the origin/main files the brief lists. The file list touches no governed path; this is the contract-tier review the ruling asked for the migration half.

Gates on the head, at the final read: 42 check-runs, none pending (the second PR Automation run's Check Changeset had completed success). 40 are green or skipped, every required context among them except Test Core. Two are red, both the open-card signatures and not this diff's: Test Core (6/6) is red at the step "Check this shard's timing drift" only — "Run this shard's tests" and "Test completeness guard" on that shard are success (#22966); the Test Core aggregator is red at "Check the per-package suite-duration ceilings" and its "Verify test shard results" roll-up only (#22949). The three reds of head 288ef6fb69 — Test Core (1/6), Test Core (3/6), Type Check · consumer gates — are green on this head, the four i18n steps of the consumer gates included.

① Derived judgments

Each judged against ruling 6116165299 (letter A, Q2 i), on the diff and the origin/main code it lands in.

  • The member column — right. SysMetadataObject gains bundle_member, nullable text, maxLength 100, documented as written from the body and never authored. overlayMemberValue answers undefined for every type outside ITEM_KEY_DISCRIMINATORS, and every caller — whereFor, the row payload in put, overlayMemberWhere, historyRowsOfMember — adds the column only when that answer is defined, so a name-only type's reads and writes are byte-identical (CONTROL pinned in sys-metadata-repository.bundle-member.test.ts over every payload and every where; the backfill case pins every other type's rows equal, all columns). NULL is the one spelling of the canonical member: overlayMemberValue folds en-US, an empty value and a missing value to null, so a pre-column row and a post-column row of one member cannot sit under two keys. The additive schema sync (ALTER TABLE ADD COLUMN) provisions the column on a table that booted before; a store whose sync did not run fails its first member-keyed read or write loudly, and the backfill reports at error with the fix.

  • The declared index and both runtime partial indexes — right, with one residual to state. The declared idx_sys_metadata_overlay_active gains the column; OVERLAY_INDEX_COLUMNS and OVERLAY_NULL_SENTINELS gain it, folded to the empty string the way package_id is, so overlayIndexKeyParts, buildOverlayIndexSql (active and draft), buildOverlayFallbackIndexSql and buildOverlayDuplicateProbeSql read one key (pinned as literals in overlay-index.test.ts). The ceremony is probeThenReplaceIndex, unchanged: the new form is built under the probe name over the live data and dropped, and only then is the real name dropped and rebuilt. A store that booted before holds the four-part partial index; the five-part key is a strict relaxation of it, so no row that index admitted can fail the probe, and the real-SQLite pin starts from the four-part index and watches it replaced. The one row set that can still block is the one [metadata-protocol] ensureOverlayIndex 先 DROP 后 CREATE:partial 索引建失败时 sys_metadata 会静默地失去覆盖层唯一约束 #6418 already reports — two package-less active rows distinct only by a NULL package_id under the declared, never-migrated index — and the conflict arm reports it exactly as before. syncDeclaredIndexes skips by name, so the five-column declaration never replaces the live four-part index by itself; the ceremony does, lazily, at the first protocol write (ensureOverlayIndex, reached from the eight write doors), not at boot — a member insert through a path that bypasses those doors before any protocol write would meet the old index once, loudly. The SQL drift planner cannot misread the result: isSyncReproducibleIndex answers false for any partial index, so the five-part runtime index is runtime-managed and never a drift op. A driver without partial indexes (the fallback arm, MySQL): the probe fails unsupported, the non-unique composite fallback is added without a drop, and the degradation report names the dialect (its text now names canonical members too). On such a store that booted before, the four-column declared UNIQUE stays under the name, so a second member's save — an INSERT under the new write head — is refused by that index: loud, where before it merged silently; os migrate plan lists the declaration change as a destructive recreate_index for the operator. A fresh store there gets the five-column declared UNIQUE and two members coexist. The survival pin is therefore delivered on the ceremony's dialects, where the pins run (real SQLite, the showcase); on the fallback dialect the defect changes shape from silent replacement to a refused save. That is the ceremony's standing scope, which the ruling chose, and not a breach — but neither the PR body nor the changeset states it; the seat should add the sentence.

  • The backfill — right. backfillOverlayBundleMembers runs through the engine (find by type, update by id, as system), over the discriminated types only and in every organization; skips a row that already carries a member (idempotent, pinned); parses the body and writes only a non-canonical member. A row whose body has no locale is left NULL: itemDiscriminator answers the canonical en-US for a missing field, overlayMemberValue folds it to null, nothing is written — the ruling's "NULL means the canonical member" and the schema's own default. An unparseable body is counted and left NULL. It runs from loadMetaFromDb once the hydration read has answered, and from ensureOverlayIndex before the first write, once per protocol instance; it never throws; a missing table is benign through isMissingTableError; any other failure is an error-level report naming the consequence and the fix, retried at the next boot. Its order against the index rebuild is free, as the module says: the new key only relaxes the old one.

  • The repository — right; one member, never a sibling. put takes the member from the body through overlayMemberOfBody, never from a caller; resolveWriteHead narrows the inherited-draft base, the head lookup and the orphan-draft adoption to it; the row payload carries the column only when defined, on insert and update alike. get, headAt, delete, promoteDraft (the draft read, the active parent read, the put, and the drain — dropPromotedDraftRow receives the option bag whole, so the member it was promoted from reaches the delete), restoreVersion (refuses a version whose history body belongs to another member with 404 VERSION_NOT_FOUND, and reads the active row by member) and history (historyRowsOfMember: a row's member from its body, a tombstone's from the row it removed via previous_checksum; an unstamped parent or an unparseable body reads as canonical) all address one member; omitted means canonical. Pinned on the repository alone, the cross-member revert refusal and the member-scoped drain included.

  • The save door — right. saveMetaItem hands itemDiscriminator of the body to storedHeadAt for its parent-version resolution and to the credential carry-forward (storedBodyForCarryForward), so the head the optimistic lock judges is the row put will key; both derive from the same body.

  • By-name reads and deletes with no selector — right. findServedOverlayRow adds overlayMemberWhere (canonical) for a bundled type and nothing for every other type; the publish door's draft read and the diff door's history rows (historyRowsOfMember) and active row address the canonical member; deleteMetaItem pre-reads through repo.get and deletes through repo.delete, both canonical by default; the history door (repo.history) and the revert doors (repo.restoreVersion) default to canonical. getMetaItem canonicalises request.type at its head, so the new block's discriminator lookup sees the singular. When only a non-canonical member is overlaid, a bare getMetaItem finds no canonical stored row and serves the code layer's canonical member through lookupArtifactItem (getArtifactItem is canonical-first inside each tier, independent of registration order); only a bundle that ships no canonical member at all falls to the registry's bare read, which answers its overlay tier first — and there no canonical member exists to answer. Pinned: the package wording for a zh-CN-only customisation, before and after a restart. The ruling's premise that the registry's bare read already answers the canonical member is false by SchemaRegistry.getItem's own code (bare tier before local and other; canonical-first only inside a tier); the dev implemented the operative half at the protocol and left registry.ts alone, as the claim required. One note: overlayLockLayerAt still gathers every active row of the name and reduces to the strictest lock document — a protection envelope, never a body, in the conservative direction; unchanged by this diff and acceptable.

  • The shared version counter (the seat's Q2 answer A) — right. nextItemVersion is per (organization, type, name) from the maximum in sys_metadata_history, and that table declares unique (organization_id, type, name, version), so a per-member counter would collide or need a second ceremony on a second table. With history filtered by member, the diff door filtered the same way, restoreVersion refusing a sibling's version, and revertCommit items restoring under the canonical member (a commit item recorded for a non-canonical member answers 404 there rather than restoring the wrong member), no history or revert path reaches a sibling member's snapshot. The visible cost is non-consecutive version numbers within one member. A faithful reading of "the version counter carries the member": what that sentence protects holds.

  • Patch round 1 (three commits, +247/−14 over 13 files) — right.

    • operatorFacingErrorText in the backfill's catch: kept, with its census row (calls: 1) and a sentinel pin in raw-exec-operator-detail-16657.test.ts for both legs (find with the synthetic bound statement; update binding the row id, which is the sentinel). The helper's answer reaches exactly two carriers — the result's detail and the logProblem report — and both are pinned sentinel-free with the dialect's words intact; the success line carries a count only, and ensureOverlayMembersBackfilled discards the result. The sentinel leaks nowhere. Keeping the call is right: the engine-level backfill has no enumerated reporting site, and the helper is the one cut.
    • The seven regenerated platform-objects bundles: en rewritten from the source; the three translated locales carrying the English text with their source-hash companions recording the fill — the generator's documented merge mode; check:i18n, check:i18n-stale-fill and check:i18n-coverage are green on the head.
    • The pre-flight: buildOverlayMemberPresenceSql (a zero-row select of the column) and buildOverlayDuplicateProbeSqlWithoutMember (grouped by the other four key parts, the member projected as the constant empty key) live in overlay-index.ts; runProbe tries the presence statement — no result set reads unreadable, as every statement there is judged; a refusal selects the member-less listing — and reports the statement that ran as the entry's duplicateSql. A pre-column store cannot answer anything other than its true state under the key the rebuild will judge: every row there is the empty member, so grouping by the other four parts IS the five-part grouping of that table — no under-report is possible, and the groups are the ones the full statement would find. The one over-report is the strict case the dev names: a discriminated-type pair whose bodies name different locales, which the serving boot's backfill separates. Report shape: RuntimeIndexPreflight's keys are unchanged (status, groups, duplicateSql, keyParts, detail); group keys are built generically from the row's columns, so bundle_member_key appears inside groups[].key on every listing, full and member-less alike; the CLI consumer (duplicates.ts) reads status and rowCount, and its contract and integration tests are green in Test Core (3/6). A presence-probe failure other than a missing column also routes to the member-less statement; on a post-column store that could list a legitimate two-member pair as blocked — the strict direction, with the statement that ran in the entry.
  • Exports — no change at the package doors. metadata-protocol's index.ts is untouched and the package has one entry; overlay-index.js's new names and historyRowsOfMember are not re-exported. SysMetadataRepository's option bags gain an optional member (additive). metadata-core's SysMetadataObject gains a field and an index column (additive). ITEM_KEY_DISCRIMINATORS is untouched; PUT stays body-addressed; ?locale= is untouched.

  • The ruling's pins, on the head: two locales then a restart each serve their own wording (environment-overlay-email-locale-restart.test.ts on the real engine and protocol; the showcase dogfood on a real SQLite file with real sendTemplate and a cold boot); a single-locale save unchanged (one row, member NULL, the other locale on the package wording); a package-shipped bundle unchanged across a restart; a bare-name read answers the canonical member; a duplicate (type, name, member) save refused by the index with UNIQUE constraint failed on real SQLite, the canonical NULL folded to one bucket; every other type's rows byte-identical before and after the backfill and the rebuild. The ablation the PR records removed the member from the write head and reddened exactly the three member pins while both CONTROLs stayed green.

② Semver level

  • The changeset names @objectstack/metadata-protocol and @objectstack/metadata-core at patch; Clause-②: no is PR body line 2, the changeset's declaration and the claim's. True for stage 1: no door accepts a new key or parameter; the save door's body-addressed PUT is unchanged; the only behavioural movement is the store keeping a second member it used to replace. Stage 2's selector is the widening, not this PR. Patch fits a fix in released packages; the added column and the option bags are not an authorable surface.
  • Packages: the diff also changes @objectstack/platform-objects' generated bundles — a published package the changeset does not name. All three packages are in the fixed group of .changeset/config.json, so they version together whatever the changeset lists; the omission changes nothing in what publishes or at which version, and leaves only the platform-objects CHANGELOG without a line for the regenerated bundles. A note, not a defect in the level.
  • Narrowing: none. The index replacement is a relaxation and cannot refuse a store that booted before; the conflict class it still reports pre-dates this PR. The backfill never throws. The pre-flight lists and never refuses, erring only strict. No ADR-0087 marker is owed. On the fallback dialect, one request shape — a second member's save — is refused by the surviving four-column declared index until the operator rebuilds it; that is a refused save where the store previously accepted and lost, not a deployment refusal, and it belongs in the changeset text (see ①), not in a marker.

③ Boundary flags

  • Seat answer Q1 (sequencing A) — right against the ruling, which dispatches "two steps (or two PRs)" and gives the migration half the contract-tier review either way. The window stage 1 leaves: a non-canonical member is written by a save naming its locale and served by the layered list and the send path, but with no selector it cannot be reset, have its draft published, or be read, diffed or reverted by name until stage 2's selector lands; a pre-fix second-locale row, once backfilled, is no longer removed by a bare DELETE. The changeset states it in substance — a by-name reset removes the en-US customisation only; read, publish, history and revert by name address the en-US member; a non-en-US customisation is reached through the list and replaced by a save naming its locale — without naming the by-name selector as the closer. One sentence would complete it.
  • Seat answer Q2 (shared counter A) — right, as judged in ①: a reading within the ruling, not a change to it.
  • The premise half-false (the registry's bare read answers its overlay tier first): verified in SchemaRegistry.getItem; the protocol carries the operative half; objectql/registry.ts is untouched; carried to stage 2 together with the member-aware registry heal.
  • runtime-index-preflight.ts outside the claim's listed files: same package, same directory, same lane; one function (runProbe) and one private table; the statements themselves live in overlay-index.ts, as that module's ownership rule demands; without the edit os migrate duplicates answers unreadable on every store this version has not booted. Accepted as an in-lane deviation; the adopting seat accepts it by adopting this record.
  • The domain:cli census row (packages/types/src/driver-error-classification.callers.test.ts): the frozen enumeration of operatorFacingErrorText callers, whose own header makes the PR that adds a caller add its row and its pin; one row, additions only, the pin in metadata-protocol. Acceptable on its merits. The claim on the card does not pre-name this edit class, so it is judged here rather than taken from the dispatch.
  • The untranslated bundle keys: the English fill in zh-CN, ja-JP and es-ES is the generator's merge-mode output, recorded in the source-hash companions and accepted by the i18n gates on the head; the two strings are a system table's field label and help. Translation is owed to the area:i18n lane as a follow-up; not a stage-1 blocker.
  • Dev findings carried to stage 2, verified: removeRuntimeShadow sweeps a bundle's whole bare group on a by-name reset (its own comment says the overlay layer is keyed by name alone — the premise this PR retires); the other member's row stays, and the list and the send path read the store, as the reset pin shows. applyRemoteMetadataMutation re-reads through repo.get, so a cluster peer hydrates the canonical member for a non-canonical mutation until its next boot; the package-batch doors address the canonical member. The driver-sql drift fixture stays valid: recognition is by the partial predicate, not the part count.
  • First-stage deviations (the version counter, history rows without a member column, the merge of main, the pre-existing branch ref, the lock-discipline slip, the model-free trailers): answered above, or process-only with no bearing on the diff. Patch-round deviations (the strict direction; the PR body edits owed): the seat's refreshed body carries the member-less-listing bullet, the head's evidence and the acceptance notes.
  • First-dev report (6113736478): its two questions were answered by the ruling; its folded finding (a bare-name read serving the second locale) is fixed and pinned here; its translation note stands.
  • Notes for the adopting seat, none blocking: (a) add the fallback-dialect residual to the PR body and the changeset; (b) ADR-0005's present-tense amendment still spells the current key as four parts — the maintainer's 「同意」 on A is the decision's provenance, and an amendment line is Tier H, the maintainer's hand: file it; (c) origin/main moved on protocol.ts and sys-metadata-repository.ts since the merge base (GitHub reports the PR mergeable) — the queue rebuilds, and a merge of main before arming is multi-agent §10's own rule; (d) the PR stays draft and waits on ci: PR-time Test Core (6/6) reds on shard-timing-drift at 1.60–1.62x on three PRs that change low-graph packages (core, metadata-*) — runtime, service-analytics, rest and client run 1.4–2.4x their refreshed weights #22966 and ci: merge-queue Test Core reds on the new suite-duration ceiling — @objectstack/cloud-connection measured 177.5 s and 200.3 s vs its 163.89 s ceiling, in two groups of PRs that do not touch it (live since 1bd612bd6) #22949 for arming, as the seat's CI comment says; the review itself is not blocked by those two steps.

Implemented-by: claude/issue-22877-locale-bundle-overlay
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-12T06:59Z. The record came from an isolated at-tier reviewer. Its transcript shows read-only tool use: REST reads, a fetch into a review ref and repository reads, and its only write was its own scratch record. The seat accepts the runtime-index-preflight.ts edit as an in-lane deviation, as the record judges. Before arming, the seat acts on the record's notes:

  • (a) and the ③ window: a patch round adds two sentences to the changeset. One gives the fallback-dialect residual: on a driver without partial indexes, a store that booted before keeps its four-column declared UNIQUE, so a second member's save is refused until the operator rebuilds it.

    ⚠️ Correction · seat 2 · 2026-10-12T07:46Z: patch round 2's measurement (6119963033) falsifies the "refused" part, in this note and in the record's ① and ②. Metadata-door rows store no organization, and the four-column UNIQUE never collides NULLs, so on that dialect two members coexist and no save is refused. The changeset now states the verified behaviour, and the delta record on the next head adjudicates it. The other names stage 2's by-name selector as what closes the reset and publish window.

  • (c): the same round merges origin/main once. The PR body carries the residual too. A delta record follows on that head.
  • (b): ADR-0005's four-part key spelling is Tier H, the maintainer's hand. It is filed at landing, when it becomes false on main.
  • (d): arming waits for a green head, after the domain:devx change ruled on ci: merge-queue Test Core reds on the new suite-duration ceiling — @objectstack/cloud-connection measured 177.5 s and 200.3 s vs its 163.89 s ceiling, in two groups of PRs that do not touch it (live since 1bd612bd6) #22949 (6119186511) lands.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4d6105d8baa931a5e0731218aebbd96578430556
Local-runs: none

Delta record on the head above, following the PASS record 6119363786 on f21b6f9d24. Reviewed: card #22877 (body and all 14 comments; ruling 6116165299 binding; patch-round reports 6119103318 and 6119963033), PR #22954 (body with the seat's re-edited fallback-dialect bullet, its three comments including the prior record and the seat's correction on its adoption note, the 24-file list), the delta f21b6f9d24..4d6105d8ba, the net diff against main at the new merge base 246edf5b39, the head's check-runs, and the origin/main files the brief lists. Where this record is silent, the prior record's judgment stands; where it speaks on the fallback dialect, it supersedes the prior record.

Gates on the head, at the final read: 42 check-runs, all completed, none pending: 35 success, 5 skipped, 2 failure. Both Check Changeset runs (the second PR Automation run included) are success. The two reds are the open-card signatures and not this diff's: Test Core (6/6) is red at the step "Check this shard's timing drift" only (#22966); the Test Core aggregator is red at "Check the per-package suite-duration ceilings" and its "Verify test shard results" roll-up only (#22949). Every other check is green or skipped — Type Check · consumer gates, Lint & Repo Gates, Build Core, the three Dogfood Regression Gate shards and their aggregator, Dogfood Verify CLI, Temporal Conformance (live PG + MySQL) among them.

① Derived judgments

  • The delta changes no code; nothing else the prior record judged moves. Since f21b6f9d24 the branch gained two changeset commits — 8a092fa7e8 (+3/−1 on .changeset/22877-overlay-locale-member.md) and c3a7db0595 (one line of the same file) — and the merge 4d6105d8ba of origin/main 246edf5b39 (parents c3a7db0595 and 246edf5b39). git show --remerge-diff of the merge is empty: the committed tree is the automatic merge, with no hand resolution. The net diff against the new base is the same 24 files, +1755/−70 (it was +1753/−70 against e357a441c6; the two lines are the changeset's). For 23 of the 24 files the PR's hunks are byte-identical before and after the merge once hunk offsets are ignored — protocol.ts, sys-metadata-repository.ts, environment-overlay-five-types.test.ts and engine-security-catalog-package-door.test.ts included, although main's feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874 rename changed each of those four files in other regions; the one file that differs is the changeset, by exactly the stage-2 sentence and the MySQL paragraph. So every judgment the prior record rendered on the code — the member column, both runtime partial indexes, the backfill, the repository, the save door, the by-name doors, the shared version counter, patch round 1's three fixes, the exports, the ruling's pins and the ablation — is carried unchanged onto this head.

  • The contested point: the code supports the dev's reading, and this record supersedes the prior record's ① and ② sentences on it. Walked on origin/main and the head:

    • The declared index. SysMetadataObject.indexes names idx_sys_metadata_overlay_active with unique: 'global', four columns on main (type, name, organization_id, package_id), five on the head.
    • How driver-sql materialises it. SqlDriver.syncTableIndexes hands every declared index to syncDeclaredIndexes, which normalises it through normalizeDeclaredIndex. That function marks a NULL-safe key part only for unique: 'organization'; for 'global' the listed columns are taken verbatim and nullSafeColumns stays empty, so the create is knex's table.unique(columns) over the bare columns, no COALESCE. The docblock states the consequence in so many words: NULL-distinct semantics are the default across SQLite, Postgres and MySQL, so multiple NULL rows remain allowed while non-NULL duplicates are rejected. The sync also skips any name that already exists (existing.has(name)), which is why a store that booted before keeps the four-column index under that name.
    • The hash-shadow form on MySQL. It is attempted only after the server refuses the direct key. createHashShadowUniqueIndex builds a stored generated column UNHEX(SHA2(CONCAT(part, 0x1f, part, ...), 256)) where a plain part is the bare column and only a NULL-safe part is spelled COALESCE; its docblock, measured on live MySQL 8.0.46, records that CONCAT is NULL when any argument is NULL and that the UNIQUE key does not collide NULL shadows — "an any-NULL tuple still conflicts with nothing". A shadow from before df1c75c4b hashed the raw columns, which for a 'global' index is the same key, so hashShadowColumnState reads it as reusable and the semantics are the same. The history of sql-driver.ts holds no CONCAT_WS spelling that would have folded NULLs.
    • What the door writes. The protocol's getOverlayRepo is repositoryFor(null), which constructs SysMetadataRepository with organizationId: null; SysMetadataRepository.put writes organization_id: this.organizationId on the sys_metadata row and on the history row, inside a transaction context that carries no tenantId. The engine's resolveSystemInsertOrganization returns before resolving for a platform-namespace object the inventory does not classify tenant-scoped: sys_metadata has no entry in PLATFORM_OBJECT_TENANCY, so isPlatformObjectOutOfTenantAuditScope answers true and no tenantId reaches SqlDriver.injectTenantOnInsert (which would otherwise overwrite an explicit NULL, so this early return is load-bearing). The one repository bound to an organization, legacyOrganizationRepo, is used by removeLegacyOrganizationRowOnUninstall for a delete and never for a write. So every row the metadata door writes stores organization_id NULL — ADR-0131 D6 and D7 as the code implements them. The showcase pin (email-template-two-locales-survive-boot.dogfood.test.ts, booted with orgContext: true) reads the served document back through GET /meta after a cold boot, a read that findServedOverlayRow scopes to organization_id NULL; it is green in the dogfood gate on this head.
    • Therefore. Under the kept four-column UNIQUE, a second member's row — same type, name and package, NULL organization — is a tuple with a NULL part, and neither the direct index nor the shadow collides it with the first. Two members coexist on that dialect and no save is refused. The prior record's ① sentence (a second member's save is refused by that index, loud) and its ② sentence (one request shape is refused until the operator rebuilds it) are false by this code. They are superseded by this record. The seat's correction on the adoption note is right. The dev's SQLite measurement is a scratch probe, not in the diff, and is not what this reading rests on: it rests on the driver code, and SQLite's NULL-distinct UNIQUE is the rule the driver documents for all three dialects.
    • What does hold of the prior residual. The index is kept under its name: on the fallback arm probeThenReplaceIndex fails before any drop, buildOverlayFallbackIndexSql is additive CREATE INDEX IF NOT EXISTS (refused outright on MySQL proper, a no-op on a name that exists elsewhere), and reportDegradation says the index that carried the name is left exactly as it was. os migrate plan reports the declaration change: diffManagedIndexes compares the expected five-column unique against the physical four-column one, a plain four-column UNIQUE is isSyncReproducibleIndex and so not runtime-managed, and the entry is index_mismatch, category destructive, op recreate_index, with a message naming os migrate apply --allow-destructive; a shadow-carried index is compared through its read-back key and reaches the same entry, with the one documented fail-safe that a shadow whose generation expression cannot be read reports nothing. A fresh store gets the five-column declaration from syncTableIndexes at create. And the standing scope: on that dialect the index-level refusal of a duplicate (type, name, member) is not delivered, as the four-part key's never was there; one row per member is kept by the write head (resolveWriteHead updates the member's existing row) and not by an index. That is the ceremony's scope the ruling chose, unchanged by this PR.
  • The MySQL paragraph, read against the code — true, sentence by sentence. "The rebuild above is refused there, as before, and reported in the server log at the first metadata write": ensureOverlayStateIndex answers unsupported, reportDegradation reports through logProblem, which calls the logger's error — the protocol passes console, so the server log — and it is reached from the protocol's ensureOverlayIndex, which runs once per protocol instance from the doors saveMetaItem, promoteDraftForPublish, publishPackageDrafts, discardPackageDrafts, revertStoredPackage, revertCommit and rollbackMetaItem; boot hydration (loadMetaFromDb) runs ensureOverlayMembersBackfilled only. So "at the first metadata write", not "at boot" — the correction in c3a7db0595 is right. One precision, not a defect: listDrafts, a read door, also calls ensureOverlayIndex, so a console that lists drafts before any write sees the report then. "A store that booted an earlier version keeps the four-column declared index under idx_sys_metadata_overlay_active, because the schema sync skips an index whose name already exists": syncDeclaredIndexes, verified above. "That index has never constrained an environment-wide overlay row: those rows store no organization, and a UNIQUE key does not collide NULLs. So two languages of one template coexist there too, and no save is refused": verified above. "os migrate plan lists the declaration change on that index as a destructive recreate_index. To bring the index to the five-column declaration, run os migrate apply --allow-destructive": diffManagedIndexes, verified above. "A fresh store gets the five-column declaration": verified above. The PR body's matching bullet says the same, adds that the NULL-distinct rule holds "in either the direct index or driver-sql's hash-shadow form" — exactly what createHashShadowUniqueIndex guarantees — and states that the behaviour was measured on SQLite and not on a live MySQL, which is honest: no gate on the head exercises sys_metadata on MySQL (the live PG + MySQL conformance job is another surface). Both texts are true to the code.

  • The stage-2 selector sentence — true to the diff. "Resetting, publishing or reading a non-en-US language by name needs the by-name member selector, which arrives with stage 2 of [finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877; until then those doors address the en-US member." findServedOverlayRow adds overlayMemberWhere(type), the canonical member, for a bundled type; the bare-name getMetaItem block answers the canonical member and, with no canonical row stored, the code layer's canonical member; the publish door's draft read carries overlayMemberWhere(singularType); deleteMetaItem pre-reads and deletes through SysMetadataRepository.get and delete, whose omitted member means canonical; the diff and history doors use historyRowsOfMember and the revert doors restoreVersion the same way. ?locale= on /meta keeps its meaning (the UI translation locale) and is no selector. The sentence names stage 2 as the closer of the window the prior record's ③ described; that note is answered.

② Semver level

  • Unchanged. The changeset still names @objectstack/metadata-protocol and @objectstack/metadata-core at patch; Clause-②: no is PR body line 2 and the changeset's own line, both untouched by the delta. Right for stage 1: no door widens or narrows its accept set, and the only behavioural movement is the store keeping a second member it used to replace. The platform-objects generated bundles move with the named packages in the fixed group — the prior record's note, not a defect in the level.
  • The fallback-dialect reading and refusals. Under neither reading is a deployment refused: the ceremony is best-effort (ensureOverlayIndex catches, and every arm of ensureOverlayStateIndex returns a status), the column arrives by the additive schema sync, and the declared index's drift is report-only until an operator chooses --allow-destructive. Under the corrected reading no request is refused on MySQL either: the one refused request shape the prior ② named does not exist, so the sentence that put it "in the changeset text, not in a marker" is withdrawn with it. The only MySQL-side residual is the declared index's drift entry, operator-paced. No narrowing, no ADR-0087 marker owed; patch holds with more room than the prior record allowed it, not less.

③ Boundary flags

Every deviations entry of report 6119963033, answered:

  • "Note 1 is not written as the review phrased it." Right to check and right in the reading. A reviewer's note is not a ruling; a changeset ships to consumers as CHANGELOG.md, so verifying a note against the code before writing it is what the changeset rule asks. The dev named the disagreement in the report instead of writing the note as given, which is the correct shape for a deviation. Adjudicated in ①: the dev's reading is the code's; the prior record's two sentences are superseded; the seat's correction on the adoption note and the re-edited PR body bullet align the three texts. Nothing further to edit.
  • Two changeset commits instead of one (c3a7db0595 corrects "reported at boot" to "reported in the server log at the first metadata write", as a new commit because amend is barred). Acceptable, and the correction is right against the call sites of ensureOverlayIndex (seven write doors plus listDrafts; loadMetaFromDb runs only the backfill). Two small commits on a branch that is never rebased is the repo's own discipline; nothing to fold.
  • No second merge of main (merged 246edf5b39; origin/main has since moved to a2240c4dcf; merge-tree was clean). Verified independently: the three commits since the merged base (a2240c4dcf, 19805d8956, 117817ec28) touch none of the PR's 24 files, so a second merge would change no PR hunk; GitHub reports the PR mergeable (its blocked state is branch protection, not a conflict). Acceptable. Merging main before arming stays the seat's act at arming time, when main will have moved again.
  • check:dual-build-cjs-loads exit 3 then 0 (the full packages build was the prerequisite in a fresh worktree). A local-prerequisite note, measured and re-run; process-only, no bearing on the diff; Build Core and Lint & Repo Gates are green on the head.
  • Worktree recreated and removed; the changeset gates and check:i18n ran outside the lock. Process-only; Check Changeset (both runs) and Type Check · consumer gates are green on the head.
  • "Not measured on a live MySQL" stands as both texts say it; this record's reading does not depend on the measurement.

Carried from the prior record, unchanged by the delta: the in-lane runtime-index-preflight.ts edit (accepted by the adopting seat on adoption); the domain:cli census row with its sentinel pin; the untranslated bundle_member keys (a follow-up owed to the area:i18n lane, not a stage-1 blocker); the findings carried to stage 2 (removeRuntimeShadow's whole-group sweep, applyRemoteMetadataMutation's canonical re-read, the package-batch doors addressing the canonical member); ADR-0005's four-part key spelling (Tier H, the maintainer's hand, filed at landing as the seat recorded); and arming waiting on a green head after #22966 and #22949, which this review does not block. Notes for the adopting seat, none blocking: (a) the prior record's ① and ② sentences on the fallback dialect are superseded here, and the adoption note already carries the correction; nothing to post beyond this record; (b) an optional precision for a later round, not this one: the fallback report can also arrive at the first listDrafts call.

Implemented-by: claude/issue-22877-locale-bundle-overlay
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-12T08:01Z. The record came from an isolated at-tier reviewer. Its transcript shows read-only tool use: REST reads, a fetch into a review ref, and repository reads and greps. Its only write was its own scratch record. The seat independently confirmed the merge-invariance of the PR's hunks before dispatching it. With this record, the fallback-dialect statements in the prior record's ① and ② are superseded, and the PR body, the changeset and the adoption-note correction now agree. Arming waits for a head whose required contexts are all success, after the C2 change for #22949 (PR #22973) lands. At arming, the dev merges main once if it has moved.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 09:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 5677add Oct 12, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22877-locale-bundle-overlay branch October 12, 2026 09:28
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/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants