Skip to content

fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) - #23706

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23657-draft-save-inherits-one-package
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23657-draft-save-inherits-one-package

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #23657

Clause-②: yes (narrowing)

Dev session session_01MNCZd8XNeUr1ihWwz3ecHZ, dispatched by domain:engine seat 1 (session_01GP3comALHRcRBixS2TEnLF), claim 6196478876. The seat owes the review of the narrowing before the queue.

What this does

Take an item never published into a package (no active row bound to one) whose drafts are bound to packages. A draft save that names no package (PUT /api/v1/meta/TYPE/NAME?mode=draft) now takes the address that PR #23654 gave the per-item publish and the draft discard naming none. Before, the save always wrote the package-unbound draft, because SysMetadataRepository.resolveWriteHead read the address from the active rows only.

drafts stored (no active binding) draft save naming no package, before after (= publish and discard naming none)
none creates the package-unbound draft unchanged
package-unbound (with or without packages' drafts beside it) updates the package-unbound draft unchanged
only Q's minted a package-unbound draft beside Q's, 200 updates Q's draft, receipt package=Q
Q's and R's, no package-unbound draft minted a package-unbound draft beside them, 200 409 RESOURCE_CONFLICT with the ?package= remedy, nothing written

ADR-0070 TL;DR 1 (the package-less state is not an authoring destination) and ADR-0048 (prefer-local: the package-unbound draft keeps the address when it exists) are the governing records. I grepped docs/adr/ for the surface, and no ADR is reversed.

The landing point is the claim's file surface: resolveWriteHead and protocol.ts's packagelessDraftAddress and its draft-discard receipt. saveMetaItem needed no change, because its draft save already delegates the address to put.

Verification (head 23b58a388, merge base 8c85141c5)

  • Reproduced before the fix (H4) at base 3bc407d30, on the real stack (better-sqlite3, real ObjectQL, protocol and RestServer routes): the corner save answered 200 with "Saved view 'case_grid' (env-wide, state=draft) [seq=2]" (a package-unbound write); the two-package save answered 200 where 409 was expected; the discard receipt was "Draft discarded — view/case_grid. [seq=0]". Result: 3 failed, 2 passed (the two controls).
  • Pins after the fix:
    • packages/rest/src/meta-draft-head-package-inheritance.test.ts 21/21, with six [#23657] pins: the corner (save, strict draft read version, an If-Match save on it, publish, discard); a first-write pin (If-None-Match: *) refused 409 METADATA_CONFLICT with Q's draft unchanged; the discard receipt; the two-package refusal on all three doors (409 + RESOURCE_CONFLICT + ?package=, nothing written); CONTROL active binding; CONTROL package-unbound draft.
    • protocol.item-name-every-door.test.ts 40/40, with the new ADR-0086 D3 pin: a permission set whose only draft is BOUND's, saved as a draft naming no package, is judged against BOUND (an agreeing body is accepted, a body naming another package is refused naming BOUND).
    • sys-metadata-repository.draft-package-inherit.test.ts [#23657]: one package's draft (headAt and put agree), two packages' drafts in both storage orders (409 RESOURCE_CONFLICT), an unbound draft beside packages' drafts, an active binding.
  • Ablation (scripts/ablation-replace.mjs --hold, the inheritance arm swapped for null, metadata-protocol rebuilt, ablation-dist-preflight marker present in dist/index.cjs and dist/index.js): 4 pins red, namely the REST corner, the REST If-None-Match: *, the repository one-package pin and the permission-set pin; the two-package pins stay green (that arm was left intact). Restored with blob == HEAD 2f2030f9d205, git diff HEAD empty, rebuilt, --absent clean across 24 built files.
  • Fixture triage: two metadata-protocol: the draft doors naming no package resolve three addresses — the save and read inherit the active binding, the discard reads the draft rows, the publish takes the first draft row a package-agnostic findOne returns #23495 fixtures in protocol.packageless-save-address.test.ts reached an unbound draft beside a package's through the save this PR closes; they now seed that legacy row (seedRow), so both storage orders stay covered.
  • Spec: SaveMetaItemRequestSchema.packageId's draft arm rewritten in the words of the publish and discard descriptions (description only). Regenerated with pnpm --filter @objectstack/spec build, gen:schema, gen:docs; only content/docs/references/api/protocol.mdx moved. pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date.
  • Suites on 23b58a388: @objectstack/metadata-protocol 236 files passed, 3 skipped; 20566 tests passed, 19 skipped. Post-merge re-runs: rest meta-draft-head-package-inheritance + meta-draft-read-door-census 26/26; runtime package-promote-runtime-owner-refusal.integration (PR fix(metadata-protocol)!: the promote doors ask ADR-0029 D9.9's question the save door asks (#23630) #23683's pin), meta-published-runtime-publish, meta-save-preconditions-parity 35/35. Earlier, at 60d1a418f (the runtime change, unchanged since): rest full suite 290 files / 6085 tests; runtime draft doors 10 files / 848; objectql draft doors 9 files / 214.
  • Typecheck on 23b58a388: @objectstack/spec, @objectstack/metadata-protocol exit 0; @objectstack/rest exit 0 at 4275150c5 (its typecheck runs check:test-typecheck).
  • Derived gates on 23b58a388: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (9 paths), reconciled with --ran: 111 derived, 110 run (all exit 0), 1 NOT MEASURED (node scripts/check-plugin-teardown-shape.mjs --self-test, exit 3: the shallow clone cannot reach its pinned fixture commit), 0 unrun. check:adr-0087-registration: [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription), one disposition.

Neighbours

These open PRs touch protocol.ts / sys-metadata-repository.ts:

Acceptance notes

Body refreshed 2026-10-15T19:14Z by domain:engine seat 1 from the dev's round-2 report 6199998142 (its five listed edits; the OQ3 line names the card the seat filed).


Generated by Claude Code

…ublished into a package (#23657 pins, pre-fix)

Claude-Session: https://claude.ai/code/session_01MNCZd8XNeUr1ihWwz3ecHZ
Co-authored-by: Claude <noreply@anthropic.com>
…never published into a package, takes the draft doors' address; the draft discard receipt names the package (#23657)

Claude-Session: https://claude.ai/code/session_01MNCZd8XNeUr1ihWwz3ecHZ
Co-authored-by: Claude <noreply@anthropic.com>
… a package's draft that the draft save no longer mints (#23657)

Claude-Session: https://claude.ai/code/session_01MNCZd8XNeUr1ihWwz3ecHZ
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 15, 2026
@github-actions

github-actions Bot commented Oct 15, 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).

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

  • content/docs/api/error-catalog.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/api/index.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/automation/flows.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), SysMetadataRepository (symbol, a top-level class), sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/data-modeling/drivers.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/data-modeling/objects.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/deployment/cli.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/deployment/environment-variables.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/deployment/validating-metadata.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/kernel/cluster.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/kernel/contracts/metadata-service.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/kernel/services-checklist.mdx (via deleteMetaItem (symbol, a method of class ObjectStackProtocolImplementation), sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/permissions/authorization.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/permissions/permission-sets.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/plugins/packages.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))

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

  • content/docs/releases/implementation-status.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/17-3.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/17-5.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/17-6.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/17-7.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))
  • content/docs/releases/v17/index.mdx (via sys_metadata (literal, a string literal in resolveWriteHead))

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
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 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 8c85141c5b7575f75a3fb638c11c934f49f75e42 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 75e09b02cfeb436a52936a3a5c6767caa8b6a212 — the merge of head 23b58a388b252f21dc2f7719a493da2c1198a10f into base 8c85141c5b7575f75a3fb638c11c934f49f75e42, 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 75e09b02cfeb436a52936a3a5c6767caa8b6a212 && git checkout 75e09b02cfeb436a52936a3a5c6767caa8b6a212
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8c85141c5b7575f75a3fb638c11c934f49f75e42 23b58a388b252f21dc2f7719a493da2c1198a10f && git checkout -B drift-repro 8c85141c5b7575f75a3fb638c11c934f49f75e42 && git merge --no-ff 23b58a388b252f21dc2f7719a493da2c1198a10f

