Repository navigation
feat(metadata-protocol): one language of an email template is addressed by name — the by-name /meta doors accept a bundle member selector (#22877, stage 2) - #23070
Conversation
…r selector (#22877, stage 2) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…lector and its closed query sets (#22877, stage 2) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
#22877) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
…#22877) 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>
…artifact — a hydrated overlay of an unshipped member wears the bundle's envelope (#22877) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 142 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 a105ad041a9da332bd38f6f5cafa86da7bba57ad && git checkout a105ad041a9da332bd38f6f5cafa86da7bba57ad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5b43b606807e26d873e2407cbd38a844e624895 2c055c088f2e75046eed9e17b8fa9203a38e3019 && git checkout -B drift-repro c5b43b606807e26d873e2407cbd38a844e624895 && git merge --no-ff 2c055c088f2e75046eed9e17b8fa9203a38e3019
node scripts/docs-audit/affected-docs.mjs --json c5b43b606807e26d873e2407cbd38a844e624895
|
Contract reviewServed-tier: Inputs: card #22877 (body and all 20 comments; ruling 6116165299, seat answers 6118224899, landing record 6121513183, stage-2 claim 6123966682, dev report 6127796890), PR #23070 (body, 14-file list, the net diff Check-runs on the head at 16:07Z: 40 runs; 29 ① Derived judgmentsEach accept-set or public-surface change the diff implies, against the ruling's letter (A, Q2 i):
② Semver levelChangeset
③ Boundary flagsDev deviations (6127796890), each answered:
Out-of-scope findings (dev), each answered or escalated:
Added by this review:
Adopted by Implemented-by: VERDICT: FAIL The one blocking finding is ② — |
… take an optional member (#22877) Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Delta record after the FAIL on Check-runs on the head at 16:46Z: 48 runs, 41 ① Derived judgments
② Semver levelChangeset
③ Boundary flagsDev's flags this round (6128562834), each answered:
The prior record's escalations, against the seat note 6128113137:
Added or re-read by this review:
Adopted by Implemented-by: VERDICT: PASS The FAIL's one blocking finding is closed: |
Fixes #22877
Clause-②: yes
Widens: the accept set of the five by-name
/metadoors (GET, DELETE,/history,/publish,/rollbackon/meta/:type/:name) by one query parameter,member, and the four protocol requests behind them (GetMetaItemRequest,DeleteMetaItemRequest,PublishMetaItemRequest,HistoryMetaItemRequest) by one optional field,member; and three exported surfaces, each by one optionalmember: the last argument ofSchemaRegistry.getItem,getArtifactItem,removeRuntimeShadowandremoveOverlayEntry(@objectstack/objectql), theMetadataMutationEventfield (@objectstack/metadata-protocol, also carried on theClusterMetadataMutationPayloada peer accepts), and the header on eachSysMetadataRepository.listDraftsentry.Stage 2 of 2 of ruling 6116165299 (letter A, Q2 i). Stage 1 (PR #22954,
5677add93f) stores two locales of oneemail_templateas twosys_metadatarows (bundle_member); every by-name door was still canonical-only. This PR lets each by-name door address one member, and carries the four items stage 1 left (seat answers 6118224899, landing record 6121513183).The ruling, quoted:
What changed
1. The selector (
metadata-protocol,rest,spec)member. It is what the stored column holds (sys_metadata.bundle_member), and it is type-neutral. It is notlocale, which on/metaalready names the response translation locale (extractLocale). The seat suggestedmember; nothing measured argued for another name.resolveBundleMemberSelectorinprotocol.ts, beside stage 1's member plumbing. Each of the five protocol doors calls it once, right after its type fold:getMetaItem,deleteMetaItem,publishMetaItem,historyMetaItem,rollbackMetaItem.en-US) is the same request. The value is trimmed.VALIDATION_ERROR(param: 'member', the message names the selector):email_template);maxLengthofbundle_member(pinned equal toSysMetadataObject).item: undefined, REST 404);reset: false;404 NO_DRAFT;{ events: [] };404 VERSION_NOT_FOUND.TransportScopedMetaRequest). SoGetMetaItemRequestSchema,DeleteMetaItemRequestSchema,PublishMetaItemRequestSchemaandHistoryMetaItemRequestSchemagain one optionalmemberwith one shared describe (cross-lanedomain:spec).rollbackMetaItemhas no declared request (the door calls it through a cast), so none was added.authorable-surface/api.jsonandcontent/docs/references/api/protocol.mdxare regenerated.2. The five routes' closed query sets (
rest, AGENTS.md route rule 5)None of the five routes declared a closed set before. Each set below is measured from the handler's own read points and from the helpers it calls, and each route now opens with
refuseUnknownQueryParams, ahead of its arity gate:GET /meta/:type/:namelayers,state,preview,package,locale,memberwantsMetaItemLayers; the two admitted draft switches;metaItemPackageBindingandserveMetaItemLayered;localea frame down (extractLocalebehind the cached validator and every translation)DELETE /meta/:type/:namestate,dropStorage,memberGET …/historysinceSeq,limit,memberPOST …/publishpackage,membermetaItemPackageBindingPOST …/rollbacktoVersion,membermemberjoins each route'srefuseRepeatedQueryParamslist too.metaMemberSelector, is shared by the five handlers. It passes a non-string value through, so the protocol refuses it; it is never dropped.getMetaItemCachedtakes no member.?layers=together with?member=is refused. The layered view reads no member, and answering with the canonical member's layers would be the silent wrong answer.@objectstack/clientandobjectuiat the pinned9646e2ed2b(DraftChangesPanel,ResourceEditPage,client.meta.*).3. The registry's bare-name read (
objectqlSchemaRegistry.getItem)zh-CN-only overlay answered a read that named no member. This was the half-false premise recorded in 6118224899.getItemandgetArtifactItemtake an optionalmember(a discriminator value), which narrows every tier to that member.4.
removeRuntimeShadowon a by-name reset (stage 1 finding 1)removeRuntimeShadow(type, name, member?)andremoveOverlayEntry(type, name, member?)address one member when one is named. With no member, they keep the group sweep, the shapeunregisterItemtakes for a withdrawal by name.restoreArtifactRegistryViewnames the member on every tier. A MetadataService baseline of another member is not this member's baseline: that service keys a type's items by name alone._packageId,_provenancegrafted bymergeArtifactProtection). SoremoveOverlayEntryrefuses it as code.zh-CNreset therefore kept sending the reset wording until the next boot.5. Package-batch doors and the cluster fan-out (stage 1 finding 2)
SysMetadataRepository.listDraftsheaders carry the storedmemberwhen it is non-canonical. The protocol's publiclistDraftsserves its declared header without it.publishPackageDrafts:member;discardPackageDrafts(andrevertStoredPackage, throughdiscardDraftInItsScope) discards each draft as its member.revertCommitreads, deletes, restores, re-binds and heals each plan item as its member. The revert's own plan carries the member too.listCommitsserves the declared four keys.MetadataMutationEventgains an optionalmember(present only for a non-canonical member);applyRemoteMetadataMutationre-reads, re-binds and heals that member's row.Pins (real code)
packages/objectql/src/environment-overlay-email-locale-restart.test.ts, stage 1's harness (realObjectQLand protocol; a restart is a new engine over the same stores). It is now a versioned package that also ships anen-US-only template:en-USboth answer the canonical member; trimmed.zh-CNreset → its row alone goes, and theen-USoverlay stays the registry's entry, at once and after a restart.zh-CNoverlay stays registered.zh-CNdraft goes live. CONTROL: the bare publish answersNO_DRAFT.404 VERSION_NOT_FOUND.zh-CN.viewtype, blank, 101 characters), and the store bytes are unchanged.publishPackageDrafts,revertCommitanddiscardPackageDraftswith a two-member bundle wherezh-CNalone has a draft. The commit timeline and the draft list keep their declared keys.packages/objectql/src/registry-i18n-bundle-key.test.ts:zh-CN-only overlay;packages/metadata-protocol/src/protocol.bundle-member-selector.test.ts: the one reading — every refusal, the trimmed pass, and the length bound pinned equal to the declared column.packages/rest/src/rest-server-meta-member-selector.test.ts, 30 cases. Per route:error.codeand the exact located message, and the protocol is never called; a repeated selector is refused;localetoo; GET bypasses the cache with a member and keeps it without one;memberkey;packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts, stage 1's showcase case extended, on a real SQLite file and realsendTemplate:?member=zh-CNreads thezh-CNwording;DELETE ?member=zh-CNresetszh-CNalone;en-USkeeps the admin's wording andzh-CNfalls to theen-USfloor (the showcase shipsen-USonly);?member=zh-CNis then 404;This case also measures that the showcase's
/metaitem read is served byRestServer's route, which reads the selector.Evidence
Every heavy run went through
scripts/pm/os-verify-lock.sh, and each read itsVERDICT command-exit 0line. Exit codes were captured before any pipe. The final head is2b882e6774, which mergesorigin/maindf7a137923; the incoming commits touch none of this PR's files.Gates.
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat2b882e6774: 118 commands, 118 exit 0.--ran(exit codes recorded):118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.0ede0f8ab3is a subset of it.12b4b5047fhadcheck:skill-examplesandcheck:dual-build-cjs-loadsat exit 3 (PREREQUISITE NOT MET). After a full packages build (turbo, 72 tasks, exit 0) both pass, and the final round has them green.Suites.
objectql, full (vitest run):Test Files 407 passed (407),Tests 7955 passed. This ran before the last registry commit (2b882e6774).objectqltest file that namesremoveRuntimeShadow,removeOverlayEntryoremail_template: 13 files,246 passed;40 passed.metadata-protocol, full, at12b4b5047f:Test Files 228 passed | 3 skipped (231),Tests 28196 passed | 19 skipped.rest, full, at00886e17d7(the merge touched norestfile):Test Files 279 passed (279),Tests 5960 passed | 327 skipped. After the merge, 6 targeted files (the new pins, the closed-set and multiplicity files, absent-404, history/diff door, publish package scope):141 passed.spec:protocol.test.tsandtype-alias-convention.pin.test.ts,187 passed.spec,metadata-protocol,objectql(withcheck:test-typecheck),rest(withcheck:test-typecheck) anddogfood.dogfood:email-template-two-locales-survive-boot.dogfood.test.ts,4 passed (4), afterobjectqlwas rebuilt at2b882e6774.pnpm --filter @objectstack/spec check:generatednamedcheck:docsstale;gen:docsregenerated one page.check:authorable-surfacewrote the 4 new request keys intoauthorable-surface/api.json.ESLint, a proven narrowing.
pnpm exec eslint --no-inline-config --format jsonover the 11 changed.tsfiles: 11 files linted, 0 errors, 0 warnings.eslint.config.mjs'sfiles: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']minusNEVER_LINTED. No file was reported ignored.parserOptions.project, as its own header records), so this diff cannot move the verdict on any file it does not touch.pnpm lintis CI's.Ablations
Each ablation went through
scripts/ablation-replace.mjs(wrap mode, on committed HEAD; anchor count 1 then 0, blob changed, restored blob equal to HEAD,git diff HEADempty). Ametadata-protocolmutation was rebuilt, becauseobjectqlresolves it throughdist, aKNOWN_UNALIASED_TEST_IMPORTSpair.ablation-dist-preflightwas run present on the mutation leg and--absenton the restore leg.rollbackMetaItemwithout the member (therestoreVersionmember option replaced by an expression that isundefinedat runtime, with the marker__ABLATION_22877_ROLLBACK_MEMBER_DROPPED). The restart harness went1 failed | 17 passed. Only the member-revert pin was red (No history row at version 2 … (its canonical member)); the bare-name revert CONTROL stayed green. Restored:18 passed.getItem's bare-name branch back to the first tier's first member, with the marker__ABLATION_22877_REGISTRY_TIER_ORDER;objectqlsource, so no build). The registry bundle file and the harness went1 failed | 38 passed. Onlya bare-name read answers the canonical member when only a second locale is overlaidwas red. The harness's bare-name pins stay green under this ablation, becausegetMetaItemanswers the canonical artifact before it asks the registry, which is a second layer by design. Restored:39 passed.removeRuntimeShadow(singular, name, memberKey)withmemberKeyreplaced). The harness went3 failed | 15 passed: DELETE by member, DELETE with no member (the reverse), and the cluster member reset were red, while stage 1's store-level reset pin stayed green. Restored:18 passed.success: false,outcome: refused,failed: [{ code: NO_DRAFT, error: No pending draft exists for email_template/acme.welcome — nothing to publish. }]. That is the measured shape behind the changeset's sentence; both the publish and the revert pins were red.zh-CNdraft row stayed.These four ablations ran at
fdf01eb9e9, the head before2b882e6774(the versioned harness package, the unshipped-member pin and the bundle-level shadow test), when the restart harness had 18 cases. The code each one removed is unchanged since. At2b882e6774the harness has 19 cases, all green.Deviations, stated
handleRouteError. It comes out flat:body.codeisVALIDATION_ERROR, andbody.erroris the sentence.body.error.code.check:route-envelope, the repo-wide ratchet). The GET handler's own note says why converting part of it here would mint a new divergence.VALIDATION_ERROR.?layers=plus?member=refusal is raised by the GET handler itself, not by the protocol. It is thrown into the samehandleRouteError, so it is flat too; that is the handler's only pre-existing shape for a 400 of its own, so it mints no new dialect.GET /meta/_draftsandGET /packages/:id/commitsdo not servemember. The ruling widens the doors' accept set, not these responses.typeandnamethere.removeRuntimeShadowsemantics, named member. The artifact test stays at the level of the name: a packaged artifact of the bundle remains, as the method's contract always said, so it does not have to be an artifact of that member. Section 4 says why.protocol.lookup-artifact-item-call-sites.test.ts, whose census records the re-scoped artifact-lookup site and the definition's new parameter;authorable-surface/api.jsonand the protocol reference page.Acceptance notes
/metaitem read (packages/runtime/src/domains/meta.ts) does not read?member=. A host served by the dispatcher alone would answerGET …?member=zh-CNwith the canonical member, because that transport has no closed query set at all. The showcase is served byRestServer(measured by the dogfood case above). This is outside the claim's surface (packages/runtime), and is not measured on a dispatcher-only host./diff,/layers,/audit,/publishedand/references.MetadataManager) keys a type's items by name alone, so for a bundle it holds one member.getMetaItemnow takes its answer only when it is the addressed member; the service itself is unchanged.environmentId, never on org #6602, by design). The cluster pin therefore boots control-plane replicas.bundle_memberkeys stay owed toarea:i18n, as the claim says.Generated by Claude Code