Skip to content

fix(metadata-protocol): the strict draft read naming no package serves its content from the draft its version names - #23727

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23715-strict-draft-read-address
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23715-strict-draft-read-address

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23715

Clause-②: yes

Dev session session_01HTBbFnY86QT5g7BxeMo3Ub, dispatched by domain:engine seat 1 (session_01GP3comALHRcRBixS2TEnLF), claim 6201610005. Related, not closed here: #23657 / PR #23706 (the draft save's address, unboundItemDraftAddress) and #23495 / PR #23654 (the three draft doors' address).

What this does

The strict draft read that names no package (GET /api/v1/meta/TYPE/NAME?state=draft) now takes its content from the draft its version names: the draft packagelessDraftAddress gives the draft save, the per-item publish and the draft discard naming none. Before, the content address came from the item's ACTIVE rows (packagelessReadAddress), which name no package for an item never published into one, so findServedOverlayRow took the first draft row the store returned.

state (no ?package=) content before content after version (unchanged)
no active row, drafts [package-unbound, Q], Q's stored first (in-memory double) Q's draft package-unbound draft package-unbound draft
no active row, drafts [package-unbound, Q], unbound stored first package-unbound package-unbound package-unbound
active rows bound to two packages, drafts [package-unbound, P], P's stored first P's draft package-unbound draft package-unbound draft
active rows bound to one package P P's draft, else the package-unbound one unchanged unchanged
no active row, only Q's draft / only a package-unbound draft that draft unchanged unchanged
no active row, drafts bound to Q and R, none package-unbound best-effort first row, version: null unchanged (best-effort fallback kept) null
  • strictDraftReadAddress (new private helper in protocol.ts) maps packagelessDraftAddress to a content address: the draft's binding, null for the package-unbound draft alone; an ambiguous address keeps the package-unbound draft, else the best-effort first match ADR-0048 §3.3 states.
  • servedOverlayRowCandidates / findServedOverlayRow accept packageId: null = the package-unbound row only. Every existing caller passes a string or undefined, so their candidate order is unchanged.
  • H3, measured: the draft-overlay preview naming no package (previewDrafts) shared the defective address: with Q's draft stored first it served q while the fixed strict read served unbound. It now takes the same draft address (it carries no version). The layered read (getMetaItemLayered) reads ACTIVE rows only, so it has no draft door to fix.
  • Rider 6200550012: PublishMetaItemRequestSchema.packageId's description now carries the "no package-unbound draft" qualifier SaveMetaItemRequestSchema.packageId's has (PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706). Description only; content/docs/references/api/protocol.mdx regenerated by check:generated --fix.
  • No accept set narrows: no request answered 200 before is refused now; ?package= still reaches each package's draft. The write, publish and discard addresses are not touched.

