Skip to content

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

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-22877-member-selector
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-22877-member-selector

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22877
Clause-②: yes
Widens: the accept set of the five by-name /meta doors (GET, DELETE, /history, /publish, /rollback on /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 optional member: the last argument of SchemaRegistry.getItem, getArtifactItem, removeRuntimeShadow and removeOverlayEntry (@objectstack/objectql), the MetadataMutationEvent field (@objectstack/metadata-protocol, also carried on the ClusterMetadataMutationPayload a peer accepts), and the header on each SysMetadataRepository.listDrafts entry.

Stage 2 of 2 of ruling 6116165299 (letter A, Q2 i). Stage 1 (PR #22954, 5677add93f) stores two locales of one email_template as two sys_metadata rows (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:

Q2 → i: PUT stays body-addressed. GET, DELETE, publish, history and revert by name accept one member selector in their closed query-parameter set (AGENTS.md route rule 5), named after the discriminator so it cannot be read as the UI translation locale (?locale= on /meta already means that); absent, it means the canonical member, which is what the registry's bare-name read answers; a wrong value is a located 400. This widens the doors' accept set: Clause-②: yes, a contract-tier review before landing.

What changed

1. The selector (metadata-protocol, rest, spec)

  • The name is member. It is what the stored column holds (sys_metadata.bundle_member), and it is type-neutral. It is not locale, which on /meta already names the response translation locale (extractLocale). The seat suggested member; nothing measured argued for another name.
  • One reading, shared by every door: resolveBundleMemberSelector in protocol.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.
    • Absent means the canonical member. Naming the canonical value (en-US) is the same request. The value is trimmed.
    • Refused with a located 400 VALIDATION_ERROR (param: 'member', the message names the selector):
      • a selector on a type with no discriminator (every type except email_template);
      • a value that is not a string;
      • a blank value (never read as absent);
      • a value longer than 100 characters, the declared maxLength of bundle_member (pinned equal to SysMetadataObject).
    • A well-formed member with nothing stored or shipped, measured per door:
      • GET answers it as absent (protocol item: undefined, REST 404);
      • DELETE answers reset: false;
      • publish answers 404 NO_DRAFT;
      • history answers { events: [] };
      • revert answers 404 VERSION_NOT_FOUND.
  • The requests carry it. The REST door builds each typed request literal against the spec request shapes (TransportScopedMetaRequest). So GetMetaItemRequestSchema, DeleteMetaItemRequestSchema, PublishMetaItemRequestSchema and HistoryMetaItemRequestSchema gain one optional member with one shared describe (cross-lane domain:spec). rollbackMetaItem has no declared request (the door calls it through a cast), so none was added. authorable-surface/api.json and content/docs/references/api/protocol.mdx are 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:

route closed set read points
GET /meta/:type/:name layers, state, preview, package, locale, member wantsMetaItemLayers; the two admitted draft switches; metaItemPackageBinding and serveMetaItemLayered; locale a frame down (extractLocale behind the cached validator and every translation)
DELETE /meta/:type/:name state, dropStorage, member the draft discard and the opt-in teardown
GET …/history sinceSeq, limit, member the declared paging pair
POST …/publish package, member metaItemPackageBinding
POST …/rollback toVersion, member the query fallback after the body
  • member joins each route's refuseRepeatedQueryParams list too.
  • One reading of the parameter, metaMemberSelector, is shared by the five handlers. It passes a non-string value through, so the protocol refuses it; it is never dropped.
  • A GET naming a member bypasses the cached arm, because getMetaItemCached takes 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.
  • In-repo callers were measured, and none sends a parameter outside these sets: @objectstack/client and objectui at the pinned 9646e2ed2b (DraftChangesPanel, ResourceEditPage, client.meta.*).

3. The registry's bare-name read (objectql SchemaRegistry.getItem)

  • A bare-name read of a discriminated type now answers the canonical member, wherever it sits in the tier order (overlay, then prefer-local, then the first composite).
  • Before, the first tier that held any member won, and canonical-first applied only inside that tier. So a zh-CN-only overlay answered a read that named no member. This was the half-false premise recorded in 6118224899.
  • A bundle with no canonical member still answers its first member, as before.
  • getItem and getArtifactItem take an optional member (a discriminator value), which narrows every tier to that member.

4. removeRuntimeShadow on a by-name reset (stage 1 finding 1)

  • removeRuntimeShadow(type, name, member?) and removeOverlayEntry(type, name, member?) address one member when one is named. With no member, they keep the group sweep, the shape unregisterItem takes for a withdrawal by name.
  • restoreArtifactRegistryView names 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.
  • Found by the dogfood, fixed here: a member's shadow is lifted when the bundle ships any artifact, not only an artifact of that member.
    • A hydrated overlay of a member the package does not ship wears the bundle's artifact envelope (_packageId, _provenance grafted by mergeArtifactProtection). So removeOverlayEntry refuses it as code.
    • On the showcase, a zh-CN reset therefore kept sending the reset wording until the next boot.
    • It is pinned in the objectql harness against a versioned package, so artifacts carry the envelope.

5. Package-batch doors and the cluster fan-out (stage 1 finding 2)

  • SysMetadataRepository.listDrafts headers carry the stored member when it is non-canonical. The protocol's public listDrafts serves its declared header without it.
  • publishPackageDrafts:
    • each draft is promoted as its member;
    • the pre-publish capture reads that member's active row;
    • the revert plan item records member;
    • a failed batch names the causal draft by member.
  • discardPackageDrafts (and revertStoredPackage, through discardDraftInItsScope) discards each draft as its member.
  • revertCommit reads, deletes, restores, re-binds and heals each plan item as its member. The revert's own plan carries the member too. listCommits serves the declared four keys.
  • The cluster fan-out:
    • MetadataMutationEvent gains an optional member (present only for a non-canonical member);
    • save, publish, delete, rollback and revert stamp it;
    • applyRemoteMetadataMutation re-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 (real ObjectQL and protocol; a restart is a new engine over the same stores). It is now a versioned package that also ships an en-US-only template:

  • GET: each member by name; absent and en-US both answer the canonical member; trimmed.
  • GET: a member nobody stored answers the code layer's member; one that no layer holds answers nothing.
  • DELETE by member: zh-CN reset → its row alone goes, and the en-US overlay stays the registry's entry, at once and after a restart.
  • DELETE of a member no artifact ships: its overlay leaves the registry and the layered list, and the canonical overlay stays.
  • DELETE with no member (the reverse): the canonical member is reset, and the zh-CN overlay stays registered.
  • publish by member: a zh-CN draft goes live. CONTROL: the bare publish answers NO_DRAFT.
  • history and revert by member:
    • one shared counter, three distinct versions;
    • each member sees its own events;
    • a revert to another member's version answers 404 VERSION_NOT_FOUND.
  • CONTROL: a bare-name revert reverts the canonical member to its own version and leaves zh-CN.
  • A wrong selector is a located 400 on all five doors (a view type, blank, 101 characters), and the store bytes are unchanged.
  • publishPackageDrafts, revertCommit and discardPackageDrafts with a two-member bundle where zh-CN alone has a draft. The commit timeline and the draft list keep their declared keys.
  • The cluster fan-out (two control-plane replicas over one store, a delivering bus): the peer converges on the member that changed, and a member reset heals only that member.
  • The six stage 1 pins, unchanged.

packages/objectql/src/registry-i18n-bundle-key.test.ts:

  • the bare-name read answers the canonical member with a zh-CN-only overlay;
  • named-member reads, artifact included;
  • the member-addressed heal, including the shipped-bundle and unshipped-bundle cases; naming no member keeps the sweep.

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:

  • refusal: a misspelled selector is answered with status, nested error.code and the exact located message, and the protocol is never called; a repeated selector is refused;
  • preservation: the exact request the protocol received, the member and every measured name included, locale too; GET bypasses the cache with a member and keeps it without one;
  • the layered refusal;
  • a CONTROL that no selector sends no member key;
  • the value refusals from a real protocol over an engine that is never reached.

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 real sendTemplate:

  • ?member=zh-CN reads the zh-CN wording;
  • DELETE ?member=zh-CN resets zh-CN alone;
  • after a cold boot, en-US keeps the admin's wording and zh-CN falls to the en-US floor (the showcase ships en-US only);
  • ?member=zh-CN is then 404;
  • a misspelled selector, and a selector on a type with no members, are located 400s.

This case also measures that the showcase's /meta item read is served by RestServer's route, which reads the selector.

Evidence

Every heavy run went through scripts/pm/os-verify-lock.sh, and each read its VERDICT command-exit 0 line. Exit codes were captured before any pipe. The final head is 2b882e6774, which merges origin/main df7a137923; the incoming commits touch none of this PR's files.

Gates.

  • The derived union is node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2b882e6774: 118 commands, 118 exit 0.
  • Reconciled with --ran (exit codes recorded): 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN.
  • The lead list of 86 commands at 0ede0f8ab3 is a subset of it.
  • An earlier round at 12b4b5047f had check:skill-examples and check:dual-build-cjs-loads at 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).
  • After that commit:
    • every objectql test file that names removeRuntimeShadow, removeOverlayEntry or email_template: 13 files, 246 passed;
    • the restart harness and the registry bundle file: 2 files, 40 passed.
  • metadata-protocol, full, at 12b4b5047f: Test Files 228 passed | 3 skipped (231), Tests 28196 passed | 19 skipped.
  • rest, full, at 00886e17d7 (the merge touched no rest file): 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.ts and type-alias-convention.pin.test.ts, 187 passed.
  • Typecheck exit 0 for spec, metadata-protocol, objectql (with check:test-typecheck), rest (with check:test-typecheck) and dogfood.
  • dogfood: email-template-two-locales-survive-boot.dogfood.test.ts, 4 passed (4), after objectql was rebuilt at 2b882e6774.
  • pnpm --filter @objectstack/spec check:generated named check:docs stale; gen:docs regenerated one page. check:authorable-surface wrote the 4 new request keys into authorable-surface/api.json.

ESLint, a proven narrowing.

  • pnpm exec eslint --no-inline-config --format json over the 11 changed .ts files: 11 files linted, 0 errors, 0 warnings.
  • The population is eslint.config.mjs's files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED. No file was reported ignored.
  • That config never enables type-aware linting (no parserOptions.project, as its own header records), so this diff cannot move the verdict on any file it does not touch.
  • The repo-wide pnpm lint is 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 HEAD empty). A metadata-protocol mutation was rebuilt, because objectql resolves it through dist, a KNOWN_UNALIASED_TEST_IMPORTS pair. ablation-dist-preflight was run present on the mutation leg and --absent on the restore leg.

  1. rollbackMetaItem without the member (the restoreVersion member option replaced by an expression that is undefined at runtime, with the marker __ABLATION_22877_ROLLBACK_MEMBER_DROPPED). The restart harness went 1 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.
  2. The registry tier-order change (getItem's bare-name branch back to the first tier's first member, with the marker __ABLATION_22877_REGISTRY_TIER_ORDER; objectql source, so no build). The registry bundle file and the harness went 1 failed | 38 passed. Only a bare-name read answers the canonical member when only a second locale is overlaid was red. The harness's bare-name pins stay green under this ablation, because getMetaItem answers the canonical artifact before it asks the registry, which is a second layer by design. Restored: 39 passed.
  3. The registry heal without the member (removeRuntimeShadow(singular, name, memberKey) with memberKey replaced). The harness went 3 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.
  4. The package-batch doors without the member.
    • With the publish promotion's member dropped, the batch answered 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.
    • With the discard's member dropped, the discard answered success while the zh-CN draft row stayed.
    • Both were restored, and the dist was proven absent.