node scripts/docs-audit/affected-docs.mjs --json 8c85141c5b7575f75a3fb638c11c934f49f75e42

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 60d1a418f059bebf76472b37dd04de83697d71d4
Local-runs: none

Read: card #23657's body and all four comments (triage 6191435680, claim 6196478876, dispatch 6196513781, os-dev-report 6197717474); PR #23706's body, file list and net diff origin/main...60d1a418f (6 files, +306/−14, merge base 3bc407d30); the 39 check-runs on the head (30 success, 6 skipped, 3 still in progress: Test Core 2/6, 3/6 and 4/6; none failed. Check Changeset, Governed Surface Queue Guard, TypeScript Type Check and every Type Check and Lint lane are green). Cited code read at the head: resolveWriteHead, headAt, put, lockAccepts and delete in sys-metadata-repository.ts; packagelessDraftAddress, packagelessAddress, packagelessReadAddress, saveMetaItem, saveWriteBinding, authoredLockFamily, storedBodyForCarryForward and deleteMetaItem in protocol.ts; SaveMetaItemRequestSchema and PublishMetaItemRequestSchema in packages/spec/src/api/protocol.zod.ts. Judged against AGENTS.md Prime Directives #10 and #12 and Post-Task step 3 (changeset level, the Clause-② arms, the ADR-0087 disposition), ADR-0048, ADR-0070 TL;DR 1, ADR-0086 D3, ADR-0087's pre-GA level amendment (#18003) with check-changeset-no-major.mjs's header, and pm-dispatch's contract-review.md and execution-duties.md (the Clause-② criterion and its narrowing line).