Verification (head eaa4b0ed)

  • Negative control on main (9e0945cf4), in-memory double: the new [#23715] pins, run before the fix: Tests 2 failed | 4 passed — expected 'q' to be 'unbound' (Q's draft first) and expected 'p' to be 'unbound' (two active packages, P's draft first).
  • better-sqlite3 (REST harness): all three orders (unbound saved first, Q's saved first, both seeded Q's first) were already green on main, and stay green: sys_metadata's (type, name, package_id, bundle_member) index returns the NULL-bound row first, so on this store content and version agreed by index order alone. These pins are positive pins plus the If-Match round trip; the double is the store that diverged.
  • Ablation (scripts/ablation-replace.mjs, mutation restoring main's address logic, the suite imports ./protocol.js from src): Tests 2 failed | 55 passed (57) — exactly the two divergence pins; restore proven (blob == HEAD, git diff HEAD empty).
  • pnpm --filter @objectstack/metadata-protocol test: Test Files 236 passed | 3 skipped, Tests 20572 passed | 19 skipped.
  • Draft-door pins from PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706 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: protocol.packageless-save-address.test.ts + sys-metadata-repository.draft-package-inherit.test.ts Tests 81 passed; meta-draft-head-package-inheritance.test.ts + meta-draft-read-door-census.test.ts Tests 30 passed (against a rebuilt metadata-protocol dist).
  • Typecheck: metadata-protocol, spec, rest, each exit 0.
  • pnpm --filter @objectstack/spec check:generated: All 14 generated artifacts are up to date after --fix regenerated check:docs alone.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at eaa4b0ed: 111 commands; --ran with exit codes: 111 derived, 110 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED: pnpm check:dual-build-cjs-loads (exit 3, PREREQUISITE NOT MET: needs every package built; CI runs it). Two first-pass exit-3 prerequisites were satisfied and re-run to exit 0: check-plugin-teardown-shape.mjs --self-test (fetched its pinned fixture commit) and spec check:skill-examples (built client-react).

Acceptance notes


Generated by Claude Code

…to the draft its version names (WIP)

Claude-Session: https://claude.ai/code/session_01HTBbFnY86QT5g7BxeMo3Ub
Co-authored-by: Claude <noreply@anthropic.com>
…s the draft its version names

Over drafts [package-unbound, Q] with no active row, GET ?state=draft
served its version from the package-unbound draft (headAt) but its content
from the first draft row a package-agnostic lookup returned. The read now
takes its content from packagelessDraftAddress, the address the draft
save, the per-item publish and the draft discard naming no package use.

Claude-Session: https://claude.ai/code/session_01HTBbFnY86QT5g7BxeMo3Ub
Co-authored-by: Claude <noreply@anthropic.com>
…erves the draft the strict read serves

Claude-Session: https://claude.ai/code/session_01HTBbFnY86QT5g7BxeMo3Ub
Co-authored-by: Claude <noreply@anthropic.com>
…ave's no-package-unbound-draft qualifier

Description only.

Claude-Session: https://claude.ai/code/session_01HTBbFnY86QT5g7BxeMo3Ub
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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/data-modeling/drivers.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/services-checklist.mdx (via getMetaItem (symbol, a method of class ObjectStackProtocolImplementation))

⛔ 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 — 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 6ee483cdb217e26b477e7eda9706488077e12a59 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7865f5f582ffd104e2c1bbe51181ba6b1e1b2b51 — the merge of head eaa4b0ed563a796fbb1722342de936f8a8ee65d6 into base 6ee483cdb217e26b477e7eda9706488077e12a59, 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 7865f5f582ffd104e2c1bbe51181ba6b1e1b2b51 && git checkout 7865f5f582ffd104e2c1bbe51181ba6b1e1b2b51
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6ee483cdb217e26b477e7eda9706488077e12a59 eaa4b0ed563a796fbb1722342de936f8a8ee65d6 && git checkout -B drift-repro 6ee483cdb217e26b477e7eda9706488077e12a59 && git merge --no-ff eaa4b0ed563a796fbb1722342de936f8a8ee65d6

node scripts/docs-audit/affected-docs.mjs --json 6ee483cdb217e26b477e7eda9706488077e12a59

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: eaa4b0ed563a796fbb1722342de936f8a8ee65d6
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts + packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/sys-metadata-repository.ts, content/docs/references/api/protocol.mdx, .changeset/23715-strict-draft-read-address.md, packages/metadata-protocol/src/protocol.packageless-save-address.test.ts, packages/rest/src/meta-draft-head-package-inheritance.test.ts

Read: card #23715 (body, rider 6200550012, triage 6200989991, claim 6201610005, dispatch 6201650972, report 6202831799); PR #23727's body, file list and the net diff origin/main...refs/remotes/pr/23727 (6 files, +292/-13; head confirmed at eaa4b0ed5, merge-base 9e0945cf4); the 41 check-runs on the head; and, at the head, in full: servedOverlayRowCandidates, findServedOverlayRow, the content-address, preview and strict-read regions of getMetaItem, readVersionToken, packagelessReadAddress, strictDraftReadAddress, packagelessDraftAddress, promoteDraftForPublish, the draft-discard arm of deleteMetaItem, getMetaItemLayered's overlay read, addressPackages and servedStoredRow with every caller (protocol.ts); unboundItemDraftAddress, headAt, resolveWriteHead (sys-metadata-repository.ts); both packageId descriptions in protocol.zod.ts and what c03077e294 (PR #23706) wrote into the save's. Governing text: ADR-0048 and ADR-0070 TL;DR 1 as the cited code states them; AGENTS.md Prime Directive #11 and Multi-agent discipline rules 3, 4, 6 and 7; the WHICH LEVEL prose in .github/workflows/pr-automation.yml (754-793); the LEVEL axis of scripts/check-changeset-no-major.mjs and scripts/pm/clause2-line.mjs; .claude/skills/pm-dispatch/references/lanes/spec.md (clause ② is a widening of the accept set or the public surface; a narrowing declared yes is never wrong). The whole diff is read at this tier; nothing is left to a TIER_DEFAULT accept.

① Derived judgments

  1. PublishMetaItemRequestSchema.packageId description (rider 6200550012): describe-only, three lines; no key, type, enum or default moves. TRUE against the code: packagelessDraftAddress (protocol.ts 19239) answers ambiguous for active rows bound to two or more packages, and for an item bound to none hands the draft bindings to unboundItemDraftAddress (sys-metadata-repository.ts 613), which returns the package-unbound draft whenever one is stored and ambiguous only when none is stored and two or more packages hold drafts; promoteDraftForPublish (24516) turns ambiguous into packagelessAddressAmbiguityRefusal('publish', ...), 409 RESOURCE_CONFLICT, thrown after the lock gate. The new clause, "when the active rows (or, with none and no package-unbound draft, the drafts) are bound to two or more packages", is word-for-word the save's clause that c03077e294 wrote into SaveMetaItemRequestSchema.packageId, and the regenerated content/docs/references/api/protocol.mdx carries the same sentence. RIGHT.

  2. The strict draft read naming no package (getMetaItem, state: 'draft', no packageId): its content address moves from packagelessReadAddress (the item's ACTIVE rows; undefined, any row, for an item never published into a package) to strictDraftReadAddress (19104), which is packagelessDraftAddress mapped to a candidate address: the draft's key (null for the package-unbound draft), or null plus bestEffort for ambiguous. That is the resolution the read's version already took: readVersionToken (19049) asks headAt with packageId: null at state: 'draft', and resolveWriteHead resolves one active binding to that package (with the orphan-adoption fallback to the unbound row), two or more to the unbound row, and none through the same unboundItemDraftAddress. Traced state by state at the head: bound to one package P with drafts at P and/or unbound, candidates [P, null], as before; bound to none with drafts [package-unbound, Q], [null], the unbound draft in either storage order (before: the first stored row), the version's row; bound to none with Q's draft only, [Q, null], Q's, as before; bound to none with drafts at Q and R and none unbound, no row at [null], then the package-agnostic fallback, version: null, as before; active rows bound to two or more packages with drafts [package-unbound, P], [null], the unbound draft, the row resolveWriteHead targets for the version (before: the first stored row); every none kind, the same candidate list as before, 404 NO_DRAFT. No request answered 200 before is refused now and no 404 becomes 200: the accept set does not move; exactly two answers move, both from a storage-order-dependent row to the row the version names. The write (resolveWriteHead and put), the publish (promoteDraftForPublish) and the discard (deleteMetaItem, draft arm at 29649) are untouched: the diff's protocol.ts hunks sit at 2578-2595, 10651-11016, 19071-19117 and 19199-19206. RIGHT, and inside the card's Wanted, the triage scope and the claim's file surface.

  3. The draft-overlay preview naming no package (previewDrafts, readState active): takes the same draftContentAddress with the same best-effort fallback. In every state the new candidate list finds the row it found before or the package-unbound draft in its place, and it reaches the active fallback in exactly the states it did before (bound to P with no draft at P or unbound; no draft at all), so nothing is lost and nothing narrows. The dev measured the divergence (Q stored first served q beside the strict read's unbound). One helper for both doors is the right shape; the card's harm (a token naming another row) cannot arise on the preview, which carries no version, so this is the consistency move the dispatch's H3 covers, not a second fix. RIGHT. getMetaItemLayered reads state: 'active' only (11570), so it has no draft door. RIGHT.

  4. servedOverlayRowCandidates (2580) and findServedOverlayRow (10650) accept packageId: null as "the package-unbound row alone". Module-private and private: no public surface moves. Every pre-existing caller passes a string or undefined: getMetaItemLayered spreads packageId only when truthy (11570); addressPackages(packageId: string | undefined) (2602) from servedViewExpansion (2637) and namesWithOwnStoredRow (10539); servedStoredRow(list, real) (2741) with real: string | undefined, called with undefined (2783) or a member of reals, which filters c.pkg !== undefined (2799); the system-opt-in test passes 'app.pin'. Before the diff a null would have fallen into the [undefined] arm (any row); no caller relied on that, so no existing reader changes. RIGHT.

  5. Pins. protocol.packageless-save-address.test.ts (the in-memory double): [package-unbound, Q] with no active row in both storage orders, content and version the unbound draft's, Q's draft still reachable by naming Q under its own token, an If-Match save with the served token editing the row read and the preview serving it; active rows bound to two packages with drafts [package-unbound, P] in both orders; CONTROL single draft (unbound, and Q's alone); CONTROL an active binding serving its package's draft beside another package's; CONTROL two packages' drafts and no unbound draft served best-effort with version null. meta-draft-head-package-inheritance.test.ts (better-sqlite3, the real routes): three orders plus the If-Match round trip and a single-draft control. The dev reports the double as the negative control (2 failed on main; 2 failed and 55 passed under ablation) and the better-sqlite3 pins as positive on main by index order, which the card's own measurement anticipated ("both orders built returned the unbound row first"). The card's three pins (both orders, a CONTROL, a negative control) are present. RIGHT; not re-run here, the Test Core shards on the head are the gate verdict.

  6. Changeset .changeset/23715-strict-draft-read-address.md: '@objectstack/metadata-protocol': minor, '@objectstack/spec': patch, Clause-②: yes; its body names the move FROM (the first stored draft row) TO (the draft the version names) and states that no accept set changes, which item 2 confirms. The level is judged in ②.

② Semver level

  • Shipped: Clause-②: yes (claim 6201610005, PR body, changeset) with @objectstack/metadata-protocol minor and @objectstack/spec patch. Check Changeset concluded success: the LEVEL axis reads yes from the PR body, finds one moved package graded minor, verdict clean.
  • What the diff publishes: no new key, type, enum or default on any published payload (the spec hunk is three description lines; the mdx is the same sentence regenerated); no accept-set move either way in metadata-protocol (① 2 and 3); no new export (both widened signatures are private). Clause ② is "a new key on a published payload" (the gate's header) or a widening of the accept set or the public surface (spec.md). Neither happens here, and nothing narrows. Under WHICH LEVEL (pr-automation.yml 754-767) the act earns patch for both packages: "a fix( that changes no public surface stays patch"; a changed served row at a fixed request is behavior, not surface.
  • Clause-②: yes is admissible (spec.md: declaring yes is never wrong; the dispatch ordered it after PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706) but it over-states. PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706's yes (narrowing) carried a real accept-set move (200 creates to 409); this diff carries none, and the yes is what forces metadata-protocol to minor for a patch act, in a lockstep group where the highest bump carries. Option B is the line the diff earns: Clause-②: no, both packages patch.
  • For B, exactly: (1) the PR body line Clause-②: yes to Clause-②: no (the gate re-reads the body on the edited event; verdict not-declared, exit 0, no push for that half); (2) .changeset/23715-strict-draft-read-address.md: line 2 '@objectstack/metadata-protocol': minor to patch, and its Clause-②: yes line to Clause-②: no, a commit, so a new head on which this record is re-rendered; (3) the claim's Clause-②: yes on the card stays as the seat's history, corrected in the seat's own comment if it takes B. Nothing else moves. Option A as shipped is permitted and gate-consistent, so this is not a FAIL condition on this head; B is the seat's call before the queue.

③ Boundary flags

  • open_questions[0] (level): answered in ②. B is what the diff earns; A is permitted; the seat decides before the queue.
  • open_questions[1] (an ambiguous address stays best-effort, version: null): right within this card. The Wanted and the triage scope say the content follows the address the version names; where that address is ambiguous the version names no row (resolveWriteHead leaves the target at the package-unbound row, which is absent) and the save naming none is refused 409, so the publishPackageDrafts can promote (and drain) ANOTHER package's draft row — the promote resolves the draft without the ADR-0048 package dimension #8907 If-Match harm cannot arise there. Answering 409 or 404 on the read would be an accept-set narrowing (Clause-② narrowing, an ADR-0087 marker) that the card did not rule and the claim's file surface excludes. Escalated as a candidate card for triage, not a change to this PR.
  • open_questions[2] (the previewDrafts fix in this PR): accepted. One helper, one address, no accept-set change (① 3), one extra assertion pins it; inside the dispatch's order to measure every draft read door naming no package and inside the claim's file surface (the content address in getMetaItem).
  • open_questions[3] (the dev container's shared checkout: HEAD detached, a same-named local branch at 9e0945c deleted): accepted, recorded. AGENTS.md Migrate documentation site to Fumadocs with monorepo structure and shared content #11 forbids file edits on the shared checkout (hook-enforced); none were made. Rules 3, 4 and 6 concern pushes and shared branches; the deleted local branch held no commits of its own and that step touched no remote ref, so nothing was lost. This seat's own checkout is unaffected (its HEAD is on its own branch; no local claude/issue-23715* branch exists there). Not a seat-discipline finding; the dev container sitting on a detached HEAD at 9e0945c is the dev's own state.
  • open_questions[4] (--force-with-lease=claude/issue-23715-strict-draft-read-address:4b88f9809 before the PR existed): accepted under AGENTS.md rule 3. Condition 1, the branch is claude/issue-*; condition 3, every commit on origin/main..pr/23727 carries one author and committer identity; condition 4, the PR (21:37:30Z) postdates the rewritten commits (20:40 to 20:51Z), so no reviewer or approval existed; condition 5, the push is spelled with the branch and the last-pushed sha. Condition 2 (this worktree created it) rests on the dev's account, and nothing contradicts it. 4b88f9809 is unreachable from the fetched refs, as a rewritten-away sha should be, so "tree byte-identical" is the dev's statement, not checked here. main has moved 6 commits past the branch point (the report said 3); one, fix(metadata-protocol): an orphan adoption rebinds a draft view in storage only, never registering it live #23724 (9bb93706f), touches protocol.ts at 27627-27735 (reassignOrphanedMetadata), far from this diff's hunks; GitHub reports mergeable_state: clean, and the queue rebuilds onto current main (rule 7). No merge is needed now.
  • out_of_scope_findings[0] (the list's draft-preview merge, mergePackageAwareOverlay through servedStoredRow, unmeasured for [package-unbound, Q] with no active binding): outside this card. The list carries no per-item version, so the card's harm cannot arise, and its slots are per package by design (ADR-0048), so whether it should agree with the by-name preview is a separate question. Escalated: the seat measures it or files it with the dev's dedupe words; not blocking.
  • out_of_scope_findings[1] (open PRs on protocol.ts at opening): fix(metadata-protocol): an orphan adoption rebinds a draft view in storage only, never registering it live #23724 has since landed (above); fix(metadata-protocol)!: a save naming a package of an object its package-less definition owns is refused, with a way out into that package (ADR-0029 D9.9) #23704's region (saveMetaItem, D9.9) is not touched here.
  • NOT-MEASURED pnpm check:dual-build-cjs-loads: hosted by Build Core (ci.yml 2643; lint.yml 5795), which concluded success on the head.
  • premise_still_valid: c03077e294 (PR fix(metadata-protocol)!: a draft save naming no package, of an item never published into a package, takes the draft doors' address (#23657) #23706) is an ancestor of the head; the save's text the publish's now matches is the one on main.
  • CI on the head: 41 check-runs, all completed; 36 success, 5 skipped (the Dogfood Regression Gate matrix shard, Console Pin Gate, Dogfood Verify CLI, Temporal Conformance, Packed-tarball smoke opt-in), 0 failure. Among the successes: Check Changeset, Governed Surface Queue Guard, Spec property liveness, Type Check (workspace, source gates, consumer gates, debt ledger), every Lint job, Test Core 1-6 and the aggregate, Build Core, Build Docs, The card this PR closes must claim this branch, No other open PR may claim the same issue and the same single-writer path, Flag docs affected by code changes, Check Documentation Links, Check PR Size.

Implemented-by: session_01HTBbFnY86QT5g7BxeMo3Ub
Reviewed-by: session_01GP3comALHRcRBixS2TEnLF

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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

Head: eaa4b0ed563a796fbb1722342de936f8a8ee65d6.

Item Reading
Shape PR → main. Line 1 is Fixes #23715, the only closing keyword. #23657 / PR #23706 and #23495 / PR #23654 are named without one.
Scope 6 files (+292 / −13): protocol.ts (getMetaItem's draft content address through the new private strictDraftReadAddress, and servedOverlayRowCandidates / findServedOverlayRow taking packageId: null as "the package-unbound row only"); PublishMetaItemRequestSchema.packageId's description (the rider 6200550012) and its regenerated protocol.mdx row; pins in metadata-protocol and packages/rest; and the changeset. That is the claim's surface (6201610005). The cross-lane parts are declared: the spec hunk on #18883 (6201661454), the packages/rest test on #6024 (6203379542). No governed path (check-governed-merges.mjs --pr 23727: 0 of 6). Under the 3,000-line threshold.
Clause-② yes, in the claim, the PR body and the changeset; @objectstack/metadata-protocol minor, @objectstack/spec patch. The review (②) reads that the diff earns no with both patch, and that yes is admissible and gate-consistent (declaring yes is never wrong, references/lanes/spec.md). The seat keeps option A as shipped. Moving to B costs a commit, a new head and a re-rendered record for a level the gates already accept.
Contract review 6203538652, PASS at this head, CONTRACT_REVIEW_TIER (schema face, Scope: line). The seat checked one load-bearing claim itself. servedOverlayRowCandidates' new null arm is reached only from strictDraftReadAddress. The pre-existing callers pass a string or undefined: addressPackages is typed string | undefined, the active read passes contentPackageId conditionally, and the expansion loop passes a real package. So no existing read moves.
Pins and controls Negative control on main (the in-memory double): 2 red. The read served Q's / P's body under the unbound draft's token. better-sqlite3 agreed by index order on main, so those pins are positive pins. Ablation (main's address logic restored): exactly the 2 divergence pins red; the restore is green.
Gates The dev's derived union: 111 derived, 110 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, hosted by CI's Build Core, green). CI on this head: 41 check-runs, 36 success, 5 skipped (the path-filtered roster), none failed.

Disposition:

  • open_questions: [0] A kept (above). [1] A: best-effort content on an ambiguous address. The version is null there and the save naming none is refused 409, so no If-Match writes a row other than the one read. A refusing read would be a narrowing no card has ruled, with no measured pull (startup-focus axis). It goes to Acceptance notes and is not filed. [2] A: the previewDrafts arm shares the address and the helper. [3], [4] accepted, as the record judges them (the dev container's own checkout; a --force-with-lease on its own branch before the PR existed).
  • out_of_scope_findings: the list's draft-preview merge (mergePackageAwareOverlay) was not measured. It carries no per-item version, so it cannot pair content with the wrong token, which was this card's harm. That goes to Acceptance notes and is not filed. The open PRs on protocol.ts are in other regions; fix(metadata-protocol): an orphan adoption rebinds a draft view in storage only, never registering it live #23724 has landed (9bb93706f), and the queue rebuilds on it.
  • The rider 6200550012 is carried here and closes with this card.

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