Repository navigation
fix(metadata-protocol): two locales of one email template survive a restart — sys_metadata overlay rows keyed by bundle member (#22877, stage 1) - #22954
Conversation
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>
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>
… doubles Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…cale-bundle-overlay
📓 Docs Drift CheckThis PR changes 3 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
… 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>
|
CI red, not this PR's ·
|
…dates the member column, every row the empty member Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewed: card #22877 (body and all 13 comments; ruling 6116165299 binding), PR #22954 (body, both comments, the 24-file list, the net diff against Gates on the head, at the final read: 42 check-runs, none pending (the second ① Derived judgmentsEach judged against ruling 6116165299 (letter A, Q2 i), on the diff and the
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Adopted by
|
…ember selector 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>
…cale-bundle-overlay
Contract reviewServed-tier: Delta record on the head above, following the PASS record 6119363786 on Gates on the head, at the final read: 42 check-runs, all completed, none pending: 35 ① Derived judgments
② Semver level
③ Boundary flagsEvery
Carried from the prior record, unchanged by the delta: the in-lane Implemented-by: VERDICT: PASS Adopted by |
…cale-bundle-overlay
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_templatesaved through the metadata door are twosys_metadatarows and both survive a restart. Stage 2 (the by-name member selector on the/metadoors,Clause-②: yes) is a separate PR; #22877 remains open for it.The ruling, quoted:
What changed
metadata-coresys-metadata.object.ts):bundle_member, nullable text (maxLength: 100). NULL is the canonical member and every row of a name-only type. The declaredidx_sys_metadata_overlay_activegains it.metadata-protocolmigrations/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 theos migrate duplicatesprobes all read the same builders.backfillOverlayBundleMembers): through the engine, eachemail_templaterow 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 aterrorwith the consequence and the fix. A missing table counts as benign (isMissingTableError).sys-metadata-repository.ts):puttakes 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),restoreVersionandhistoryall address one member. An omittedmembermeans the canonical one. For a name-only type, nowhereand no row payload ever carries the column.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-namegetMetaItemwith 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(realObjectQL+ real protocol,sys_metadataregistered fromSysMetadataObject, a restart is a new engine and protocol over the same stores):zh-CNis customized;packages/metadata-protocol/src/migrations/overlay-index.test.ts(real SQLite,node:sqlite):(type, name, member)is refusedUNIQUE constraint failed;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 orwherecarries the column.packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts: the showcase on a real SQLite file throughPUT /meta, realsendTemplateper locale, and a cold boot.Evidence
Every heavy run went through
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0unless noted). Each exit code was captured before any pipe.f21b6f9d24(three commits on288ef6fb69; 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):operatorFacingErrorTextcaller census (Test Core (1/6)red):overlay-index.tskeeps its call. It gains a census row and a sentinel pin inraw-exec-operator-detail-16657.test.ts(read and write legs, and the sentinel reaches neitherresult.detailnor the error report) (a9f79a360c);check:i18ndrift (Type Check · consumer gatesred): the 7platform-objectstranslation bundles regenerated withnode scripts/check-i18n-bundles.mjs --write, committed alone (10f3b686f4);os migrate duplicatespre-flight on a pre-column store (Test Core (3/6)red on10f3b686f4), as the residual bullet below describes (f21b6f9d24);f21b6f9d24: 96 commands, all exit 0 (81 derived, reconciled--ran81 of 81, plus the declared wide families and the i18n and census checks).cliduplicates files, both tiers: 6 files, 37 passed.types:test:repo11 passed,local754 passed.platform-objects: 1083 passed.metadata-protocolsrc/migrations/: 232 passed. Full packages build: exit 0.f21b6f9d24: every job is green except the two timing gates this PR does not cause:Test Core (6/6)'s drift step (ci: PR-timeTest Core (6/6)reds onshard-timing-driftat 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 theTest Coreaggregator's suite-duration ceilings (ci: merge-queueTest Corereds on the new suite-duration ceiling —@objectstack/cloud-connectionmeasured 177.5 s and 200.3 s vs its 163.89 s ceiling, in two groups of PRs that do not touch it (live since1bd612bd6) #22949:cloud-connection,client,service-analytics,service-package,knowledge-ragflow).288ef6fb69, before patch round 1 (this branch merged withorigin/maine357a441c6; the incoming commits overlap none of the files here, and inmetadata-corethey add onlymeta-write-capability.ts):turbo run build --filter=...@objectstack/objectql: exit 0.metadata-corefull suite:17 passed,394 passed.metadata-protocoltypecheck: exit 0.metadata-protocoltargeted (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.objectqltargeted (environment-overlay-email-locale-restart,environment-overlay-five-types,engine-security-catalog-package-door,registry-i18n-bundle-key):4 passed,65 passed.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat288ef6fb69: 79 commands, 79 exit 0. Reconciled with--ran(exit codes recorded):79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN.metadata-protocolfull suite (c7f399ba7f):Test Files 1 failed | 226 passed | 3 skipped (230),Tests 2 failed | 28183 passed. The one red file was thelookupArtifactItemcall-site census, which needed the new site recorded (59841e1570); after that it is4 passed.objectqlfull suite, both projects (59841e1570):Test Files 404 passed (404),Tests 7864 passed.139a2cbc9d:metadatafull suite:58 passed,871 passed.metadata-core,metadata-protocol,objectql(withcheck:test-typecheck) anddogfood: all exit 0.turbo, 63 tasks): exit 0.email-template-two-locales-survive-boot.dogfood.test.ts:3 passed (3).packages/cliis 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 isundefinedat runtime, carrying the marker__ABLATION_22877_MEMBER_DROPPED. The change was made throughscripts/ablation-replace.mjson committed HEAD. The anchor count went 1 → 0, the marker count 0 → 1, and the blob314afe10d8→a28c0832a6.metadata-protocolwas rebuilt (objectql resolves it throughdist/), andablation-dist-preflightconfirmed the marker indist.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.314afe10d8,git diff HEADempty,distrebuilt,ablation-dist-preflight --absentexit 0, and the file goes6 passed (6).Deviations and residuals, stated
(org, type, name), not per member.sys_metadata_historydeclaresunique (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.historyandrestoreVersioncarry the member: history filters to one member, and a revert to another member's version answers404 VERSION_NOT_FOUND.previous_checksum) through the sameoverlayMemberOfBody. No second table and no history migration.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.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 duplicateson a store this version has not booted yet (asys_metadatatable 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 reportedblockedand is never reportedunreadable. Both statements live inoverlay-index.ts. The choice between them is made inruntime-index-preflight.ts, which reports the statement that ran as the entry'sduplicateSql. The report's keys are unchanged. On such a store, anemail_templatepair 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 thepackages/cliduplicates integration red on10f3b686f4.)syncDeclaredIndexesskips 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 withorganization_idNULL are both accepted; CONTROL: a non-NULL organization refuses the second member). It is not measured on a live MySQL.os migrate planlists the declaration change as a destructiverecreate_index, applied withos 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
zh-CN,ja-JPandes-ESbundles carry the English source text for the newbundle_memberkeys, which still need translating.check:i18n-coveragestays green.packages/types/src/driver-error-classification.callers.test.ts(adomain:clifile) gains one row foroverlay-index.ts, the edit class the seat named. The sentinel pin sits inmetadata-protocol.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 readsoverlay-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.tskeeps its own four-part fixture. It tests the drift detector's reading of aCOALESCEkey part and imports nothing changed here, so it stays a valid detector fixture.objectqlharnesses (environment-overlay-five-types.test.ts,engine-security-catalog-package-door.test.ts) hand-declaresys_metadata. They gain thebundle_memberfield the platform object declares, because the engine refuses an undeclared field in awhere.protocol.lookup-artifact-item-call-sites.test.tsrecords the newlookupArtifactItemsite with its disposition.scripts/engine-double-contract.pinned.jsonrecords the new test file's pinned doubles (--write, additions only).Generated by Claude Code