① Derived judgments

  1. Two or more packages' drafts, no package-unbound draft, no active row bound to a package. The draft save naming no package went from 200 (it minted a package-unbound draft) to 409 RESOURCE_CONFLICT with the ?package= remedy, and nothing is written to sys_metadata. RIGHT:
  2. Only package Q's draft, same item. The save updates Q's draft (receipt package=Q) and no longer mints a package-unbound draft beside it. RIGHT: ADR-0070 TL;DR 1, and the unbound arm keeps ADR-0048 prefer-local. Every judge that reads the head follows, because each asks headAt, the one resolution put locks on (finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128): the strict draft read's version, the lock parent, the credential carry-forward, the _lock family carry and saveWriteBinding. Three derived accept-set moves follow. Each is right, each pulls back to a declared contract, and none adds a key or value:
  3. Unchanged cases. A package-unbound draft (with or without packages' drafts beside it), an active binding, a save naming a package, and a brand-new item all behave as before. RIGHT; controls are pinned in both layers.
  4. packagelessDraftAddress now asks unboundItemDraftAddress. Behaviour is preserved: the same four outcomes, with into equal to key as before. RIGHT; this is the shared rule H2 asked for.
  5. Draft discard receipt. It now reads Draft discarded (package=Q) — TYPE/NAME. [seq=N] whenever the removed draft is bound to a package, the named discard included. A package-unbound draft's receipt is byte-identical. RIGHT (the card's same-family line). This is message text, not an accept set. Nothing in the repo parses it, and the pinned objectui sha only matches toContain('Draft discarded').
  6. Public surface. unboundItemDraftAddress is exported from its module but not from src/index.ts, and the package's exports map has only .. No published export or type moves. RIGHT.
  7. WRONG: the narrowed door's published contract text is left false. SaveMetaItemRequestSchema.packageId (packages/spec/src/api/protocol.zod.ts lines 837–854) ships in @objectstack/spec/api and is rendered at content/docs/references/api/protocol.mdx:2745. It says a draft save naming no package writes the draft of the active row's package "when it is package-bound …, else the env-local draft". At this head that is false on both arms the diff changes: with only Q's draft the save writes Q's draft, and with two or more packages' drafts it is refused 409. PR fix(metadata-protocol)!: the per-item publish and the draft discard that name no package act on the draft the draft save writes (#23495) #23654 amended the publish's and the discard's packageId descriptions for this same rule ('@objectstack/spec': patch). After this head, the three doors' runtime agrees but their published descriptions do not, so the disagreement this card closes moves into the contract. This falls under Prime Directive chore: version packages #10 (declared ≠ enforced) and Add comprehensive test suite for Zod schema validation #12 (the spec is the one contract; change it deliberately).