These four ablations ran at fdf01eb9e9, the head before 2b882e6774 (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. At 2b882e6774 the harness has 19 cases, all green.

Deviations, stated

  • The value refusal's envelope.
    • The selector's value is judged by the protocol, so the REST door renders that refusal through its one error door, handleRouteError. It comes out flat: body.code is VALIDATION_ERROR, and body.error is the sentence.
    • The route's own recognition refusal of a misspelled name is nested: body.error.code.
    • This is the existing split for every refusal the protocol raises on these routes (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.
    • The code is the same in both: VALIDATION_ERROR.
    • The ?layers= plus ?member= refusal is raised by the GET handler itself, not by the protocol. It is thrown into the same handleRouteError, 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.
  • Response contracts are unchanged.
    • GET /meta/_drafts and GET /packages/:id/commits do not serve member. The ruling widens the doors' accept set, not these responses.
    • Two drafts or plan items of one name in two languages therefore read as two entries with the same type and name there.
  • removeRuntimeShadow semantics, 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.
  • File surface. Every edit is inside the claim's listed files, plus:
    • the four test files and the dogfood case the claim names;
    • protocol.lookup-artifact-item-call-sites.test.ts, whose census records the re-scoped artifact-lookup site and the definition's new parameter;
    • two regenerated artifacts: authorable-surface/api.json and the protocol reference page.

Acceptance notes

  • The runtime dispatcher's /meta item read (packages/runtime/src/domains/meta.ts) does not read ?member=. A host served by the dispatcher alone would answer GET …?member=zh-CN with the canonical member, because that transport has no closed query set at all. The showcase is served by RestServer (measured by the dogfood case above). This is outside the claim's surface (packages/runtime), and is not measured on a dispatcher-only host.
  • By-name doors not in the ruling's list stay canonical-only: /diff, /layers, /audit, /published and /references.
  • The MetadataService (MetadataManager) keys a type's items by name alone, so for a bundle it holds one member. getMetaItem now takes its answer only when it is the addressed member; the service itself is unchanged.
  • The environment kernel's write-through. On an environment kernel, a save and a peer's convergence do not write a non-object overlay through to the registry (An org-scoped overlay row reaches the process-wide SchemaRegistry on unscoped kernels — both the write-through and the read hydration gate only on environmentId, never on org #6602, by design). The cluster pin therefore boots control-plane replicas.
  • The untranslated bundle_member keys stay owed to area:i18n, as the claim says.

Generated by Claude Code

…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>
…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>
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/rest, @objectstack/spec, touching 76 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

32 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 c5b43b606807e26d873e2407cbd38a844e624895.

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

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/authorable-surface/api.json) — pages documenting those are invisible to this run
  • 1 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 — 142 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 c5b43b606807e26d873e2407cbd38a844e624895 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a105ad041a9da332bd38f6f5cafa86da7bba57ad — the merge of head 2c055c088f2e75046eed9e17b8fa9203a38e3019 into base c5b43b606807e26d873e2407cbd38a844e624895, 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 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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2b882e6774dda4bd54b14104da070b900fb6e5b2
Local-runs: none

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 df7a137923..2b882e6774, fetched into refs/review/pr-23070), and the head's check-runs (REST, polled at 15:57Z, 16:06Z and 16:07Z). Nothing was built, run or re-run.

Check-runs on the head at 16:07Z: 40 runs; 29 success; 5 skipped (Dogfood Verify CLI, Console Pin Gate, Temporal Conformance, Dogfood Regression Gate matrix shard, Packed-tarball smoke); 6 still in_progress — Test Core 1/6 through 6/6. Every lint, type-check, changeset, docs, size, claim and single-writer gate is green; the suite verdict is not yet in. The verdict below does not rest on the running shards; a red shard re-judges this head.

① Derived judgments

Each accept-set or public-surface change the diff implies, against the ruling's letter (A, Q2 i):

  1. The selector's name, member — right. The discriminator's FIELD is locale (ITEM_KEY_DISCRIMINATORS.email_template = { field: 'locale', canonical: 'en-US' }), so a literal "named after the discriminator" would yield the one name the ruling forbids; the operative clause is "so it cannot be read as the UI translation locale", and the discriminator's stored name since stage 1 is sys_metadata.bundle_member. member is that name, type-neutral, and collides with no other query parameter on the REST surface ('member' occurs once in rest-server.ts, as META_ITEM_MEMBER_PARAM). Pinned at three points (BUNDLE_MEMBER_SELECTOR, META_ITEM_MEMBER_PARAM, the rest test's first case).

  2. The five closed query sets — right, re-measured from the head's handlers and the helpers they call. GET /meta/:type/:name (handler from line 6369): the recognition gate is the handler's FIRST req.query read (6381); then layers via wantsMetaItemLayers (6480, reads query.layers only), state (6523), preview (6531), package (6542, 6845, and serveMetaItemLayered 4077), locale a frame down (extractLocale 6656 and metaItemAnswerSources, both through metaRequestLocale, which reads query.locale and nothing else). The whole-request helpers (resolveExecCtx, resolveObjectMasker, anonymousFormIntakeWarnings) read no query key, so no working read is turned into a loud 400. DELETE: state 7343, dropStorage 7352. /history: sinceSeq 7467 and limit 7476 through readDeclaredQueryNumber. /publish: package 7826. /rollback: toVersion 7968 (after the body's toVersion/version). Each handler opens with refuseUnknownQueryParams over an exported readonly string[] and adds member to its refuseRepeatedQueryParams list. Both pin halves exist per route in rest-server-meta-member-selector.test.ts: refusal (status, NESTED error.code, the exact unknownQueryParamMessage, protocol never called; plus the repeated-selector refusal) and preservation (the exact request object each protocol method received, locale and package included; GET's cache bypass with a member and its CONTROL without). In-repo callers (client, objectui at the pinned sha) send nothing outside the sets, as the dev measured; I did not re-measure objectui.

  3. The located 400 cases — right. One reading, resolveBundleMemberSelector, refuses with code VALIDATION_ERROR, status 400, param 'member' and a sentence naming the selector: (a) a selector on a type with no discriminator, after the type fold (either spelling); (b) a non-string value (an adapter's nested parse, passed through by metaMemberSelector, never dropped); (c) a blank value, never read as absent; (d) a value over 100 characters, pinned equal to SysMetadataObject.fields.bundle_member.maxLength. The spec's EmailTemplateDefinition.locale is z.string() with no grammar beyond its description (BCP-47 by prose only), so there is no further "wrong value" the ruling's letter could ask for: a non-blank value of at most 100 characters can be a stored member, and one nothing holds is correctly the door's absent answer rather than a 400. Envelope: the route's recognition refusal is nested (error.code), the protocol-raised value refusal is flat (body.code) through handleRouteError — the pre-existing split check:route-envelope ratchets; same code on both. One note: the ?layers= plus ?member= refusal at 6489 is raised by the REST handler itself, not the protocol, and is also flat; it follows the handler's only pre-existing own-400 pattern, so it mints no new dialect, but the PR's deviation note ("the value refusal's envelope") does not describe it.

  4. The four spec request fields — right. GetMetaItemRequestSchema, DeleteMetaItemRequestSchema, PublishMetaItemRequestSchema, HistoryMetaItemRequestSchema each gain member: z.string().optional() with ONE shared describe (BUNDLE_MEMBER_SELECTOR_DESCRIPTION), whose sentences match the implementation (absent means canonical, naming the canonical value is the same request, the four refusals, the absent-item answer). No RollbackMetaItemRequest exists in packages/spec (verified by search), and the REST door calls rollbackMetaItem through a cast (7871), so none was added — right. authorable-surface/api.json gains exactly the 4 keys; protocol.mdx gains the 4 rows. The spec gates are in the dev's 118 and CI's Lint jobs are green.

  5. The registry's bare-name canonical read — right. SchemaRegistry.getItem on a discriminated type now answers entriesOfMember(tiers, canonical)[0] ?? tiers[0] over the unchanged tier order, and a named member narrows every tier; getArtifactItem narrows the same way. Pinned: a zh-CN-only overlay no longer answers a bare read (the half-false premise in 6118224899 is closed). One edge, stated for the record: a bundle that ships NO canonical member keeps the legacy first-member answer on a bare read, while ?member=en-US answers nothing — so "naming the canonical value is the same request" (the describe and the changeset) is not exact for that bundle; the ruling's letter ("absent means the canonical member") favours answering nothing. Deliberate per the code comment, unpinned, not blocking.

  6. removeRuntimeShadow keeping its artifact test at name level — right, judged. A member-level artifact test cannot work for a member the package does not ship: mergeArtifactProtection grafts the bundle's _packageId/_provenance onto that member's hydrated overlay, so removeOverlayEntry refuses it as code, and the shadow tier is the only one that can retire it (the dogfood found exactly this residue). With a member named, only that member's shadow goes; with none, the group sweep stays for unregisterItem. Pinned in registry-i18n-bundle-key.test.ts for a shipped member, an unshipped member, a no-package bundle and the no-member sweep, and in the harness against a versioned package. The root cause — an unshipped member's overlay wearing the artifact envelope, so isCodeArtifactBody misclassifies it — is left in place; see ③.

  7. The package-batch doors and the cluster event — right; and yes, MetadataMutationEvent.member is a public-surface addition. SysMetadataRepository.listDrafts headers carry a non-canonical member; the protocol's listDrafts and listCommits strip it, so both declared responses are unchanged. publishPackageDrafts promotes, captures, plans (CommitPlanItem.member, commitPlanKey) and attributes a failure per member; discardPackageDrafts and revertStoredPackage discard per member; revertCommit reads, deletes, restores, re-binds, heals and re-plans per member. MetadataMutationEvent gains an optional member, stamped on save, publish, delete, rollback and revert, read by applyRemoteMetadataMutation. That interface is exported from the package index (line 127) and is the event of ClusterMetadataMutationPayload, which a peer ACCEPTS from the wire — an additive widening of @objectstack/metadata-protocol's published surface, as is the member? header on the exported SysMetadataRepository.listDrafts. The package is graded minor, so the level covers both; the PR's Widens: sentence names neither (see ②). Mixed-version note: a replica on the previous release ignores member and re-reads the canonical row, which is the pre-PR behaviour until that replica's next boot — transient and self-healing, not a correctness hole on either side.

  8. A well-formed member nothing holds, per door — right. GET: item: undefined, REST 404 (pinned in the harness with fr-FR and in the dogfood after the reset and reboot). Publish: 404 NO_DRAFT (pinned as the bare CONTROL, the same path). Revert: 404 VERSION_NOT_FOUND (pinned for another member's version). DELETE reset: false and history { events: [] } are stated and measured by the dev but not pinned in the harness; both follow the existing no-row paths (the probe-first delete, the empty lineage) and nothing in the diff changes those answers.

  9. The runtime dispatcher (packages/runtime/src/domains/meta.ts) — a silent-drop hole, a follow-up, not this PR's to close. The dispatcher's item read mounts GET, HEAD and PUT only (METADATA_ITEM_METHODS; the other verbs are 405), reads state, preview, layers, package and locale, has no closed set, and never passes member: a dispatcher-only host answers GET …?member=zh-CN with the canonical member, which is the plausible wrong answer the closed-set rule exists to prevent, now that the name has a defined meaning on the sibling transport. Not this PR's: the ruling cites AGENTS.md route rule 5, a packages/rest ingress rule converted per lane; the claim's surface excludes packages/runtime; the dispatcher had no closed set before this PR and the hole is read-only (no reset, publish, history or revert is mounted there). It must become a card, not an acceptance note — ③.

② Semver level

Changeset .changeset/22877-member-selector.md: @objectstack/metadata-protocol minor, @objectstack/rest minor, @objectstack/spec minor, @objectstack/objectql patch. Clause-②: yes on PR body line 2 and in the changeset body, no arm (allowed: at most one). check:changeset-no-major and Check Changeset are green; that gate asks only that ONE touched package carry minor, so it cannot see the finding below.

  • metadata-protocol, rest, spec at minor — right: each publishes an additive widening (the four request fields; the five routes' accepted name; the protocol doors' member, the event field, the repository header).
  • objectql at patch — wrong. SchemaRegistry is exported on the package index, and four of its public methods — getItem, getArtifactItem, removeRuntimeShadow, removeOverlayEntry — each gain an optional member argument. That is a purely additive widening of a published package's public surface (a new accepted value), which the maintainer's WHICH LEVEL rule (pr-automation.yml lines 754–767, ruled 2026-09-04 decision batch [WIP] Add query enhancements and advanced validation features #35 on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294, restated in check-changeset-no-major.mjs's header) grades at least minor: "the commit type may raise a bump but never lower it below what the act requires; a fix( that widens is therefore minor". The changeset body itself describes the new capability under this package. Recorded for the seat: stage 1 graded SysMetadataRepository's new member options patch under Clause-②: no and was accepted (6120204973); that reading is not binding here, and the rule's letter is. Remedy: one line.
  • The Widens: sentence names the five doors' query parameter and the four request fields — right as far as it goes. It should also name MetadataMutationEvent.member (exported type; the cluster wire payload), the member header on SysMetadataRepository.listDrafts, and the member argument on the four SchemaRegistry methods. A PR-body edit; Check Changeset re-reads the body on edited.

③ Boundary flags

Dev deviations (6127796890), each answered:

  • File surface (+protocol.lookup-artifact-item-call-sites.test.ts, +two regenerated artifacts): accepted — the census must record the re-scoped site and the definition's new parameter, and AGENTS.md requires regenerated spec artifacts in the same push.
  • removeRuntimeShadow at name level: accepted, judged in ① 6.
  • Response contracts unchanged (listDrafts, listCommits strip member): consistent with the ruling, which widens the doors' accept set, not these responses; the Studio consequence is a follow-up (below).
  • No RollbackMetaItemRequestSchema: verified, accepted; the rollback door's member is declared by its REST closed set and the implementation signature only.
  • Suite heads (objectql full before the last commit; rest full pre-merge): the Test Core shards on this head are the record of that; they are still running.
  • Attribution trailer: not a contract matter.
  • PR body line 3 (Widens:): see ②.

Out-of-scope findings (dev), each answered or escalated:

  • Runtime dispatcher does not read ?member=: judged in ① 9. Escalated — the seat files a card for the runtime lane (transport parity for the by-name member selector on the dispatcher's item GET, with a closed set or at least member read and forwarded); the dev's "noted, not filed" is not an end state for a silent drop on a public transport.
  • GET /meta/_drafts and GET /packages/:id/commits do not serve member; Studio's DraftChangesPanel would open the canonical body for a second language's draft: pre-existing shape, but now a user-visible ambiguity that stage 1 made reachable and this PR makes addressable everywhere but there. Escalated — a card (the two list responses, DraftChangesPanel/ResourceEditPage, and the client SDK's meta.* methods gaining the selector; the claim excluded the SDK deliberately, so it is owed).
  • /diff, /layers, /audit, /published, /references stay canonical-only; ?layers= with ?member= is refused: right under the ruling's list. Added by this review: PUT /meta/:type/:name declares no closed set (its reads are mode, force, package through metaSaveRequestOptions and lines 7161/7170), so PUT …?member=zh-CN with an en-US body is silently dropped and writes en-US; the ruling keeps PUT body-addressed, so the fix is PUT's own closed set refusing member loudly — the same per-lane conversion. Escalated — one card for the remaining by-name doors' closed sets, PUT included.
  • MetadataService keyed by name alone: pre-existing; getMetaItem and the heal now accept its answer only for the addressed member — right.
  • The flat/nested envelope split: pre-existing ratchet; see ① 3 for the handler-raised layered refusal the note omits.

Added by this review:

  • The envelope graft: an unshipped member's hydrated overlay wears _packageId/_provenance via mergeArtifactProtection, so isCodeArtifactBody reads it as code. This PR routes around it in removeRuntimeShadow; every other isCodeArtifactBody consumer that meets a bundle member inherits the misclassification. Escalated — a card to measure it (removeOverlayEntry alone, the disabled-package gate, the layered view's provenance fields).
  • Docs: the drift bot (6127777257) names 32 hand-written pages touching changed anchors, with authorable-surface/api.json uncovered. No hand-written page enumerates these five routes' query parameters (content/docs/api/metadata-api.mdx and references/api/rest-server.mdx checked), so no stale list ships; content/docs/automation/email-templates.mdx line 219 describes the by-name read and could name ?member= — optional, not blocking.
  • Check-runs: six Test Core shards were still running at 16:07Z; their conclusions are the suite verdict on this head and are to be read before any arming.

Adopted by domain:engine seat 2 at 2026-10-12T16:12Z, after a tool-use audit of the isolated reviewer's transcript. It made 58 tool calls: reads, git fetch into a review ref, scratch files inside this PR's scratch directory, and one record write. It made no GitHub write and ran no build, test or gate. The seat re-read the blocking finding itself: SchemaRegistry is exported from packages/objectql/src/index.ts, the PR adds an optional member argument to its public methods, and the WHICH LEVEL rule (pr-automation.yml, the maintainer's batch #35 on #15294) grades an additive widening of an exported surface at least minor.

Implemented-by: claude/issue-22877-member-selector
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: FAIL

The one blocking finding is ② — @objectstack/objectql graded patch for an additive public-surface widening the maintainer's WHICH LEVEL rule grades minor. Fold into the same patch round: the Widens: sentence naming the event field, the repository header and the registry arguments. Every judgment in ① holds at this head and is a delta re-read at the next; the three escalated cards are the seat's to file, not the dev's to build here.

… take an optional member (#22877)

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2c055c088f2e75046eed9e17b8fa9203a38e3019
Local-runs: none

Delta record after the FAIL on 2b882e6774 (6128093700). Inputs: card #22877 (body and all 22 comments; ruling 6116165299, seat answers 6118224899, landing record 6121513183, stage-2 claim 6123966682, dev report 6127796890, seat note 6128113137, patch-round report 6128562834), PR #23070 (the body as edited at 16:44Z, the 14-file list, the net diff df7a137923..2c055c088f fetched into refs/review/pr-23070-r1, the delta 2b882e6774..2c055c088f, the drift-bot comment 6127777257 and the prior record 6128093700), and the head's check-runs (REST, read at 16:46Z). Nothing was built, run or re-run.

Check-runs on the head at 16:46Z: 48 runs, 41 success, 7 skipped, 0 running, 0 failed. The skipped seven are the opt-in and matrix placeholders (Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, Packed-tarball smoke, the Dogfood Regression Gate matrix shard) plus the Auto Label and Check PR Size runs of the 16:44Z edited trigger. Test Core 1/6 through 6/6 and the Test Core aggregate are success (the suite verdict the prior record was waiting on). Check Changeset is success on both triggers: the push at 16:14Z and the body edit at 16:44Z. Every lint, type-check, build, docs, claim, size and single-writer gate is green.

① Derived judgments

  1. The delta changes no code. 2c055c088f is one commit whose sole parent is 2b882e6774; git diff --name-status between the two heads lists .changeset/22877-member-selector.md alone (+11 / −1). The net diff against the unchanged merge-base df7a137923, with .changeset excluded, hashes identically on both heads (sha256 d48e30a8a632…), so every code hunk is byte-identical to the one the prior record read. 14 files, +1385 / −105. No rebase, no merge, no force-push.

  2. The prior record's ① 1 to 9 therefore hold on this head unchanged, and were spot re-read in the head's tree rather than taken on trust: the four SchemaRegistry methods (getItem, getArtifactItem, removeRuntimeShadow, removeOverlayEntry, each with the optional trailing member, the non-discriminated branches untouched), the SysMetadataRepository.listDrafts header (storedRowMemberHeader), the four spec request fields with the one shared describe, MetadataMutationEvent.member (stamped through mutationEventMember, empty for the canonical member and for every type with no members), and the five door request literals on ObjectStackProtocolImplementation (rollbackMetaItem's included).

  3. Two of the prior record's non-blocking notes are now carried in the PR's own text. The handler-raised ?layers= plus ?member= refusal that the deviation note omitted (prior ① 3) is now a bullet under "The value refusal's envelope". The no-canonical-bundle edge (prior ① 5) is now stated in the changeset's code-user section ("A bundle with no en-US member still answers its first member"); the administrator section's "naming en-US is the same request" remains inexact for that one bundle, as before; unpinned, deliberate per the code comment, not blocking.

  4. Prior ① 9 (the runtime dispatcher's silent drop of ?member=) is unchanged and remains the seat's card, not this PR's; see ③.

  5. The suite verdict is now in. The six Test Core shards the prior record could not wait for are success, so the derived gate families the head's check-runs answer are all answered green.

② Semver level

Changeset .changeset/22877-member-selector.md at the head: @objectstack/metadata-protocol minor, @objectstack/rest minor, @objectstack/spec minor, @objectstack/objectql minor. PR body line 1 Fixes #22877, line 2 Clause-②: yes, line 3 Widens:; the changeset body carries Clause-②: yes too, one arm, no BREAKING banner, no ADR-0087 disposition (none is owed: nothing is removed or narrowed on a published surface beyond the closed-set conversion AGENTS.md route rule 5 prescribes and the ruling cites).

  • The blocking finding is resolved. @objectstack/objectql goes from patch to minor. SchemaRegistry is exported from packages/objectql/src/index.ts (line 37), and four of its public methods gain an optional member argument: a purely additive widening of a published package's public surface, which the WHICH LEVEL rule (.github/workflows/pr-automation.yml lines 754 to 767, the maintainer's batch [WIP] Add query enhancements and advanced validation features #35 on finding(changeset): two independent contract reviews read the repo's own history to opposite bumps for "add an exported symbol to a published index" #15294) grades at least minor. The level now matches the act.
  • Each other level re-checked against what the diff publishes, under the same rule:
    • metadata-protocol minor, right: ObjectStackProtocolImplementation (index line 3) accepts member on its five by-name door methods; MetadataMutationEvent (index line 127, and the event of the exported ClusterMetadataMutationPayload) gains an optional field; SysMetadataRepository (index line 141) serves an optional member on each listDrafts entry. The module-level BUNDLE_MEMBER_SELECTOR, BUNDLE_MEMBER_MAX_LENGTH and resolveBundleMemberSelector are not re-exported from the index, and the package has a single . export, so they are not published surface.
    • rest minor, right: the five routes accept member, and each declares its closed set. META_ITEM_MEMBER_PARAM and the five META_ITEM_*_PARAMS constants are not on packages/rest/src/index.ts (which re-exports only RestServer, RestProtocol, MountedRoute and metaItemPackageBinding from rest-server.ts), so they are not published surface either.
    • spec minor, right: GetMetaItemRequestSchema, DeleteMetaItemRequestSchema, PublishMetaItemRequestSchema and HistoryMetaItemRequestSchema each gain the optional member; BUNDLE_MEMBER_SELECTOR_DESCRIPTION is a module-private constant; authorable-surface/api.json gains exactly the four keys.
    • No other published package moves: @objectstack/dogfood is private: true; content/docs releases nothing.
  • The changeset's new section ("For code that uses these packages directly"), each claim against the diff: getItem(type, name, packageId, member) answers that member over the unchanged tier order or undefined (entriesOfMember(tiers, disc, member)[0]); with no member it answers the canonical member and falls back to the first member of a bundle that has none (entriesOfMember(tiers, disc, disc.canonical)[0] ?? tiers[0]); getArtifactItem narrows the same way; removeRuntimeShadow and removeOverlayEntry retire one member's entries when named and keep the group sweep when not; the non-discriminated branches do not read the argument, so a type with no members ignores it; MetadataMutationEvent.member is present only for a non-canonical member (overlayMemberValue answers null for the canonical or blank value and undefined for a type with no discriminator, and the spread is then empty), reaches onMetadataMutation listeners (signature at protocol.ts line 6923) and rides ClusterMetadataMutationPayload; the listDrafts header is present for a non-canonical draft and the protocol's GET /meta/_drafts strips it. All hold. One nuance for the record, not a changeset claim: entriesOfMember reads a blank member as the canonical one (member.trim() || disc.canonical), so a direct registry call with '' answers the canonical member rather than nothing; the protocol and REST doors refuse a blank before it reaches the registry, and the describe's "naming the canonical value is the same request" is consistent with it. Not blocking.
  • The Widens: line now names every item the prior record asked for: the five doors' query parameter, the four request fields, the four SchemaRegistry methods' last argument, the MetadataMutationEvent field with the cluster payload that carries it, and the listDrafts header. One residual, not blocking: the protocol-level request literal of rollbackMetaItem (the exported implementation's fifth door, which has no spec request) is covered by the sentence's "five by-name doors" clause rather than named as a protocol-level item; metadata-protocol is graded minor regardless, and the prior record accepted "the REST closed set and the implementation signature" as that door's carrier. A few words if the seat edits the body again; nothing to hold the queue for.
  • check:changeset-no-major and Check Changeset are green on the head, including the re-run the body edit triggered.

③ Boundary flags

Dev's flags this round (6128562834), each answered:

  • The PR body was not PATCHed by the dev (the os-dev.md rule: the dev writes the body once and names later edits for the seat). The seat wrote both prepared edits: the body's updated_at is 16:44:36Z, line 3 is the new Widens: sentence verbatim, and the ?layers= plus ?member= bullet is present under "The value refusal's envelope". Closed. Check Changeset re-read the edited body and is green.
  • The Evidence section still says "The final head is 2b882e6774". Stale by one changeset-only commit; every code-bearing measurement was taken there and the code is byte-identical here, and the head's 48 check-runs are the record on this head. Accepted. An optional one-line seat edit ("2c055c088f adds the changeset level and section; no code") would make the body self-consistent.
  • No suite or ablation re-run this round: right, since no code changed; the dev re-derived the 118-command gate union on the new head and reports 118 exit 0, and CI's full set is green.
  • Worktree recreated: not a contract matter.

The prior record's escalations, against the seat note 6128113137:

  • The seat's note names two follow-up families to file at ACCEPT: (1) transport parity for the selector (the runtime dispatcher's item GET with no closed set and no member; PUT /meta/:type/:name with no closed set, so ?member= is dropped silently there; the remaining by-name doors /diff, /layers, /audit, /published, /references); (2) the drafts and commits lists with Studio's DraftChangesPanel / ResourceEditPage and the client SDK's meta.* methods. Both right.
  • The prior record escalated a third, which the seat's note does not list: the envelope graft, where mergeArtifactProtection stamps an unshipped member's hydrated overlay with the bundle's _packageId and _provenance, so isCodeArtifactBody reads it as code; this PR routes around it in removeRuntimeShadow (pinned for a shipped member, an unshipped member, a no-package bundle and the no-member sweep), and every other isCodeArtifactBody consumer that meets a bundle member inherits the misclassification (removeOverlayEntry alone, the disabled-package gate, the layered view's provenance fields). The dev's round-1 report lists it among the review's escalations; it is a measurement card for the seat to file alongside the two families, or to fold into one of them explicitly. None of the three blocks this PR: each is outside the claim's surface or pre-existing, and the ruling's five doors are built and pinned here.

Added or re-read by this review:

  • Docs: the drift bot re-ran on the new head (6127777257, updated 16:15Z) with the same anchors; the prior judgment holds since no code moved: no hand-written page enumerates the five routes' query parameters, so no stale list ships; content/docs/automation/email-templates.mdx could name ?member= on the by-name read, optional, not blocking.
  • Mixed-version note unchanged: a replica on the previous release ignores MetadataMutationEvent.member and re-reads the canonical row until its next boot; transient and self-healing.
  • Nothing is outstanding on the head before arming beyond the seat's usual merge-tree and governed-surface checks: the check-runs are complete and green, the changeset levels match the acts, and the declarations on lines 2 and 3 match the diff.

Adopted by domain:engine seat 2 at 2026-10-12T16:53Z, after a tool-use audit of the isolated reviewer's transcript. It made 30 tool calls: reads, git fetch into a review ref, and one record write. It made no GitHub write and ran no build, test or gate. This record supersedes the FAIL 6128093700 for landing purposes; that record stays as history.

Implemented-by: claude/issue-22877-member-selector
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

The FAIL's one blocking finding is closed: @objectstack/objectql is minor, and the Widens: line names the event field, the repository header and the registry arguments. The delta is changeset-only and the code hunks are byte-identical to 2b882e6774, so every ① judgment of the prior record carries to this head; the six Test Core shards it was waiting on are green. For the seat: file the envelope-graft measurement card with the two follow-up families, and optionally refresh the Evidence section's "final head" sentence.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 16:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 16:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 038d4c9 Oct 12, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22877-member-selector branch October 12, 2026 17:24
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

2 participants