Skip to content

feat(spec, metadata-protocol): the draft list and the commit timeline tell two languages of one email template apart — each entry carries member (#23073) - #23099

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23073-list-member
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23073-list-member

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23073
Clause-②: yes (widening: a new declared response key)

Completes ruling 6116165299 (letter A) on #22877 for the two list doors: each bundle member is addressable by name, and now each list entry says which member it is. Stage 2 (PR #23070, 038d4c958) gave the by-name /meta doors the member selector; the lists still answered two languages of one email_template as two entries with the same type and name, so a reader could not tell them apart or address the second one.

What changed

Contract (@objectstack/spec, domain:spec)

  • ListDraftsResponseSchema (GET /meta/_drafts): each draft entry gains member: z.string().optional().
  • ListPackageCommitsResponseSchema (GET /packages/:id/commits): each commit item gains member: z.string().optional().
  • Both describes say the same thing: the bundle member the entry is, for a type whose identity is a pair (today only email_template, (name, locale)); its discriminator value (zh-CN), with the canonical member spelled out (en-US); present on every entry of such a type; absent for a type whose identity is its name alone; it is the by-name doors' member selector (?member=) for that entry.
  • Regenerated: content/docs/references/api/protocol.mdx and content/docs/references/api/package-lifecycle.mdx (check:generated proved only check:docs stale; --fix regenerated that one and re-checked it green). No authorable-surface, api-surface or json-schema artifact moved: these are response shapes, and no export was added.

Serving (@objectstack/metadata-protocol, domain:engine, cross-domain exception per triage 6129686435)

  • listDrafts: serves the repository header's stored member (SysMetadataRepository.listDrafts, the bundle_member column through storedRowMemberHeader), which it used to strip.
  • listCommits: serves the stored revert-plan item's member (CommitPlanItem, the member revertCommit acts on), which it used to strip.
  • Both go through bundleMemberDiscriminator(type, storedMember), the stage-2 function every by-name door already uses to turn a selector into a discriminator value. It is keyed on ITEM_KEY_DISCRIMINATORS, the one declaration of which types are discriminated. There is no second list of types, and nothing is read off the body.
  • Stored, the canonical member is NULL (the column) or an absent key (the plan). This is the ruling's "NULL means the canonical member". The list spells it out as en-US rather than omitting it. The reasons:
    • Key presence then answers "is this type discriminated" for every entry: triage's direction is "carry member for a discriminated type, and nothing for a name-only type".
    • Two languages read as two distinct values, not one value beside an absence.
    • ?member=en-US is the same request as no selector (stage 2's resolveBundleMemberSelector), so the spelled-out value round-trips through every by-name door.
  • Only these two serving regions of protocol.ts changed, plus one comment-only docblock (CommitPlanItem, near L2093) whose sentence "the commit timeline serves the declared four keys only" this PR makes false. The three open PRs editing protocol.ts (fix(metadata-protocol): a migrate-stored pass records the user who ran it, or nobody — never a label #23085 landed since; feat(metadata-protocol): an unmanaged install copies no protection lock, and names each one it drops #23075, feat(plugin-security)!: no seeder or materializer writes a sys_permission_set row; the boot seeds no row for a declared set (#15204 stage 6b-2) #22900) are hunk-disjoint from these.

Premises measured on origin/main (a22e67b3c)

  • The repository draft header carries member when it is non-canonical (storedRowMemberHeader, sys-metadata-repository.ts), and the protocol stripped it: drafts.map(({ member: _member, ...header }) => header). Holds.
  • The stored plan item carries member when it is non-canonical (CommitPlanItem), and listCommits stripped it the same way. Holds.
  • Re-declarations of either list shape, by grep over packages/**/src (excluding dist):
    • @objectstack/client: meta.listDrafts and packages.listCommits return ListDraftsResponse / ListPackageCommitsResponse imported from @objectstack/spec/api. This is derived, not a re-declaration, so it widens with the schema. Its return-type-precision.test.ts pins toEqualTypeOf against those same imported types.
    • @objectstack/rest: GET /meta/_drafts answers res.json(result), a pass-through. The route ledger names the schema by string.
    • @objectstack/runtime: the /metadata/_drafts and /packages/:id/commits dispatcher arms pass the protocol answer through (success({ commits })). The route ledger names the schemas by string.
    • @objectstack/cli: zero reads of either list.
    • Result: no re-declaration exists, so none was updated.
  • The one discriminated-type predicate: stage 2's resolveBundleMemberSelector and bundleMemberDiscriminator both read ITEM_KEY_DISCRIMINATORS (@objectstack/metadata-core). This PR reuses bundleMemberDiscriminator.

Pins

  • packages/objectql/src/environment-overlay-email-locale-restart.test.ts (stage 1/2's real-engine harness, real ObjectQL and protocol):
    • New: en-US and zh-CN drafts of one template, bound to a package, read from listDrafts as two entries [email_template, acme.welcome, en-US] and […, zh-CN]. Each listed member is handed back to the by-name door (getMetaItem({ state: 'draft', member })) and reads its own draft's wording. After publishPackageDrafts, listCommits reads the commit's two items with the same two members, and each item's member reads its own published wording through getMetaItem.
    • Inverted: stage 2's pin that a commit item carries no member key now asserts zh-CN on both the publish commit and its revert. The stage-2 comment "the public draft list keeps its declared header (no member)" is removed, and its assertion (drafts empty after a discard) is kept.
  • packages/metadata-protocol/src/sys-metadata-repository-14938-list-drafts-updated-at.test.ts §D: two email_template draft rows (bundle_member NULL and zh-CN) whose bodies BOTH say en-US, so the served member is shown to come from the stored column, not the body. They read as en-US / zh-CN. An object draft's keys are exactly the six declared ones, with no member key. The response parses as ListDraftsResponseSchema with every member preserved.
  • packages/metadata-protocol/src/protocol-14038-list-commits-created-at-iso.test.ts: a stored plan of an email_template item without member, one with member: 'zh-CN', and an object item serves en-US, zh-CN, and no key. It parses as ListPackageCommitsResponseSchema with every member preserved.
  • packages/spec/src/api/protocol.test.ts and package-lifecycle.test.ts: two members of one template plus a name-only entry parse, the parse keeps member, and the name-only entry has no member property.

Reverse verification (ablation)

One-off, from the committed fix at fe5c3a5e3, through scripts/ablation-replace.mjs. In each leg the anchor hit exactly once and the blob changed. Each restore was proven by blob == HEAD and an empty git diff HEAD, and the final tree diff was 0 lines. Predicted direction for each leg: red, exactly the new case.

leg mutation result
A1 listDrafts serves header (no member) sys-metadata-repository-14938…: 1 failed | 18 passed (19): the #23073 case
A2 listCommits serves item (no member) protocol-14038…: 1 failed | 4 passed (5): the #23073 case
B1 ListDraftsResponseSchema key renamed away from member spec protocol.test.ts: 1 failed | 184 passed (185): the #23073 case
B2 commit-item key renamed away from member spec package-lifecycle.test.ts: 1 failed | 19 passed (20): the #23073 case

The objectql harness leg was not ablated. It reads @objectstack/metadata-protocol from dist (KNOWN_UNALIASED_TEST_IMPORTS), so an ablation there needs a rebuild per leg. The serving it exercises is the code that A1 and A2 ablate at source level.

Tests and gates (each reading names the tree it was taken on)

Targeted pins and the ablation ran at fe5c3a5e3. The merge that followed, to f735cffa6, brought only packages/rest, packages/runtime and two changesets, none of which these packages depend on.

  • Targeted: spec protocol.test.ts and package-lifecycle.test.ts: Test Files 2 passed (2), Tests 205 passed (205). metadata-protocol's two edited files: 2 passed, 24 passed. The objectql harness: 1 passed, 20 passed.
  • @objectstack/spec: typecheck (tsc, scripts and the test program) exit 0, at fe5c3a5e3. check:generated proved only check:docs stale. --fix regenerated it and re-checked it green, and the other 13 artifacts were green.
  • @objectstack/metadata-protocol: typecheck exit 0. Its tsc program includes both edited test files (--listFiles: 2 of 2). The full test ran at fe5c3a5e3: Test Files 228 passed | 3 skipped (231), Tests 28207 passed | 19 skipped (28226).
  • @objectstack/objectql: typecheck exit 0, check:test-typecheck ledger held, at fe5c3a5e3. The full test ran at f735cffa6: Test Files 408 passed (408), Tests 7975 passed (7975).
  • @objectstack/client (consumer: its two list methods return the spec types), at f735cffa6:
    • tsc --noEmit (build program): exit 0.
    • return-type-precision.test.ts, unwrap-misfire.pin.test.ts and meta-automation-descriptors.test.ts: 3 passed, 27 passed.
    • The test program (tsconfig.test.json) is a narrowed reading. 0 errors fall in those three files. All 35 errors elsewhere are TS2307 cannot-find-module for unbuilt devDependency dist (@objectstack/runtime, objectql, plugin-hono-server, …). So the whole-program verdict is NOT MEASURED here and is left to CI.
  • @objectstack/rest, @objectstack/runtime: pass-throughs with no re-declaration (see the premises section). No test owed: the public byte surface they touch is the spec type, already read above.
  • @objectstack/cli: zero reads of either list, so no unit run is owed.
  • Lint, targeted: eslint --no-inline-config --format json over the 9 changed .ts files: files_linted=9 errors=0 warnings=0.
    • All 9 files fall in the config's own **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} object; the json lists all 9, so none is ignored.
    • The narrowing excludes nothing untouched: the config enables no type-aware linting (no parserOptions.project, stated in eslint.config.mjs), and its plugins are per-file rules. A diff cannot move an untouched file's verdict.
  • Derived gate families, at f735cffa6: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 114, and each ran with its exit code recorded before any pipe. --ran printed: 114 derived famil(ies) accounted for — 113 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
    • Three first runs refused their prerequisite, and each was re-run at the same head after its prerequisite was met. All three re-runs exited 0:
      • check-engine-split-ratio: shallow clone. Re-run after git fetch --shallow-since=2026-07-07 origin main.
      • check:skill-examples: needs the client and client-react dist. Re-run after building them.
      • check:lean-entry-closure: needs the objectql dist. Re-run after building it.
    • NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET. It needs every package's dist, with 59+ unbuilt here, and a whole-monorepo build is CI's.

Acceptance notes

  • Same family, not fixed here (read, not measured at a door). The package-batch result arrays name two members of one template identically too: publishPackageDrafts' published[] / failed[], discardPackageDrafts' discarded[] / failed[], and revertCommit's reverted[] / failed[] carry { type, name } only. That is the same shape this card closes for the two lists. Carrier: none.
  • A pending changeset sentence this PR makes false. .changeset/22877-member-selector.md (stage 2, unreleased) says "The protocol's draft list (GET /meta/_drafts) does not include it." That file is outside this claim's file surface, so it is not edited here. This PR's changeset says the earlier sentence is superseded. The one-line amendment belongs to the seat: delete that sentence, or replace it with "The protocol's draft list serves it too."
  • Comment-only edits outside the two serving regions. Two docblocks whose sentences this PR made false: CommitPlanItem in protocol.ts and the member header doc of SysMetadataRepository.listDrafts.
  • The objectql harness is the serving's real-engine test, and it held stage 2's opposite pin. It is edited as the serving's test.
  • Studio's half (DraftChangesPanel addressing each entry with ?member=) is Studio's draft panel and commit timeline address a bundle member: read member off _drafts / package commits and pass ?member= (the objectui half of objectstack#23073) objectui#12271.

Generated by Claude Code

… carry each bundle entry's member (#23073)

ListDraftsResponseSchema's draft entry and ListPackageCommitsResponseSchema's
commit item gain an optional member: present on every entry of a type whose
identity is a pair (email_template), the canonical member spelled out, and
absent for a name-only type. listDrafts and listCommits serve it from the
repository header's stored member and the stored revert plan item.

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

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/spec, touching 7 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via listDrafts (symbol, a method of class ObjectStackProtocolImplementation; a method of class SysMetadataRepository))
  • content/docs/api/metadata-api.mdx (via /packages/:id/commits (route, the route ledger binds it to client method packages.listCommits))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/ui/apps.mdx _(via /api/v1/meta/drafts (route, the route ledger binds it to client method meta.listDrafts))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 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 — 141 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 55828c5145ce1ca8cb597baf3396614adf9c0583 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 333e75620cfac37a8de0f7de0c12c1c6b87eb92f — the merge of head 2f2c6feea3975d5d90e448c2427ba5c19f8117db into base 55828c5145ce1ca8cb597baf3396614adf9c0583, 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 333e75620cfac37a8de0f7de0c12c1c6b87eb92f && git checkout 333e75620cfac37a8de0f7de0c12c1c6b87eb92f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 55828c5145ce1ca8cb597baf3396614adf9c0583 2f2c6feea3975d5d90e448c2427ba5c19f8117db && git checkout -B drift-repro 55828c5145ce1ca8cb597baf3396614adf9c0583 && git merge --no-ff 2f2c6feea3975d5d90e448c2427ba5c19f8117db

node scripts/docs-audit/affected-docs.mjs --json 55828c5145ce1ca8cb597baf3396614adf9c0583

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

… two lists serve member (#23073)

The bundle member selector entry (unreleased) said the protocol's draft
list does not include the member; #23073 makes the draft list and the
commit timeline serve it. One sentence rewritten; this card's own
changeset drops its now-redundant supersession note.

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

Copy link
Copy Markdown
Contributor Author

Check Changeset is red here by design: DELIBERATE CORRECTION · domain:spec seat 2 · session session_01GZSPSQ5ArPjSJdxxuqwGoZ · 2026-10-12T21:40Z · head fe50ab688

  • Gate: Check Changeset (.github/workflows/pr-automation.yml), at the step node scripts/check-empty-changeset.mjs --base "$MERGE_BASE". It runs on pull_request only, never on merge_group, and it is not a required context.
  • Cause: this PR changes .changeset/22877-member-selector.md, which another PR added. That note has not been released yet. This is not a filename collision: this card's own note is .changeset/23073-list-member.md, a new file.
  • What changed under the note: its line 49 said "The protocol's draft list (GET /meta/_drafts) does not include it." This PR makes listDrafts and listCommits serve member, so that sentence would ship false. Exactly that one sentence is rewritten: "The protocol's draft list (GET /meta/_drafts) and a package's commit timeline serve it too, on every entry of a template, with the canonical en-US spelled out." No other line of that file moves.
  • Not done, on purpose: the old sentence is not restored from the base, and no skip-changeset label is applied (ruling D on [finding] the skip-changeset label suppresses the DELIBERATE-CORRECTION refusal whose own text says 「no label and no diff shape makes that safe」 — declared contract against enforced behaviour #18375).
  • Confirmation: a CONTRACT_REVIEW_TIER record on this same head will name the edited note and judge the rewritten sentence. A PASS there is the written confirmation, and the red stays.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

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

Inputs: card #23073 (body and its 5 comments: triage 6129686435, claim 6131409879, dev report 6132899471, seat ACCEPT 6132937336, patch round 6133092518), the ruling they cite on #22877 (6116165299) and the stage-2 records 6128093700 / 6128724141; PR #23099 (body, its 2 comments, the 13-file list, the net diff 6662e3f59..fe50ab688 against main, +215/-20); the check-runs on fe50ab688, polled in this act at 2026-10-12T21:51Z. Nothing was built, run or re-run; every file was read at the head with git show.

Check-runs on the head: 40. 30 success; 5 skipped (Temporal Conformance, Dogfood Verify CLI, the Dogfood matrix shard, Console Pin Gate, Packed-tarball smoke); 1 failure (Check Changeset); 4 in_progress at the poll (Test Core 2/6, 3/6, 5/6, 6/6). Every lint, type-check, build, docs, size, claim, single-writer and governed-queue gate is green. Check Changeset is red at step 12 alone ("Reject an empty-frontmatter changeset added by this PR", the check-empty-changeset.mjs --base step), with one annotation naming .changeset/22877-member-selector.md: the DELIBERATE CORRECTION class the script itself names (FOREIGN_CORRECTION_REMEDY: do NOT restore it; say so on the PR and get it confirmed). Steps 13 (ADR-0087 disposition) and 15 (no-major guard) of the same job ran after it and are green, so the red withholds no other verdict. pr-automation.yml triggers on pull_request only, never merge_group, and main's branch rules (GET /rules/branches/main) require Build Core, Governed Surface Queue Guard, Lint & Repo Gates, Test Core and TypeScript Type Check, not Check Changeset. The spec artifact gates (check:authorable-surface, check:docs) are steps of Type Check · source gates, green. The running shards do not carry this record's verdict; a red one re-judges the head.

① Derived judgments

  1. ListDraftsResponseSchema.drafts[].member — right. z.string().optional(), no strictness change, and the describe matches the serving (3 below): the discriminator value, the canonical member spelled out, present on every entry of a discriminated type, absent for a name-only type, the by-name doors' selector. Pinned in packages/spec/src/api/protocol.test.ts (the parse keeps en-US / zh-CN; the name-only entry has no member property).
  2. ListPackageCommitsResponseSchema.commits[].items[].member — right. Same shape and describe, naming the right source: the stored revert plan's member, the one revertCommit acts on (parseCommitItems feeds both doors). Pinned in package-lifecycle.test.ts.
  3. listDrafts serving — right. The repository header carries member only for a non-canonical member: storedRowMemberHeader spreads overlayMemberValue, which answers null for a NULL, blank or canonical column and undefined for a type with no discriminator, so {} in both cases. The protocol folds it through bundleMemberDiscriminator(header.type, stored): for email_template the stored value, or disc.canonical (en-US) when absent; for every other type undefined, and the door then returns header itself, so a name-only entry has no member key at all, not an undefined-valued one. Pinned in sys-metadata-repository-14938-list-drafts-updated-at.test.ts (both bodies say en-US; the served member follows the column; the object draft's keys are exactly the six declared).
  4. listCommits serving — right. The same fold over the plan item's member (absent for the canonical member and for name-only types, per CommitPlanItem); item is returned untouched when the fold answers undefined. Pinned in protocol-14038-list-commits-created-at-iso.test.ts (en-US, zh-CN, and no key on the object item).
  5. The served value is exactly what ?member= takes back — right, by code, both ways. Served zh-CN: resolveBundleMemberSelector returns the trimmed string, memberOption passes it, and the repository where sets bundle_member = 'zh-CN'. Served en-US: the selector returns en-US, bundleMemberDiscriminator answers the canonical member (the same wantedMember as no selector at all), and the repository maps the canonical value to null, so bundle_member IS NULL (overlayMemberValue; packageScopedRowWhere: "string → eq; null → IS NULL"), the very row the list spelled out as en-US. The REST GET's closed set carries state and member together (stage 2), so the changeset's ?state=draft&member=zh-CN example is a legal request. Pinned end to end in the objectql harness: each listed member read back through getMetaItem answers its own wording, for the two drafts and for the commit's two items.
  6. One discriminated-type predicate — right. Both doors reuse stage 2's bundleMemberDiscriminator, keyed on ITEM_KEY_DISCRIMINATORS (email_template only); no second type list, and nothing is read off the body.
  7. Public surface of @objectstack/metadata-protocol — right; minor covers it. ObjectStackProtocolImplementation.listDrafts's return literal and listCommits's items literal each gain member?: string; SysMetadataRepository.listDrafts changes a docblock only, no shape.
  8. Public surface of @objectstack/spec — right. ListDraftsResponse and ListPackageCommitsResponse widen by one optional key; no export is added, so api-surface/api.json (export names) rightly does not move. The regenerated protocol.mdx (one nested row) and package-lifecycle.mdx (the items cell; the generator renders one nesting level) are in sync by the green check:docs step.
  9. @objectstack/client — described truthfully. meta.listDrafts returns Promise of ListDraftsResponse and packages.listCommits Promise of ListPackageCommitsResponse, both imported from @objectstack/spec/api and pinned toEqualTypeOf in return-type-precision.test.ts. The client ships no changed byte, so the changeset rightly carries no client line, and its sentence (the types the client returns gain the field) is exact. The client sits in the same fixed release group as spec, so a consumer's @objectstack/client bumps with it in any case.
  10. REST and runtime doors — right, pass-throughs. GET /meta/_drafts answers res.json(result); the dispatcher answers success(data) and success({ commits }); no re-projection and no response-schema parse strips the key. The CLI reads neither list.
  11. Comment-only docblocks (CommitPlanItem; the repository's member header) — right. Both sentences this head makes false are rewritten; they lie outside the two serving regions and the dev flagged them.
  12. The inverted stage-2 pin in the objectql harness — right. The old assertion (a commit item has no member key) was the opposite contract; keeping it would have failed the serving this card orders. The drafts-empty-after-discard assertion is kept.
  13. Hand-written docs — nothing owed. No page under content/docs outside the generated reference states that either list lacks member; the Docs Drift rows name the two symbols for their authorization gates. The sibling objectui at the pinned .objectui-sha cannot break on an added optional key (Console Pin Gate skipped by filter, rightly); Studio's half is objectui#12271.

② Semver level

  • .changeset/23073-list-member.md: @objectstack/spec: minor, @objectstack/metadata-protocol: minor; the body carries Clause-②: yes (widening: a new declared response key), matching the PR body and the claim. Right: both packages publish a widened shape (① 1, 2, 7); objectql changes a test file only, and rest, runtime and client change no byte, so no further line is owed. Nothing is removed or renamed, so no ADR-0087 marker is owed (Check Changeset step 13 green: no declared-breaking changeset); no major (step 15 green). No skip-changeset: the diff publishes. Each sentence of the body was read against the code: the two doors, the en-US spelling, the ?state=draft&member=zh-CN example, "the language the stored revert plan records", the client sentence (① 9). All true.
  • Clause-②: line, yes (widening) — right. Two response schemas gain a declared key; no door's accept set changes.
  • The edited pending note: .changeset/22877-member-selector.md (stage 2, unreleased, added by PR 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). The diff rewrites exactly one line, 49, and one sentence of it; every other line is byte-identical to the base.
    • Kept sentence (line 49, first): "The entries that SysMetadataRepository.listDrafts returns have an optional member, present for a draft of a non-en-US language." Still true of this head: storedRowMemberHeader spreads {} for the canonical member (① 3).
    • Before: "The protocol's draft list (GET /meta/_drafts) does not include it." False of this head (① 3), so it could not stay.
    • After: "The protocol's draft list (GET /meta/_drafts) and a package's commit timeline serve it too, on every entry of a template, with the canonical en-US spelled out." True of this head, clause by clause: both doors serve member (① 3, 4); "on every entry of a template": bundleMemberDiscriminator answers a value for every email_template entry; "with the canonical en-US spelled out": disc.canonical is served where the column is NULL or the plan key absent. One precision for the record, not a defect: for the commit timeline, "it" is the plan item's stored member rather than the repository header's; the sentence claims what is served, not its source, and the discriminator value is the same.
    • Everything else in the note, read against this head: line 38, "The draft list and the commit timeline keep the fields they declare.", stays true (they serve exactly what they now declare); the frontmatter, the five routes' closed sets, the selector's refusals and the SchemaRegistry / MetadataMutationEvent paragraphs describe stage 2's code, which this head does not touch. Nothing else in the note is now false.
    • The correction is the right remedy. Both notes ship in one release, and the stage-2 note also lands in the rest and objectql CHANGELOGs that 23073-list-member.md does not reach; restoring the base sentence would ship it false. The seat answered the dev's open question with A (6132937336), the dev executed it as the one-sentence edit (6133092518), and the seat's PR note 6133122274 names the gate and the cause. The Check Changeset red is confined to that step and that file (above), and this record is the written confirmation it asks for.

③ Boundary flags

Dev's deviations (report 6132899471):

  1. packages/objectql/src/environment-overlay-email-locale-restart.test.ts outside the claim's literal paths — answered: it is the serving's real-engine pin and held the opposite assertion; the claim's "with their tests" covers it (① 12).
  2. Two comment-only docblocks outside the serving regions — answered, right (① 11).
  3. The canonical member spelled out rather than omitted — answered, right: triage's direction (carry member for a discriminated type, nothing for a name-only type) is met by key presence, and the ruling's "NULL means the canonical member" is a storage rule the serving maps back exactly (① 3 to 5).
  4. Two origin/main merges through os-regen-merge.sh — no regen deferral; the net diff against the merge base is the 13 files above and nothing from main.
  5. Three stray files at the dev container's filesystem root (/full1.pid, /gates2.err, /gates2.txt) — not in the repo, not in the diff, nothing in this head. Escalated to the seat as container housekeeping, not a contract matter.
  6. Zero label writes; no skip-changeset — right (②).

Open question (the stale stage-2 sentence; options A / B / C) — answered A by the seat, executed on this head, judged under ②.

Out-of-scope finding (the package-batch result arrays published[] / failed[], discarded[] / failed[], reverted[] / failed[] carrying { type, name } only, so two members of one template read identically there too), carried in the PR's acceptance notes as noted, not filed — it is the same family as this card, read against declared response shapes (PublishPackageDraftsResponseSchema, DiscardPackageDraftsResponseSchema, RevertPackageCommitResponseSchema); not this PR's surface and no regression of this head. Escalated to the seat: file it as a card from that contract text, or record on the card why not. Prime Directive #10 leaves the filing the seat's; this record does not bury it.

The stage-2 records' carried escalations (the runtime dispatcher's item GET without member; PUT's closed set) are not this PR's and are not reopened here.

Implemented-by: claude/issue-23073-list-member
Reviewed-by: session_01GZSPSQ5ArPjSJdxxuqwGoZ

VERDICT: PASS

Step 4 of os-regen-merge: the two os-regen mdx files regenerated from the
merge of origin/main 55828c5, carrying #23098's and #23075's rows and
this card's member rows.

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

Copy link
Copy Markdown
Contributor Author

Regen-provenance: 6133279014 · fe50ab6889502d229b5e0c48948535548ff8f909 → 2f2c6feea3975d5d90e448c2427ba5c19f8117db · per-file net diff, each head against its own merge base with origin/main, compared → identical hunk bodies on all 13 files; only hunk positions moved

domain:spec seat 2 · session session_01GZSPSQ5ArPjSJdxxuqwGoZ · 2026-10-12T23:20Z. The serial-relay hop after PR #23098 landed as 55828c514.

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/m tests tooling

Projects

None yet

2 participants