② Semver level

③ Boundary flags

Remedy for item 7:

Implemented-by: session_01MNCZd8XNeUr1ihWwz3ecHZ
Reviewed-by: session_01GP3comALHRcRBixS2TEnLF

VERDICT: FAIL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat answer to the contract review 6198192488 (FAIL at 60d1a418f0) · domain:engine seat 1 · sunhaolin · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T17:30Z

Verified, not adopted on the record's word. On origin/main, SaveMetaItemRequestSchema.packageId's description (packages/spec/src/api/protocol.zod.ts) still says a draft save naming no package writes "the env-local draft" when the item's active row is not package-bound. This PR changes exactly that arm: one package's draft is inherited, and two or more are refused 409 RESOURCE_CONFLICT. PR #23654 rewrote the publish and discard descriptions for the same rule, so the three doors' published texts now disagree. Finding ①7 stands. The runtime change is accepted as the record judges it.

The seat's ruling (the PR #23654 precedent):

  1. The published description. Rewrite SaveMetaItemRequestSchema.packageId's draft arm to state the rule this PR ships, in the same words the publish and discard descriptions use for it. Regenerate with the spec generators (gen:schema, gen:docs, and whatever check:generated names). ⛔ Never hand-edit a generated file. Add '@objectstack/spec': patch to the changeset. The packages/spec hunk is cross-lane domain:spec; this seat declares it on [PM seat] domain:spec · seat 3 — 🟢 baozhoutao · session_01D6F9axfoPzR7t8G1J7Ytzz #18883.
  2. Clause-②: yes (narrowing). The claim 6196478876 is amended to say so; the PR body and the changeset follow. Three reasons: the published description changes with the narrowing; the two-package arm narrows; and a permission-set body naming package Q, saved naming no package, is now accepted where it was refused 400 (ADR-0086 D3), which widens. Declaring yes is never wrong (references/lanes/spec.md). PR fix(metadata-protocol)!: the per-item publish and the draft discard that name no package act on the draft the draft save writes (#23495) #23654 is the precedent.
  3. Name every derived change in the changeset, FROM → TO. These are the two the record found unnamed:
    • If-None-Match: * on such a save now answers 409 METADATA_CONFLICT;
    • the permission-set body naming Q is now accepted.
    • The If-Match with Q's token, now accepted, belongs there too.
  4. OQ3, measure-first, not fixed here. Over [package-unbound, Q] drafts with no active binding, measure whether the strict draft read serves its content from a different row than its version. Report the reading. If it diverges, this seat files the card. ⛔ Do not widen this PR to fix it.

Next: the rework goes to the cloud dev session session_01MNCZd8XNeUr1ihWwz3ecHZ now. The next review runs at CONTRACT_REVIEW_TIER (a published schema enters the diff, the schema face), with the Scope: line. The PR does not arm the queue before a PASS.


Generated by Claude Code

…'s address the publish and discard take (#23657)

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 23b58a388b252f21dc2f7719a493da2c1198a10f
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata-protocol/src/sys-metadata-repository.ts, packages/metadata-protocol/src/protocol.ts, content/docs/references/api/protocol.mdx, .changeset/23657-draft-save-one-package-draft.md

Round 2, on the schema face. Read: card #23657's body and all five comments (triage 6191435680, the amended claim 6196478876, dispatch 6196513781, the round-1 report 6197717474, the round-2 report 6199998142); PR #23706's body as refreshed at 19:14Z, its three comments (the drift advisory 6197700560, the round-1 record 6198192488 FAIL at 60d1a418f0, the seat's ruling 6198239592), its nine commits, its file list and the net diff origin/main...23b58a388 (9 files, +349/−19, merge base 8c85141c5); card #23715; the 48 check-runs on the head. Cited code read at the head: SaveMetaItemRequestSchema, PublishMetaItemRequestSchema and DeleteMetaItemRequestSchema in protocol.zod.ts; unboundItemDraftAddress, packagelessAddressAmbiguityRefusal, headAt, resolveWriteHead, put and whereFor in sys-metadata-repository.ts; packagelessReadAddress, packagelessAddress, packagelessDraftAddress and the draft-discard receipt in deleteMetaItem in protocol.ts; the regenerated row of protocol.mdx, compared as text with the zod string (a read of two blobs, not a generator run); the precedent changeset .changeset/23495-draft-doors-one-address.md on main. Judged against AGENTS.md Prime Directives #10 and #12, ADR-0048, ADR-0070 TL;DR 1, ADR-0086 D3, ADR-0087's pre-GA level convention as check-adr-0087-registration.mjs reads it, and pm-dispatch's contract-review.md, execution-duties.md and lanes/spec.md (the Clause-② criterion, its narrowing line, and the line that declaring yes for a narrowing is never a mistake). The rest of the diff (the four test files) is the lane seat's ACCEPT at TIER_DEFAULT; it was read and nothing in it contradicts what follows.

① Derived judgments

The ruling's four points first, then the surface.

  1. Point 1, the published description: met, and TRUE against the code. SaveMetaItemRequestSchema.packageId (protocol.zod.ts, the describe from line 837) states the draft arm in four clauses, each read against the head:
    • "the draft of the one package the item's active rows are bound to, else its package-unbound draft, adopted into that package": resolveWriteHead with one active binding P sets targetPackageId to P and looks up P's draft; with none it takes the SqlDriver org auto-scope makes SysMetadataRepository.listDrafts blind to env-wide drafts (organization_id IS NULL ∧ injected org = always empty); mode=draft saves drop package_id #11087 fallback at the end of the method, the package-unbound draft, which put then stamps existingPkg ?? targetPackageId, so P. RIGHT. "the draft the per-item publish naming no package promotes": packagelessDraftAddress with own P answers P's draft, else the unbound draft with into P. The same row. RIGHT.
    • "For an item never published into a package it is the package-unbound draft, else the one package's draft, and with no draft at all the save creates the package-unbound draft": the new else arm (lines 890–913) reads the item's draft rows and asks unboundItemDraftAddress: an unbound draft present answers key null; else one candidate answers that package; else none with key null, and put inserts with package_id null. RIGHT. The phrase also covers an item whose only active row is package-unbound (it adds nothing to bindings), the reading round 1 gave the changeset's "no active row bound to one".
    • "refused 409 RESOURCE_CONFLICT when the active rows (or, with none and no package-unbound draft, the drafts) are bound to two or more packages, as that publish is": the two-or-more active bindings branch and the ambiguous answer both set ambiguousPackages, which put throws as packagelessAddressAmbiguityRefusal('save') before any write; with an unbound draft present the rule answers that draft and nothing is refused. packagelessDraftAddress takes the identical two branches for the publish. RIGHT, and the qualifier "and no package-unbound draft" is the true one.
    • The unchanged sentences (the active arm, the parent-version sentence) still hold: headAt and put share the one resolution, so the row written is the row the lock judges and the strict draft read's version names.
    • The words are the publish's and the discard's: the same sentence shape and terms ("the one package the item's active rows are bound to", "its package-unbound draft", "never published into a package", the 409 clause with its cross-reference to PublishMetaItemRequestSchema.packageId), the save adding only its own create arm. No key, type, enum, optionality or default moves (+10/−4 inside one describe).
  2. Regenerated, not hand-edited. The only artifact that moved is content/docs/references/api/protocol.mdx:2745, the generator's output surface (build-docs.ts owns content/docs/references/**; check-generated.ts lists it under gen:docs). Its cell equals the zod string byte for byte (1555 characters, the pipe escaped as the table needs). No tracked JSON artifact embeds the description (the api.json surfaces name the schema, not its text), and no other tracked file carries either the old "env-local draft" wording or the new one. The regenerate commit e27c38524 follows the source commit eedbcda1c. RIGHT.
  3. Point 2: met. Clause-②: yes (narrowing) at the head of the PR body and on its own line in the changeset. The spelling is the one check-adr-0087-registration.mjs pins as the narrowing arm beside a yes value, "a diff that widens AND narrows" (its fixture at line 2464), and Check Changeset is green on both runs. RIGHT.
  4. Point 3: met. The changeset names the three derived moves FROM → TO under "What follows from the save writing Q's draft": If-Match with Q's token, 409 METADATA_CONFLICT → accepted; If-None-Match: *, a 200 create → 409 METADATA_CONFLICT with nothing written; a permission-set body declaring packageId: Q, 400 VALIDATION_ERROR → accepted, a body naming another package still refused and now naming Q. Each matches the code (one resolution feeds lockAccepts; ADR-0086 D3 judges against the binding the write resolves). Two new pins hold the two that were unpinned in round 1, the REST If-None-Match: * pin and the [#23657] ADR-0086 D3 pin in protocol.item-name-every-door.test.ts, both red under the dev's ablation. RIGHT.
  5. The runtime change is the round-1 one. The net diff of sys-metadata-repository.ts and protocol.ts against main is identical, modulo hunk headers, to the round-1 net diff 3bc407d30..60d1a418f. No round-2 commit touches either file; the main merge (23b58a388, parents 4275150c5 and 8c85141c5) brings PR fix(metadata-protocol)!: the promote doors ask ADR-0029 D9.9's question the save door asks (#23630) #23683's 36 lines into protocol.ts at the promote door's ADR-0029 D9.9 check (lines 24480–24515), outside packagelessDraftAddress, the discard receipt and resolveWriteHead. Round 1's judgments 1–6 stand: the two-package refusal, the one-package inheritance, the unchanged cases, the shared pure rule, the discard receipt, and no published export moving (unboundItemDraftAddress is exported from its module, not from src/index.ts).
  6. Public surface of @objectstack/spec. One describe() string of a published request schema changes; the schema's parse accept set is byte-identical before and after. Under lanes/spec.md that is the auxiliary face of the contract, which rides with the source PR together with its regenerated references artifact. RIGHT.
  7. Fixture triage in protocol.packageless-save-address.test.ts is unchanged since round 1 (setup lines only, seeding the legacy row the save no longer mints; assertions untouched). RIGHT.
  8. Check-runs on the head: 48, all completed; 41 success, 7 skipped (Console Pin Gate, Dogfood Verify CLI, the Dogfood matrix placeholder, Temporal Conformance, Packed-tarball smoke, and the Auto Label and Check PR Size re-runs the 19:14Z body edit triggered), none failed. Check Changeset is green on the 18:24Z push run and on the 19:15Z re-run; Governed Surface Queue Guard, TypeScript Type Check, the four Type Check lanes, the nine Lint lanes, Build Core, Build Docs, Test Core 1–6 and the aggregate, Spec property liveness, Flag docs affected, Check Documentation Links and the claim and single-writer guards are green.

② Semver level

  • '@objectstack/metadata-protocol': minor with the BREAKING banner, the ! in the title and adr-0087: not-required (no-migration-prescription): RIGHT for an accept-set narrowing while .changeset/pre.json holds pre mode, as round 1 judged and as the precedent .changeset/23495-draft-doors-one-address.md was graded. The disposition's facts still hold at this head, and it now says the spec changes one description only.
  • '@objectstack/spec': patch: RIGHT. A describe() text change moves no shape, key or default; lanes/spec.md grades the auxiliary face patch, and PR fix(metadata-protocol)!: the per-item publish and the draft discard that name no package act on the draft the draft save writes (#23495) #23654's changeset carries the same pair for the same kind of edit.
  • Clause-②: yes (narrowing), in the PR body and the changeset: RIGHT. The criterion (execution-duties, lanes/spec.md) is whether the card widens an accept set or a public surface. The two-package arm narrows; the If-Match with Q's token and the permission-set body naming Q are inputs the door now accepts where it refused, even though each pulls back to a declared contract; and lanes/spec.md rules that declaring yes for a narrowing is never a mistake. The seat's point 2 of 6198239592 is consistent with the governing text, and the spelling is machine-recognised.
  • The changeset's FROM, TO, Unchanged and Remedy paragraphs match the diff; nothing the diff publishes is left undeclared.

③ Boundary flags

Implemented-by: session_01MNCZd8XNeUr1ihWwz3ecHZ
Reviewed-by: session_01GP3comALHRcRBixS2TEnLF

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · domain:engine seat 1 · session_01GP3comALHRcRBixS2TEnLF · 2026-10-15T19:27Z · ACCEPT

Head: 23b58a388b252f21dc2f7719a493da2c1198a10f.

Item Reading
Shape PR → main. The body opens Fixes #23657, and that is its only closing keyword. #23715 is named without one.
Scope 9 files (+349 / −19): sys-metadata-repository.ts (resolveWriteHead and unboundItemDraftAddress), protocol.ts (the draft-discard receipt), and SaveMetaItemRequestSchema.packageId's description with its regenerated protocol.mdx row. Also four test files and the changeset. This is the amended claim's surface (6196478876). The cross-lane parts are declared: domain:spec on #18883 (6198253730), and the packages/rest test on #6024 (6200504596). No content/docs/releases/ edit and no governed path (check-governed-merges.mjs --pr 23706: 0 of 9). It is under the 3,000-line threshold.
Clause-② yes (narrowing) in the PR body and the changeset, per ruling 6198239592 point 2 (PR #23654 precedent). @objectstack/metadata-protocol is minor BREAKING, with ADR-0087 not-required (no-migration-prescription). @objectstack/spec is patch (description only).
Contract review Round 1, 6198192488: FAIL (①7, the published description was false). The seat ruled four points in 6198239592. Round 2, 6200477973, at this head: PASS, CONTRACT_REVIEW_TIER, schema face, with a Scope: line. The seat checked the description itself against unboundItemDraftAddress and resolveWriteHead at this head. Its order matches clause by clause: the one active package's draft, else the unbound draft adopted into it (#11087); with no active binding, the unbound draft, else the one package's draft, else a create; two or more packages refused 409.
Pins and controls Six REST [#23657] pins, the ADR-0086 D3 permission-set pin, and the repository pins in both storage orders. The ablation of the inheritance arm turned 4 pins red, and the two-package arm stayed green by design.
Gates The dev's derived union at this head: 111 derived, 110 exit 0, 1 NOT MEASURED (check-plugin-teardown-shape.mjs --self-test on a shallow clone; CI's Lint & Repo Gates is green). CI on this head: 48 check-runs, 41 success, 7 skipped, none failed. The skips are the path-filtered five and the body-edit re-runs of Auto Label / Check PR Size, each beside a success run.
PR body Refreshed by the seat at 19:14Z from the dev's five listed edits.

Disposition:


Generated by Claude Code

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