[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
Description
Ruled: 6116165299 · letter A (Q2 i) · 2026-10-12T01:32Z
Re-presented: analysis 6115923019 · the deferral 6115691537 is withdrawn on the maintainer's word 「所以 22877 要和我重新讨论分析新的开发方案」 · 2026-10-12T01:04Z
Ruled: 6115691537 · letter deferred (option E recorded, analysed on restart) · 2026-10-12T00:38Z
Restart-when: closed #15205 (left pm:on-hold, see the line above)
Restart-touch: packages/metadata-protocol/src/migrations/overlay-index.ts
Filing gate, class (a): a reproducible defect, reach: a public door (PUT /meta/email_template/...), measured once on the showcase by the #15205 dev (report 6111776721). Filed by the domain:services seat 2 (seat post #21118, session_013LbZ9MhPp1iriZEtgJqioA). ⛔ Not a claim.
Reader: triage (grade and lane).
What was measured
- Save
PUT /meta/email_template/NAMEwith localeen-US, then save the same name with localezh-CN. Both read back as saved. - Restart.
en-USserves the package wording again:sys_metadatakeeps one overlay row per(type, name), so the second locale's save replaced the first's. - Pre-existing: the row path that refactor(plugin-email): templates resolve the registry; the seed and the provenance stamp retire; organization-level editing is closed (ADR-0131 D6/D10) #15205 stage 2 retires also reverted at boot. It is not introduced by PR feat(plugin-email)!: email templates resolve from the registry; the built-in seed, the boot sweep and the live projector retire (ADR-0131 D6/D10, C4 stage 2) #22875, and that PR does not fix it.
Related, closed
- email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730 (a declared i18n bundle collided in the registry, keyed by name without locale), plugin-email: an email template edited through PUT /meta/email_template reverts to the package wording on the next boot (the overlay is stamped customized:false, the boot sweep overwrites it) #21785 (a PUT overlay reverted at the next boot via the boot sweep), GET /meta/email_template can collapse an i18n bundle back to one row — the list merge dedups by (package, name), no locale #7774 (a list merge deduped by package and name). Same family (locale is not part of an email template's overlay identity); this is the overlay-storage face.
Done when
- Two locales of one email template, each saved through the metadata door, both survive a restart and each serves its own wording; CONTROL: a single-locale save is unchanged.
Duplicate check
MCP search_issues on this repo, closed cards included: "email_template overlay locale reverts after restart sys_metadata one row per type name" → 8 hits, the related closed cards above; none is this storage face.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 11, 2026
Triage: first grade,
bug·priority:p2·domain:engine·area:i18n, intopm:queue. Direction: the overlay keeps the identity the spec and the registry already useTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-11T17:53Z. ⛔ Not a claim, ⛔ not a dispatch.sys_metadatarows written by the metadata door, and the boot hydration that reads them) isdomain:engine. That is the lane that fixed email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730.email-template.zod.tsdeclares that rows "with the same name but different locale form an i18n bundle".email_templateby name and locale.(type, name)is the one layer that disagrees. It is not a new design question.(name, locale)identity, so two locales are two overlays.sys_metadatacolumn or a changed unique key, that is a migration shape. Stop, and file it as a decision with the measurement.Clause-②: no. The store comes to meet the declared identity.objectstack-fleet commentedon Oct 11, 2026
Claim: PM loop round 1 · 2026-10-11T20:47Z
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22877-locale-bundle-overlayWorktree:
objectstack-issue-22877Domain:
domain:engineSeat:
domain:engine#2(seat post #20966); the lane's only seated PMFile surface (read on
origin/main2e11d9006c; stop on a breach and explain it in the report):saveMetaItemwrites asys_metadataoverlay row and how boot hydration and the read paths key it (packages/metadata-protocol/src/protocol.ts), so two locales of oneemail_templateare two overlays, matching the pair identity the registry already uses (the table in@objectstack/metadata-core, re-exported bypackages/objectql/src/registry.ts, email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730 / GET /meta/email_template can collapse an i18n bundle back to one row — the list merge dedups by (package, name), no locale #7774).@objectstack/metadata-core's pair-identity table andobjectql's registry keying; edit only if the overlay needs the same key the table already declares.sys_metadatacolumn or a changed unique key, that is a migration shape — stop and report it with the measurement, for a decision card.plugin-email,platform-objectsorsys_email_template(services seat 2's refactor(plugin-email): templates resolve the registry; the seed and the provenance stamp retire; organization-level editing is closed (ADR-0131 D6/D10) #15205, PR feat(plugin-email)!: email templates resolve from the registry; the built-in seed, the boot sweep and the live projector retire (ADR-0131 D6/D10, C4 stage 2) #22875, in flight).Container & model:
M(measure first: how the row can carry the locale),mode:subagent,model: default(dispatch-gates --tier: no path-derived mandate)Clause-②: no
Responsibility: the metadata door's overlay store, which keeps one
sys_metadatarow per(type, name)for a type whose declared identity is(name, locale)| the registry's pair keying foremail_template(email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730) and the list merge's (GET /meta/email_template can collapse an i18n bundle back to one row — the list merge dedups by (package, name), no locale #7774) | an administrator who customizes one email template in two languages throughPUT /meta/email_template/…; measured on the showcase by refactor(plugin-email): templates resolve the registry; the seed and the provenance stamp retire; organization-level editing is closed (ADR-0131 D6/D10) #15205's dev (the first locale's wording is lost at the next boot)Thread-read: 6111942095
Serial constraints cleared: no open PR touches
metadata-protocol's overlay write or hydration (PR feat(plugin-email)!: email templates resolve from the registry; the built-in seed, the boot sweep and the live projector retire (ADR-0131 D6/D10, C4 stage 2) #22875, refactor(plugin-email): templates resolve the registry; the seed and the provenance stamp retire; organization-level editing is closed (ADR-0131 D6/D10) #15205, editsplugin-emailandplatform-objects, a semantic neighbour in the email-template family, files disjoint). This seat's in-flight objectql:ScopedContext.transaction()publishes an ambient transaction entry it never closes, so anasynchook's later write inside it is lost ("Transaction query already complete") #22855 (objectqlengine.tstransactions), security(runtime):callData's engine fallback hands a caller'swheretoql.findafter judging only the addressed object, so a nested-relation condition skips the target's exposure ask (census row 10 of #22661) #22823 (runtimecallData) and the held objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scopedrequiredWhenmay disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519 (engine.tsheader read) are disjoint.objectstack-fleet commentedon Oct 11, 2026
os-dev-report
{
"issue": 22877,
"status": "needs_decision",
"branch": "claude/issue-22877-locale-bundle-overlay",
"pr": null,
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent of the domain:engine seat 2 PM; parent's id)",
"premise_still_valid": true,
"summary": "Premise holds, reproduced on origin/main 45e206e with the real ObjectQL engine, an in-memory driver and the real protocol (scratch test, never committed): saveMetaItem email_template acme.welcome en-US then zh-CN leaves ONE sys_metadata row (the zh-CN body). A reboot over the same store (new engine and protocol, loadMetaFromDb loaded=1) serves the package wording for en-US. Same family: getMetaItem by the bare name then serves the zh-CN overlay, not the canonical en-US member that the registry's bare-name read resolves to. Measured mechanism: the overlay unique key is (type, name, organization_id, COALESCE(package_id,'')) WHERE state='active', plus a draft twin (migrations/overlay-index.ts:137 OVERLAY_INDEX_COLUMNS; declared idx_sys_metadata_overlay_active in metadata-core sys-metadata.object.ts:226-230), with no locale column. saveMetaItem writes ref={type,name:request.name,org:'env'} (protocol.ts:20782), and SysMetadataRepository.put resolves the row through resolveWriteHead/whereFor (sys-metadata-repository.ts:655, :2092) by that key, so the second locale updates the first locale's row in place. Boot hydration (loadMetaFromDb, protocol.ts:26850, through hydrateOverlayIntoRegistry) and the list merge (mergePackageAwareOverlay slotOf, protocol.ts:2187) are already body-keyed by (name, locale). They would carry two rows correctly. The store's write address, and every by-name door that reads rows by (type,name), is the one layer that disagrees. STOP CONDITION HIT (triage 6111942095). Inside the existing unique key, only the name column can carry the locale. The registry's bundle-key spelling there (name@locale) is refused by the item-name grammar METADATA_ITEM_NAME_PATTERN (spec shared/identifiers.zod.ts:166). refuseUngrammaticalMetaItemName (protocol.ts:19437) enforces that grammar on saveMetaItem, on publishMetaItem, and on migrateStoredMetadata, which re-saves each row under its stored name. Measured: saveMetaItem name 'acme.welcome@zh-CN' returns 400 INVALID_REQUEST, 'is not a legal metadata item name ... express containment structurally, never with a separator inside the identity string'. That is the maintainer-ruled retirement of compound addressing (commit 311433f). Deriving that row name inside the store, past the door check, would mint rows the grammar calls illegal. os migrate meta --stored would fail each of them. A non-canonical member's draft could never be promoted through publishMetaItem. GET and DELETE of a member would need an ungrammatical name. A grammatical spelling such as an extra dot segment (auth.welcome.zh_cn) collides with real dotted template names and folds BCP-47 case lossily. So no route fits within the existing row: what is left is a new column with a changed unique key (the triage's migration shape) or a new stored body shape. Every route also needs a by-name door address for a non-canonical member: deleteMetaItem, getMetaItem and publishMetaItem take only (type, name), and ?locale= on /meta is already the UI translation locale (rest meta-item-read-gate.ts:2294-2321). Without that address, a second-locale overlay could not be deleted. Other locale-bundled types: ITEM_KEY_DISCRIMINATORS (metadata-core item-key-discriminators.ts:68) lists only email_template. translation is overlay-enabled and has a locale key, but its identity is the name alone, so it does not share this problem. PR #22875 compatibility: its loader (template-loader.ts on head 4aa656e) reads getMetaItemsForExecution and filters by the body's name and locale, and every option below keeps the list bodies' name bare. No code written, no PR. The empty branch was pushed as the claim's landing marker.",
"tests": "Measurement only, no fix and therefore no ablation. (1) bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build" -> 'os-verify-lock: VERDICT command-exit 0 · held the lock 222s'. (2) Scratch packages/objectql/src/zz-scratch-22877.test.ts (copied the harness of environment-overlay-five-types.test.ts: real ObjectQL, in-memory driver, real ObjectStackProtocolImplementation, env_prod; package com.acme.mail ships acme.welcome en-US 'PKG en' and zh-CN 'PKG zh'), run via pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/zz-scratch-22877.test.ts, exit 0, 'Tests 2 passed (2)'. Printed readings: SAVE1/SAVE2 success:true; ROWS [{name:acme.welcome,locale:zh-CN,subject:ADMIN zh}]; LOAD {loaded:1,errors:0}; LIST-AFTER [[en-US,PKG en],[zh-CN,ADMIN zh]]; GET-BY-NAME [zh-CN,ADMIN zh]. Second case: saveMetaItem name 'acme.welcome@zh-CN' -> {err:INVALID_REQUEST, msg:'"acme.welcome@zh-CN" is not a legal metadata item name. ...'}; deleteMetaItem name 'acme.welcome@zh-CN' -> success:true 'nothing to delete' (the delete door does not apply the grammar). Scratch file deleted afterwards; git status --porcelain empty, git diff HEAD empty. (3) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> exit 2, 'this branch changes nothing against origin/main (merge base 45e206e) — nothing to derive'.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "1 — POST /repos//issues/22877/comments (this os-dev-report, via scripts/pm/post-stamped.mjs). Not REST: git push -u origin claude/issue-22877-locale-bundle-overlay (empty branch, the claim's landing marker and write probe). REST reads with the session's gh: GET issues/22877, GET issues/22877/comments, GET pulls/22875, GET pulls/22875/files; git fetch of PR 22875's head into a private ref, deleted at cleanup.",
"open_questions": [
{
"question": "Q1: How does a sys_metadata overlay row carry the bundle member (locale) of a locale-bundled type, so that two locales of one email_template are two overlays? Measured: no route fits inside the existing row. The name column is the only in-key carrier, and the item-name grammar (ruling commit 311433f) refuses a separator inside the identity string.",
"options": [
"A — Structural member column. Add one nullable discriminator column to sys_metadata. NULL means the canonical member, so every existing row and every undiscriminated type stays byte-identical. Add COALESCE(column,'') to the active and draft partial unique indexes and to the declared index, through the existing ADR-0120 D4 probe-then-replace ceremony (partial-index-probe.ts, os migrate duplicates). Backfill pre-fix rows from their body's locale. The repository's write head, delete, history lineage and version counter gain the member; the write door takes it from the body through itemDiscriminator. Cost L: metadata-core object, overlay-index migration, repository, protocol doors, objectql restart pins, a dogfood case. Business need: real and measured. Built-in auth templates ship 6 names x 4 locales (24 members, plugin-email templates/auth-templates.ts at 45e206e). PR #22875 makes sends resolve by (name, locale) from the layered list. The defect was measured on the showcase. Long-term: meets the declared identity structurally, which is what the grammar ruling prescribes; one key across registry, list and store. AI-proofing: the identity is declared once (ITEM_KEY_DISCRIMINATORS) and the store's unique key enforces it; no ungrammatical names; a duplicate is refused loudly by the index. Startup focus: delivers an already-declared capability and adds none; no alias, no staging, no new gate; the cost is a real index migration.",
"B — Encode the member into the name column (the registry's bundle key, name@locale) and widen METADATA_ITEM_NAME_PATTERN for discriminated types. Cost M, plus a spec change. Business need: met. Long-term: reverses the compound-addressing retirement (commit 311433f, 'never with a separator inside the identity string'). It also persists a key format that metadata-core's docblock explicitly declined to publish even across packages. AI-proofing: worse, because it re-admits separator-bearing identity strings at the door. Startup focus: smaller now, but it is a ruling reversal and leaves debt. Not recommended.",
"C — Keep one overlay per (type, name) and make it loud. The save door refuses (409, naming the stored member and the remedy, which is to delete that overlay first) a save whose member differs from the stored overlay's, instead of silently replacing it. GET by the bare name serves the canonical member. Cost S. Business need: NOT met, because an environment can customize only one language per template, against the measured pull above. Long-term: narrows the store below what spec declares (a bundle resolved by (name, locale)): a declared capability left unimplemented. AI-proofing: loud, but the spec would still advertise bundles. Startup focus: smallest. Acceptable only as an explicit interim.",
"D — One overlay row per bundle: the stored body holds every customized member, a stored container like the view containers' expansion. Cost L+: a spec'd container schema, stored-row conversions, and expansion in every row reader (the precedent stored view containers carried known complexity). Business need: met. Long-term: two stored shapes for one type. AI-proofing: a second shape for AI to learn. Startup focus: the largest expansion."
],
"recommendation": "A. It is the only option that meets the measured business need without reversing a ruling: the store comes to meet the declared identity, structurally, which is what the grammar ruling prescribes. It also adds no authorable surface, keeps undiscriminated types byte-identical, and pays for itself with one index migration through an existing ceremony. Pins carried over from the triage: two locales saved, then a restart, each serves its own wording; CONTROL a single-locale save is unchanged; CONTROL a package-shipped bundle is unchanged."
},
{
"question": "Q2 (needed by A, B and D): how do the by-name doors (GET, DELETE, publish, history and revert of /meta/email_template/NAME) address a non-canonical member? Today they take only (type, name), and ?locale= on /meta is already the UI translation locale. Without an address, a second-locale overlay could not be deleted.",
"options": [
"i — PUT stays body-addressed (the member comes from the body's locale, as today). The by-name doors 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 locale. Absent, it means the canonical member, which is also what the registry's bare-name read answers. Cost M.",
"ii — No per-member address. GET by the bare name serves the canonical member; DELETE by the bare name resets the whole bundle's overlays. Cost S. A single member can no longer be reverted on its own.",
"iii — A path segment per member (/meta/email_template/NAME/locales/TAG). Cost M-L. A new route shape that every door and the route ledger must learn."
],
"recommendation": "i. It keeps the door shape, makes each member individually readable and revertable, and stays a closed, declared parameter with a located 400 when it is wrong. The canonical default keeps the by-name read consistent with the registry."
}
],
"out_of_scope_findings": [
"class: a · reach: public door (getMetaItem / GET /meta/email_template/NAME after en-US then zh-CN saves serves the zh-CN overlay instead of the canonical en-US member, measured in the scratch run at 45e206e) · same family as #22877 — not filed separately: fold into #22877's decision card (Q2 decides the by-name read) · dedupe words: email_template getMetaItem bare name canonical member overlay locale",
"carrier: the decision card the seat files for #22877 · noted, not filed — translation is overlay-enabled and has a locale key, but its identity is the name alone (not in ITEM_KEY_DISCRIMINATORS), so it does not share this problem; no finding"
],
"gates": [
{
"command": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack",
"exit": 2,
"note": "empty diff: 'this branch changes nothing … nothing to derive'"
},
{
"command": "dispatch gate list (69 commands, dispatch-gates.txt)",
"exit": null,
"note": "NOT RUN, reason: no diff — needs_decision returned before any code"
},
{
"command": "bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '@objectstack/objectql^...' build"",
"exit": 0,
"note": "measurement prerequisite, VERDICT command-exit 0"
},
{
"command": "pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/zz-scratch-22877.test.ts",
"exit": 0,
"note": "scratch measurement, file deleted, not committed"
}
],
"line_budget": "0 — no PR, no commit (additions 0 + deletions 0)",
"deviations": [
"Returned needs_decision although triage 6111942095 said 'If it fits within the existing row (for example, the registry's own bundle key), build it'. Measured: the registry's bundle key in the row's name is refused by the item-name grammar (ruling commit 311433f), so it does not fit. That leaves the triage's own stop condition. Conflict named here, not resolved silently.",
"Worktree based on origin/main 45e206e. origin/main had advanced from the PM's 2e11d90 by the time of the fetch, so every reading is cited against 45e206e.",
"The dispatch gate list (69 commands) was not run, because there is no diff (dispatch-gates --commands on the branch exits 2, 'nothing to derive').",
"One scratch test file was created in the worktree for the measurement and deleted uncommitted. The objectql dependency closure was built under the verify lock for it."
],
"files_changed": []
}
Generated by Claude Code
objectstack-fleet commentedon Oct 11, 2026
Decision: how an environment overlay keeps two languages of one email template
domain:engineseat 2 (#20966) ·os-steve·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-11T21:09Z. Raised by this card's dev (os-dev-report 6113736478,needs_decision), which hit the triage stop condition (6111942095: "If the only way is a newsys_metadatacolumn or a changed unique key, that is a migration shape. Stop, and file it as a decision with the measurement"). Anchored on this card; the card leaves the active queue until answered. ⛔ Classes, positions and functions only.Reader who acts: the maintainer (through the director's batch) picks one letter for Q1 and one numeral for Q2; then the
domain:enginelane re-claims this card.维护者速读
Background (measured on
origin/main45e206e498)email_templateacme.welcomeinen-US, then inzh-CN→ one overlay row (thezh-CNbody). Reboot over the same store →en-USserves the package wording. Same family: a read by the bare name then serves thezh-CNoverlay, not the canonicalen-USmember the registry's bare-name read resolves to.(type, name, organization_id, COALESCE(package_id,''))for active rows, with a draft twin; no locale.saveMetaItemwrites by(type, name), and the repository's write head resolves the row by that key, so the second locale updates the first locale's row in place./metalist merge key bodies by(name, locale)(email-template-render (a): declared i18n bundle collides — Registry keys email_template without locale, only the last locale survives #7730, GET /meta/email_template can collapse an i18n bundle back to one row — the list merge dedups by (package, name), no locale #7774). They would carry two rows correctly. The store's write address and the by-name doors are the one layer that disagrees.nameis an in-key carrier. The registry's bundle-key spelling there (name@locale) is refused at the save door: measured400 INVALID_REQUEST, "is not a legal metadata item name … never with a separator inside the identity string". A grammatical spelling (an extra dot segment such asauth.welcome.zh_cn) collides with real dotted template names and folds BCP-47 case lossily.email_templateis the only type inITEM_KEY_DISCRIMINATORS.translationhas a locale key, but its identity is the name alone, so it does not share this problem.domain:services, in flight) resolves sends by(name, locale)from the layered list, so a lost member is a lost send wording. Every option below keeps list bodies'namebare, so it stays compatible with PR feat(plugin-email)!: email templates resolve from the registry; the built-in seed, the boot sweep and the live projector retire (ADR-0131 D6/D10, C4 stage 2) #22875.Governing text
packages/spec/src/system/email-template.zod.ts(locale): "IETF BCP-47 locale tag — the second half of the bundle key. Multiple rows sharing onenameform an i18n bundle".packages/spec/src/shared/identifiers.zod.ts(METADATA_ITEM_NAME_PATTERN): "stage 1 of the maintainer-ruled retirement of compound<section>/<name>addressing, 2026-08-25"; enforced atsaveMetaItem/publishMetaItem.packages/metadata-core/src/item-key-discriminators.ts: "Every layer that keys metadata bynametherefore has to agree on ONE answer to 'what else is part of this type's identity?'". The same file declined to publish the registry's key encoding across packages.(name, locale); the store is the layer that falls short, so no option changes the protocol except B.Premises (each with a re-check)
git grep -n "OVERLAY_INDEX_COLUMNS =" origin/main -- packages/metadata-protocol/src/migrations/overlay-index.ts→['type', 'name', 'organization_id', 'package_id'].git grep -n "METADATA_ITEM_NAME_PATTERN = new RegExp" origin/main -- packages/spec/src/shared/identifiers.zod.ts, then read the segment[a-z][a-z0-9_]*above it.email_templateis the only discriminated type. Re-check:git grep -n -A2 "ITEM_KEY_DISCRIMINATORS:" origin/main -- packages/metadata-core/src/item-key-discriminators.ts.git show origin/main:packages/plugins/plugin-email/src/templates/auth-templates.ts | grep -c "locale: '"→ 24.?locale=on/metaalready means the UI translation locale. Re-check:git grep -n "?locale=" origin/main -- packages/rest/src/meta-item-read-gate.ts(the[#20320]docblock: Accept-Language, then?locale=).Q1 — how the overlay row carries the language
sys_metadata(NULL = the canonical member, so every existing row and every undiscriminated type stays byte-identical);COALESCE(column,'')joins the active and draft unique indexes through the existing probe-then-replace index ceremony (ADR-0120 D4,os migrate duplicates); pre-fix rows backfilled from their body's locale; the repository's write head, delete, history lineage and version counter carry it; the save door takes it from the body.domain:engine· 1–2 PRs (metadata-core object, overlay-index migration, repository, protocol doors, restart pins, a dogfood case) · precedent PR #6770 (+1391 / −164, the probe-then-replace ceremony) and PR #11031 (+1071 / −42, the duplicates report)name@locale) inname, and widen the item-name grammar for discriminated types.domain:engine+domain:spec· 1–2 PRs · no precedent; reverses the 2026-08-25 grammar ruling(type, name)and make it loud: a save whose language differs from the stored overlay's is refused409, naming the stored language and the remedy (delete that overlay first); a bare-name read serves the canonical member.domain:engine· 1 PR, small · no precedentdomain:engine+domain:spec· 2+ PRs · no precedent (the stored view containers carried known expansion complexity)Q2 — how a by-name door addresses one language (needed by A, B and D)
PUTstays body-addressed. GET, DELETE, publish, history and revert by name accept one member selector in their closed query-parameter set, named after the discriminator so it cannot be read as the UI locale; absent = the canonical member, which is what the registry's bare-name read answers; a wrong value is a located 400.domain:engine· rides the Q1 PR or one more · no precedent; widens the doors' accept set, soClause-②: yesand a contract review/meta/email_template/NAME/locales/TAG).domain:engine+domain:cli· a new route shape every door and the route ledger must learn业务含义直译: A = 「同一份合同模板,中文版、英文版各存一份,柜子上按『模板名 + 语种』编号」;B = 「把语种写进模板名里」,正是 8 月份明令禁止的编号方式;C = 「一个模板只准改一种语言,改第二种时当面拒绝」;D = 「把所有语种塞进同一个文件夹」。Q2 的 i = 「调某一语种的档案时报上语种」,ii = 「只能整套退回出厂版」。
四轴(业务立场):
(name, locale),终态即主流平台的建模:Zendesk Guide 的文章译本、Shopify 的 Translations 都以「资源 + 语种」为结构化键。B 推翻已裁的语法;C 让 spec 声明的多语种包在存储层落空,是声明未兑现;D 让一种类型有两种存储形状。ITEM_KEY_DISCRIMINATORS),唯一索引强制执行,重复保存会被响亮拒绝。B 重新放行身份串里的分隔符,而 8 月份收紧语法,正是因为/meta曾接受'Views/All Leads'这类拼写。C 拒绝得响亮,但 spec 仍对 AI 宣称支持多语种包。今天的现状最糟:静默覆盖,管理员看不到。推荐 Q1 = A,回退 C;Q2 = i,回退 ii。 A 与 C 工作量差 ≥3×,但①读数不相近(C 让已声明的能力在存储层落空),所以仍荐 A。置信缺口:按语种逐个撤销(Q2-i 的用途)今天没有实测调用方,Studio 的语种编辑界面是否会用到未核;生产部署里覆盖层
email_template行数未实测,迁移回填的规模不明。自检:只看① 选 A / i;②③④ 是否翻转:否。裁后执行
Clause-②: yes, with a contract review before landing. Contract-tier review for the migration either way.domain:speccard for the grammar widening first (it reverses a ruling, so the maintainer's words are quoted in it); this card then follows A's pins.domain:speccard for the stored container schema first.os-decision-facets
Prior rulings read: email_template overlay locale, sys_metadata unique key discriminator, item-name grammar compound addressing → 8 hits (#7730, #7774, #7932, #9135, #8195, #22005 closed; this card; #16612 off-topic); the 2026-08-25 grammar ruling as cited in
identifiers.zod.ts(commit311433f6b1); ADR-0120 D4; thread: this card (3 comments read).26 remaining items
objectstack-fleet commentedon Oct 12, 2026
Claim: PM loop round 1 · 2026-10-12T12:08Z · stage 2 of 2
Session:
session_01ADzJtzYTLUfgrRZHxkagkXAccount:
os-steve(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-22877-member-selector(new: stage 1's branch merged as5677add93fand is not reused)Worktree:
objectstack-issue-22877-s2Domain:
domain:engineSeat:
domain:engine#2(seat post #20966); the lane's only seated PMRuling-ref: 6116165299 (letter A, Q2 i). Its workload line places both stages in
domain:engine. Seat answers 6118224899; carried items in landing record 6121513183.File surface (read on
origin/main0ede0f8ab3; stop on a breach and explain it in the report). ⛔ Classes, positions and functions only, in every artefact.packages/metadata-protocol/src/protocol.ts:getMetaItem,deleteMetaItem,publishMetaItem,historyMetaItemandrollbackMetaItemaccept one member selector; absent means the canonical member;publishPackageDrafts,discardPackageDrafts,revertCommititems) and the cluster mutation fan-out carry the member;sys-metadata-repository.tsonly if a door needs it.packages/objectql/src/registry.ts:SchemaRegistry.getItem's bare-name read of a discriminated type answers the canonical member (the half-false premise recorded in 6118224899);removeRuntimeShadowaddresses one member on a by-name reset, not the bundle's whole hydrated group.packages/rest/src/rest-server.ts, cross-lanedomain:cli: the/meta/:type/:nameGET, DELETE,/publish,/historyand/rollbackhandlers declare the selector in their closed query-parameter set (refuseUnknownQueryParams, AGENTS.md route rule 5). A wrong value is a located400.packages/spec/src/api/protocol.zod.ts, cross-lanedomain:spec: the matching request schemas gain the selector field, only if the protocol request carries it (measured by the dev).packages/qa/dogfood/test/(test-only), one changeset.bundle_memberkeys (owed toarea:i18n). ⛔ Not ADR-0005's text (docs(adr): ADR-0005 still states the overlay uniqueness key as four parts — since 5677add93f (#22877 stage 1) it carries bundle_member (Tier H, the maintainer's hand) #23006, PR docs(adr): ADR-0005 states the five-part overlay key — bundle_member since 5677add93f (#23006) #23037, the maintainer's hand). ⛔ Not the client SDK.Fixes #22877.Container & model:
M(stage 2 of the ruled 1–2 PRs),mode:subagent,model: default.dispatch-gates --tier: no path-derived mandate, andpackages/spec/src/api/protocol.zod.tsis flagged as a Clause-② suspect. The contract review runs at the ceilingCONTRACT_REVIEW_TIERbefore the queue, in an isolated at-tier subagent: the card widens an accept set and touchespackages/specsource.Clause-②: yes
Responsibility: the
/metaby-name doors, which carry no member, so a non-canonical member cannot be read, reset, published, viewed in history or reverted by name (the window stage 1's changeset states) | none: stage 1 stores both members but leaves every by-name door canonical-only | an administrator who keeps one email template in two languages and resets or publishes the second; measured on stage 1's showcase dogfoodThread-read: 6121513183
Serial constraints cleared:
protocol.ts,rest-server.ts,registry.tsorprotocol.zod.ts. Read across the 15 open PRs at this stamp. PR test: the stage-8 test half — suites stop depending on the four catalog row objects (#15204 stage 8, S8-T) #22962 edits onlymetadata-protocoltest files. PR feat(spec,metadata-protocol)!: rename the type registry's allowOrgOverride to allowEnvironmentOverlay, on the registry and on GET /meta/types #22874 and PR feat(spec,cloud-connection,cli): the installModes manifest key and the install door's unmanaged mode (ADR-0131 D6, stage S2) #22916 are no longer open.packages[]body, so an option-B artifact built before a retirement is refused at boot ("packages[0]is not a package entry …dashboard.refreshIntervalwas renamed") #23027 is inmetadata-core's artifact conversion, which is disjoint.ReadableIdsleaf once per parent object, fail closed, refuses it on write verbs and for D5's three classes, and marks the scope'policy'(D2, D5) #22861 is blocked on spec: declare the RLS deny sentinel once in@objectstack/spec, imported byplugin-securityandobjectql(ADR-0139 D2 step 2; #22861 sub-question 2 ruled (i)) #23009, and its region is the executor boundary, not the registry or the/metadoors.wip(metadata-protocol): the by-name /meta doors accept a bundle membe…
test(objectql,rest,metadata-protocol): pins for the by-name member se…
fix(metadata-protocol): the selector reads either spelling of the type (
docs(spec): regenerate the protocol reference for the member selector (…
test(objectql): the bare-name revert CONTROL stands on its own (#22877)
fix(objectql): a member's shadow is lifted when the bundle ships any …
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22877,
"status": "done",
"branch": "claude/issue-22877-member-selector",
"pr": "#23070",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent of the domain:engine seat 2 PM; the parent's id)",
"premise_still_valid": true,
"summary": "Stage 2 of ruling 6116165299 (A, Q2 i) delivered as draft PR #23070 (Fixes #22877, Clause-② yes, head 2b882e6, +1375/-105, 14 files). The five by-name /meta doors (GET, DELETE, /history, /publish, /rollback) accept ONE bundle member through
member(the stored column's concept, neverlocale). The protocol reads it once (resolveBundleMemberSelector: absent = canonical; a selector on a type with no discriminator, a non-string, a blank or a value over the column's 100 characters is a located 400 VALIDATION_ERROR). Each REST route now opens with a measured closed query set, and the four spec requests (Get/Delete/Publish/HistoryMetaItemRequest) gain an optionalmember; rollback has no declared request. The carried items are built: SchemaRegistry.getItem's bare-name read answers the canonical member across tiers; removeRuntimeShadow/removeOverlayEntry and the protocol heal address one member; publishPackageDrafts, discardPackageDrafts, revertCommit and the cluster fan-out (MetadataMutationEvent.member) carry it. The showcase dogfood found one defect in the first cut — a hydrated overlay of a member the package does not ship wears the bundle's artifact envelope, so the heal left it serving until restart — fixed (a member's shadow is lifted while the bundle ships any artifact) and pinned in the objectql harness with a versioned package. Measured absent-member answers: GET 404 (item undefined), DELETE reset:false, publish 404 NO_DRAFT, history events [], revert 404 VERSION_NOT_FOUND.","tests": "All heavy runs under scripts/pm/os-verify-lock.sh (VERDICT command-exit 0 each); exit codes captured before any pipe. objectql full: 'Test Files 407 passed (407)', 'Tests 7955 passed' (before the last registry commit 2b882e6); after it, the 13 objectql files naming removeRuntimeShadow/removeOverlayEntry/email_template '246 passed' and restart harness + registry bundle file '40 passed'. metadata-protocol full at 12b4b50: 'Test Files 228 passed | 3 skipped (231)', 'Tests 28196 passed | 19 skipped'. rest full at 00886e1 (the main merge touched no rest file): 'Test Files 279 passed (279)', 'Tests 5960 passed | 327 skipped'; after the merge 6 targeted rest files '141 passed'. spec protocol.test.ts + type-alias-convention.pin.test.ts '187 passed'. typecheck exit 0: spec, metadata-protocol, objectql (+check:test-typecheck), rest (+check:test-typecheck), dogfood. dogfood email-template-two-locales-survive-boot.dogfood.test.ts '4 passed (4)' after objectql rebuilt at 2b882e6 (also measures that the showcase's /meta item read is RestServer's route). eslint --no-inline-config --format json over the 11 changed .ts files: 11 linted, 0 errors, 0 warnings; population = eslint.config.mjs files ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED; no type-aware linting (no parserOptions.project), so untouched files' verdicts cannot move. Ablations (scripts/ablation-replace.mjs wrap mode on committed HEAD fdf01eb: anchor 1->0, blob changed, restored blob == HEAD, git diff HEAD empty; metadata-protocol rebuilt with ablation-dist-preflight present then --absent): (1) rollbackMetaItem member dropped (marker __ABLATION_22877_ROLLBACK_MEMBER_DROPPED): harness '1 failed | 17 passed', only the member-revert pin red ('No history row at version 2 ... (its canonical member)'), bare-name revert CONTROL green; restored '18 passed'. (2) registry tier order reverted (__ABLATION_22877_REGISTRY_TIER_ORDER, objectql src): '1 failed | 38 passed', only the bare-name canonical registry pin red (the harness's protocol-level bare-name pins stay green because getMetaItem answers the canonical artifact before the registry); restored '39 passed'. (3) heal member dropped: '3 failed | 15 passed' (DELETE by member, DELETE reverse, cluster member reset); restored '18 passed'. (4) batch publish member dropped: batch answered refused / NO_DRAFT 'No pending draft exists for email_template/acme.welcome — nothing to publish.'; batch discard member dropped: discard answered success while the zh-CN draft stayed; both restored, dist absent.",
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "3 REST writes in 2 relay dispatches (objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/pulls (draft #23070) + POST /repos//issues/23070/assignees (os-steve) as one pr_create action, run 38273402759, body read back byte-identical (19409 sent = 19409 stored); POST /repos//issues/22877/comments (this os-dev-report, scripts/pm/post-stamped.mjs). Not REST: git push of the branch (the empty landing marker, then commits and one merge of main). Zero label writes (the dispatch forbade them). REST reads with the session's gh: issues/22877 and its comments, pulls/23070.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: 承接者:无 (the runtime lane) · noted in the PR's acceptance notes, not filed — packages/runtime/src/domains/meta.ts (the runtime dispatcher's /meta item read) does not read ?member=: a host served by the dispatcher alone would answer GET ...?member=zh-CN with the canonical member, since that transport has no closed query set at all. Outside the claim's surface; not measured on a dispatcher-only host (the showcase is served by RestServer, measured by the dogfood case). dedupe words: dispatcher meta member selector; runtime domains meta query; member selector transport parity",
"carrier: 承接者:无 · noted, not filed — GET /meta/_drafts and GET /packages/:id/commits keep their declared shapes and do not serve
member(the ruling widened the doors' accept set, not these responses), so two drafts or plan items of one template in two languages read as two entries with the same type and name; Studio's DraftChangesPanel would fetch the canonical body for both (not measured in a browser). dedupe words: drafts list bundle member; commit timeline member; DraftChangesPanel locale","carrier: 承接者:无 · noted — the by-name doors outside the ruling's list (/diff, /layers, /audit, /published, /references) stay canonical-only; the deprecated ?layers= spelling refuses ?member= with a 400 rather than answering the canonical member's layers.",
"carrier: 承接者:无 · noted — the MetadataService (MetadataManager) keys a type's items by name alone, so for a bundle it holds one member (the last registered); getMetaItem and the registry heal now accept its answer only when it is the addressed member. Pre-existing; the service is unchanged.",
"carrier: 承接者:无 · noted — the selector's VALUE refusal is raised by the protocol and rendered by the REST door's one error door (handleRouteError) in the flat position (body.code VALIDATION_ERROR, body.error the sentence), while the route's own recognition refusal of a misspelled name is nested (body.error.code); the existing split check:route-envelope ratchets repo-wide, same code on both."
],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-declared.mjs :: exit 0",
"node scripts/check-rest-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:generated :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:authz-resolver :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:filter-alias-parity :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:quick-reference-counts :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:route-envelope :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"gates_reconciliation": "node scripts/pm/dispatch-gates.mjs --ran (exit codes recorded) at 2b882e6: 118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN; the lead list (86 at 0ede0f8) is a subset. An earlier round at 12b4b50 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 at exit 0.",
"line_budget": "+1375 / -105 = 1480 changed lines in 14 files (PR #23070 as GitHub counts it); within the ruled 1-1.5k estimate (precedents PR #6770, PR #11031); under the 3,000-line Tier H threshold, no split.",
"deviations": [
"File surface: all edits are inside the claim's listed files, plus protocol.lookup-artifact-item-call-sites.test.ts (its census records the re-scoped artifact-lookup site and the definition's new
memberparameter) and two regenerated artifacts (packages/spec/authorable-surface/api.json, content/docs/references/api/protocol.mdx).","removeRuntimeShadow with a named member keeps the artifact test at the level of the NAME (any member's artifact), not the member — found by the showcase dogfood (a hydrated overlay of an unshipped member wears the bundle's artifact envelope, so removeOverlayEntry refuses it as code); pinned in the objectql harness, whose package is now versioned so its artifacts carry the envelope.",
"Response contracts unchanged by choice: the repository's draft headers and the stored commit plan carry
member, and the protocol's listDrafts/listCommits serve their declared keys only (see out_of_scope_findings 2).","No RollbackMetaItemRequestSchema was added: none exists in packages/spec, and the REST door calls rollbackMetaItem through a cast; the field was added only where the protocol request is declared.",
"Test-suite heads: objectql full ran before the last registry commit (then targeted reruns at 2b882e6); rest full ran pre-merge at 00886e1 (the merge touched no rest file; targeted rest reruns after); metadata-protocol full at the merge 12b4b50. CI runs the full set on the PR.",
"Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and a different PR footer; AGENTS.md (the user's instruction, enforced by the pre-push hook) requires the model-free trailer pair and the session-URL footer, so those were used.",
"PR body line 3 states what widens (the dispatch asked for a line saying what widens after the Clause-② line)."
],
"files_changed": [
".changeset/22877-member-selector.md",
"content/docs/references/api/protocol.mdx",
"packages/metadata-protocol/src/protocol.bundle-member-selector.test.ts",
"packages/metadata-protocol/src/protocol.lookup-artifact-item-call-sites.test.ts",
"packages/metadata-protocol/src/protocol.ts",
"packages/metadata-protocol/src/sys-metadata-repository.ts",
"packages/objectql/src/environment-overlay-email-locale-restart.test.ts",
"packages/objectql/src/registry-i18n-bundle-key.test.ts",
"packages/objectql/src/registry.ts",
"packages/qa/dogfood/test/email-template-two-locales-survive-boot.dogfood.test.ts",
"packages/rest/src/rest-server-meta-member-selector.test.ts",
"packages/rest/src/rest-server.ts",
"packages/spec/authorable-surface/api.json",
"packages/spec/src/api/protocol.zod.ts"
]
}
objectstack-fleet commentedon Oct 12, 2026
Seat note: PR #23070's contract review is FAIL on
2b882e6774, and patch round 1 is asked ·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T16:13ZThis concerns report 6127796890 (stage 2). The record is 6128093700 on the PR. The seat re-read its blocking finding.
Blocking, in ②.
@objectstack/objectqlis gradedpatch. But the exportedSchemaRegistrygains an optionalmemberargument on public methods, and the WHICH LEVEL rule (batch #35 on #15294) grades an additive widening at leastminor. The PR body'sWidens:line also omits three additive items:MetadataMutationEvent.member;listDrafts'memberheader;The dev is asked to fix the changeset level and the PR body. No code change is asked. A delta contract review then runs on the new head.
Holding at this head, per the record's ①:
member(the discriminator field islocale, the forbidden one);removeRuntimeShadowat name level;Follow-ups, filed at this PR's ACCEPT (the out-of-scope findings, two families):
memberselector's transport parity:/metaitem GET neither declares a closed set nor forwards?member=;PUT /meta/:type/:namedeclares no closed set, so?member=there is dropped silently;/diff,/layers,/audit,/published,/references) have no closed sets either.chore(changeset): objectql is minor — SchemaRegistry's public methods…
objectstack-fleet commentedon Oct 12, 2026
os-dev-report
{
"issue": 22877,
"status": "done",
"round": "patch round 1",
"branch": "claude/issue-22877-member-selector",
"pr": "#23070",
"new_head": "2c055c088f",
"session": "session_01ADzJtzYTLUfgrRZHxkagkX (subagent of the domain:engine seat 2 PM; the parent's id)",
"premise_still_valid": true,
"summary": "Patch round 1 on PR #23070 answers contract review 6128093700 (FAIL on 2b882e6). The new head is 2c055c0: one fast-forward commit on 2b882e6, with no rebase, amend, force-push or main merge. The PR is not conflicting: GitHub answered mergeable true, state blocked, on this head's first read, and unknown on a later read while it recomputed. (1) The blocking ② finding is fixed in .changeset/22877-member-selector.md.
@objectstack/objectqlgoes from patch to minor. The changeset gains a section, 'For code that uses these packages directly', which says what a consumer can now pass or read: SchemaRegistry's optional last argumentmemberon getItem, getArtifactItem, removeRuntimeShadow and removeOverlayEntry, including what each answers and getItem's bare-name canonical answer; the optional MetadataMutationEvent.member and the ClusterMetadataMutationPayload that carries it; and the optionalmemberon SysMetadataRepository.listDrafts entries, which GET /meta/_drafts does not serve. (2) The PR body'sWidens:line was NOT patched by this dev. .claude/agents/os-dev.md says the dev writes the PR body once, at pr_create, never PATCHes it, and names later edits in its report for the seat to write. That conflicts with the round's instruction ('the PR body update through the relay'), and the role file wins. The replacement body is ready, see deviations. (3) CI: no shard has gone red on either head. On 2b882e6, Test Core 1/6 and 5/6 concluded success; 2/6, 3/6, 4/6 and 6/6 were cancelled by this round's push, none red. On 2c055c0, at this report: Test Core 1/6, 5/6 and 6/6 success; 2/6, 3/6 and 4/6 in_progress; every other check success or skipped (Lint & Repo Gates, TypeScript Type Check, Build Core, Check Changeset, Governed Surface Queue Guard included). Code is unchanged, so no test was re-run.","fix_chosen": {
"changeset_level": "@objectstack/objectql: patch -> minor (an additive widening of SchemaRegistry, which the package index exports; the WHICH LEVEL rule grades it at least minor)",
"changeset_text": "a section naming each additive public surface and what a consumer can pass or read: SchemaRegistry's member argument (four methods), MetadataMutationEvent.member, and the SysMetadataRepository.listDrafts member header",
"pr_body": "prepared, not written (role-file rule); see deviations"
},
"tests": "No code changed this round, so no suite or ablation was re-run; round 0's records stand (os-dev-report 6127796890). Gate union re-derived on the new head:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 2c055c0 gives 118 commands, the same set as round 0; all 118 exit 0. Reconciled with --ran (exit codes recorded): '118 derived, 118 run, 0 NOT-MEASURED, 0 UNRUN'. The changeset gates are included: check-changeset-no-major --base origin/main and --self-test, check-empty-changeset, check-adr-0087-registration, check:pm-changeset-deadline-census, check:changeset-gate-self-tests. The worktree was recreated, with pnpm install --frozen-lockfile and a full packages build under the lock (turbo, 72 tasks, exit 0, cache-replayed), so the dist-reading gates read built output. CI 'Check Changeset' is success on 2c055c0.","ci": {
"head_2c055c088f": "Test Core (1/6), (5/6) and (6/6) success; (2/6), (3/6) and (4/6) in_progress at this report; every other check success, or skipped (Console Pin Gate, Temporal Conformance, Dogfood Verify CLI, Packed-tarball smoke, the Dogfood Regression Gate matrix shard)",
"head_2b882e6774": "Test Core (1/6) and (5/6) success; (2/6), (3/6), (4/6) and (6/6) cancelled by this round's push; nothing red"
},
"mcp_calls": "0 — no MCP GitHub tool called",
"api_writes": "1 REST write: POST /repos//issues/22877/comments (this os-dev-report, scripts/pm/post-stamped.mjs, through the relay). Not REST: git push of one fast-forward commit (2b882e6..2c055c0). No PR-body PATCH (see deviations), no label writes. REST reads with the session's gh: comment 6128093700, check-runs on both heads, pulls/23070.",
"open_questions": [],
"out_of_scope_findings": [
"The review's escalations are the seat's to file, as the round said, and none was built here: the runtime dispatcher's
?member=, the remaining by-name doors' closed sets (PUT included), the drafts and commits lists with Studio and the client SDK, and the envelope graft that makes isCodeArtifactBody misread an unshipped member's overlay."],
"gates": [
"node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
"node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
"node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
"node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
"node scripts/check-ci-filter-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs :: exit 0",
"node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-adoption.mjs :: exit 0",
"node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
"node scripts/check-comment-mask-corpus.mjs :: exit 0",
"node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
"node scripts/check-doc-frontmatter.mjs :: exit 0",
"node scripts/check-doc-frontmatter.mjs --self-test :: exit 0",
"node scripts/check-doc-route-spelling.mjs --advisory :: exit 0",
"node scripts/check-doc-route-spelling.mjs --self-test :: exit 0",
"node scripts/check-docs-section-name.mjs :: exit 0",
"node scripts/check-docs-section-name.mjs --self-test :: exit 0",
"node scripts/check-dts-emitted.mjs --self-test :: exit 0",
"node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
"node scripts/check-empty-changeset.mjs --self-test :: exit 0",
"node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
"node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
"node scripts/check-issue-citations.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs :: exit 0",
"node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
"node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs :: exit 0",
"node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
"node scripts/check-registry-log-declared.mjs :: exit 0",
"node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-declared.mjs :: exit 0",
"node scripts/check-rest-log-declared.mjs --self-test :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs :: exit 0",
"node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
"node scripts/check-section-landing-index.mjs :: exit 0",
"node scripts/check-section-landing-index.mjs --self-test :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs :: exit 0",
"node scripts/check-spec-docblock-symbol-anchors.mjs --self-test :: exit 0",
"node scripts/check-system-context-census.mjs :: exit 0",
"node scripts/check-system-context-census.mjs --self-test :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs :: exit 0",
"node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
"node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
"node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
"node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
"node scripts/release-pending-publish.mjs --self-test :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0",
"pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0",
"pnpm --filter @objectstack/spec run check:api-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0",
"pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0",
"pnpm --filter @objectstack/spec run check:docs :: exit 0",
"pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0",
"pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
"pnpm --filter @objectstack/spec run check:empty-state :: exit 0",
"pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0",
"pnpm --filter @objectstack/spec run check:export-origins :: exit 0",
"pnpm --filter @objectstack/spec run check:exported-any :: exit 0",
"pnpm --filter @objectstack/spec run check:generated :: exit 0",
"pnpm --filter @objectstack/spec run check:liveness :: exit 0",
"pnpm --filter @objectstack/spec run check:llms-txt :: exit 0",
"pnpm --filter @objectstack/spec run check:objectui-pin-citations :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-examples :: exit 0",
"pnpm --filter @objectstack/spec run check:skill-refs :: exit 0",
"pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0",
"pnpm --filter @objectstack/spec run check:variant-docs :: exit 0",
"pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0",
"pnpm check:authz-resolver :: exit 0",
"pnpm check:changeset-gate-self-tests :: exit 0",
"pnpm check:corpus-claim-drift :: exit 0",
"pnpm check:cross-package-test-inputs :: exit 0",
"pnpm check:dispatcher-error-vocabulary :: exit 0",
"pnpm check:doc-anchors :: exit 0",
"pnpm check:doc-authoring :: exit 0",
"pnpm check:docs-audit-scope :: exit 0",
"pnpm check:docs-redirects :: exit 0",
"pnpm check:docs-single-h1 :: exit 0",
"pnpm check:docs-spec-enumerations :: exit 0",
"pnpm check:docs-transcript-drift :: exit 0",
"pnpm check:driver-memory-census :: exit 0",
"pnpm check:dts-closure :: exit 0",
"pnpm check:dual-build-cjs-loads :: exit 0",
"pnpm check:durability-log-level :: exit 0",
"pnpm check:engine-double-contract :: exit 0",
"pnpm check:error-status-conformance :: exit 0",
"pnpm check:filter-alias-parity :: exit 0",
"pnpm check:gitlink-declared :: exit 0",
"pnpm check:issue-citations :: exit 0",
"pnpm check:lean-entry-closure :: exit 0",
"pnpm check:logger-receiver-detach :: exit 0",
"pnpm check:merge-driver :: exit 0",
"pnpm check:nul-bytes :: exit 0",
"pnpm check:objectql-double-limit :: exit 0",
"pnpm check:objectui-changeset :: exit 0",
"pnpm check:org-identifier :: exit 0",
"pnpm check:page-declaration-shape :: exit 0",
"pnpm check:pm-changeset-deadline-census :: exit 0",
"pnpm check:pm-prior-rulings :: exit 0",
"pnpm check:published-files :: exit 0",
"pnpm check:published-readme-links :: exit 0",
"pnpm check:query-options-erasure :: exit 0",
"pnpm check:quick-reference-counts :: exit 0",
"pnpm check:react-page-adapter-contract :: exit 0",
"pnpm check:refd-timer-probe :: exit 0",
"pnpm check:role-word :: exit 0",
"pnpm check:route-envelope :: exit 0",
"pnpm check:skill-identifier-liveness :: exit 0",
"pnpm check:slot-lookup :: exit 0",
"pnpm check:sourcemap-no-sources-content :: exit 0",
"pnpm check:spec-parsed-alias :: exit 0",
"pnpm check:test-source-alias :: exit 0",
"pnpm check:tier-file-adoption :: exit 0",
"pnpm check:type-check-coverage :: exit 0",
"pnpm check:type-check-debt :: exit 0",
"pnpm check:vendor-version-stamps :: exit 0",
"pnpm check:watch-hint-literal :: exit 0",
"pnpm check:where-matcher :: exit 0"
],
"line_budget": "This round: 1 file, +11/-1 against 2b882e6 (.changeset/22877-member-selector.md). The PR on 2c055c0 against its merge-base: 14 files, +1385/-105 = 1490 changed lines, still under the 3,000-line Tier H threshold.",
"deviations": [
"PR body not edited by the dev. .claude/agents/os-dev.md: 'PR 正文 dev 只写一次,在开 PR 那一笔,⛔ 不 PATCH;事后要改的报告点名改法,席位代写。' The round asked for 'the PR body update through the relay', and the role file wins on a conflict. For the seat to write, two edits, prepared in full at the scratch path below. (a) Replace line 3 with: '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.' Lines 1 and 2 are unchanged. (b) Optional, from the review's ① 3: under 'The value refusal's envelope', add the bullet 'The?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.' The full body with both edits, built from the stored body (contains no less-than character), is at /tmp/claude-0/-home-user-objectstack/99494cdf-a0fd-56b5-adae-98900c5dd071/scratchpad/issue-22877b/s2/r1/pr-body-r1.md. Its evidence section still describes head 2b882e6, where every code-bearing measurement was taken.","The worktree removed after round 0 was recreated at ../objectstack-issue-22877-s2 on the existing local branch, which matched origin at 2b882e6."
],
"files_changed": [
".changeset/22877-member-selector.md"
]
}
objectstack-fleet commentedon Oct 12, 2026
✅ ACCEPT: PR #23070 at
2c055c088f(stage 2 of 2) ·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T16:55ZBasis:
Everything below was checked on GitHub, not taken from the reports. Classes, positions and functions only.
main, assigneeos-steve;Fixes #22877, the only closing keyword;Clause-②: yes;Widens:names every additive item. The seat wrote that line in this round, as the role file assigns it: the dev writes the PR body once, and the seat writes later edits.check-governed-merges: clean);content/docs/releases/;api.jsonandprotocol.mdxare in;packages/restandpackages/specare cross-lane, declared on [PM seat] domain:cli · seat 2 — ⏳ vacant #22648 and [PM seat] domain:spec — ⏳ vacant #6017.member(the discriminator fieldlocaleis the name the ruling forbids)./history,/publish,/rollback) read it through one protocol reading,resolveBundleMemberSelector. Absent means the canonical member.member. No rollback request schema exists, so none is widened.removeRuntimeShadow,removeOverlayEntryand the heal address one member;2c055c088f: 41 check runs, none failed.2b882e6774(②:@objectstack/objectqlwas gradedpatchfor an additive widening of the exportedSchemaRegistry, and theWidens:line was incomplete);2c055c088f. That head is changeset-only, its code hunks are byte-identical,objectqlisminor, and every prior judgment in ① holds.memberis dropped silently by the runtime dispatcher GET, by PUT, and by the by-name doors outside the ruling (no closed query sets); honour it or refuse it loudly #23072: the selector is dropped silently by the runtime dispatcher's GET, byPUT, and by the by-name doors outside the ruling (no closed sets);isCodeArtifactBodyreads it as packaged code. This PR routes around it in one reader.MetadataServicekeys a type by name alone, so a bundle holds the last member registered. Pre-existing; it is now read only when it is the addressed member;bundle_memberkeys stay owed toarea:i18n, as stage 1 recorded.Landing: prechecks pass:
origin/main;The PR is readied and armed through the relay in this act. When it merges,
Fixes #22877closes the card.objectstack-fleet commentedon Oct 12, 2026
Landed: stage 2, PR #23070 →
038d4c9588·domain:engineseat 2 ·session_01ADzJtzYTLUfgrRZHxkagkX· 2026-10-12T17:25Z038d4c958837fb42e71842c18f062f37bd866811, with one parent,275ad4229a. The ACCEPT is on this card. Contract review: FAIL 6128093700 on2b882e6774(the changeset level), then delta PASS 6128724141 on2c055c088f.mainat that commit: the squash carries the accepted change exactly. All 14 files have hunks identical to the accepted head.mainnow holds: ruling 6116165299 (A, Q2 i) is complete. Two locales of oneemail_templateare two stored rows (stage 1,5677add93f). Each one is read, reset, published, viewed in history and reverted by name with?member=on the five by-name/metadoors. With no selector, a door addresses the canonical member, and so does the registry's bare read. The window stage 1's changeset stated is closed.completedby the PR'sFixes #22877.pm:dispatchedcomes off in this act.memberis dropped silently by the runtime dispatcher GET, by PUT, and by the by-name doors outside the ruling (no closed query sets); honour it or refuse it loudly #23072 (the selector is dropped silently at other doors);bundle_memberkeys, owed toarea:i18n.