Repository navigation
metadata-protocol: a draft save naming no package, of an item never published into a package whose only draft is bound to package Q, writes a package-unbound draft beside it, while the publish and discard naming none act on Q's (after PR #23654) #23657
Description
Filing gate: ① a product defect, measured at a public door, finding class (a). It comes from #23495's dev report 6189982386 (open_questions[0]) and the contract review 6190278990 on PR #23654 (③ OQ1, which escalates it for this card). Filed by domain:engine seat 1 (#6367) · sunhaolin · session_01GP3comALHRcRBixS2TEnLF. ⛔ Not a claim. Reader who acts: triage, to grade and route (the producer is packages/metadata-protocol). Order: after PR #23654 (#23495) lands, since it sets the publish's and discard's address this card aligns the save to.
reach: the draft save naming no package, PUT /api/v1/meta/TYPE/NAME?mode=draft. It was measured by #23495's dev on a real stack (better-sqlite3, the real ObjectQL, protocol and RestServer routes) at origin/main 23e689dc7d.
What happens
An item that was never published into a package (no active row) holds one draft, bound to package Q (saved with ?mode=draft&package=Q).
- The draft save naming no package writes a package-unbound draft beside it. The drafts become
[package-unbound, Q], because the save's address (resolveWriteHead, 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) is read from the active rows, and there are none. - On 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 head, the per-item publish and the draft discard naming no package act on
Q's draft in that state: the unbound draft, else the one package's draft. That holds until the save has minted the unbound one, after which they act on that. - So in this corner the doors disagree, the defect class 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 closes for items with an active binding. ADR-0070 TL;DR 1 rules the package-less state out as an authoring destination.
Wanted (the dev's option A, which the review endorses)
- With no active binding and exactly one package's draft stored, the draft save naming no package inherits that draft's package in
resolveWriteHead. Then the save, the strict draft read, the publish and the discard all nameQ's draft. - With two or more packages' drafts and no unbound draft, the save is refused
409 RESOURCE_CONFLICTwith the?package=remedy, as 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 publish and discard are there. - Pins: the corner above, before and after; the two-package refusal; a CONTROL that an item with an active binding is unchanged; a negative control that goes red when the fix is removed.
- Same family, carried here: the draft discard's receipt ("Draft discarded — TYPE/NAME") does not name the package, while the direct delete's and (after 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) the draft save's do. The dev noted it, not filed.
Clause-②: a save refused where it answered 200 is a narrowing. The claimant judges it from the diff.
Dedupe
MCP search_issues on this repo, closed included: "draft save naming no package item never published only one package draft writes package-unbound draft beside it resolveWriteHead inherit" returns 28 hits.
- The nearest are 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 (closed: the second package-less draft save's 409 when the head read and the repository's inheritance read different rows), metadata-protocol: a save naming a package beside an existing package-less row of the item mints a second row, and the read naming no package keeps serving the unbound one (S6, reachable through today's doors) #23291 (closed: the save naming a package beside an unbound row), metadata-protocol: a package-less direct save of an item a package owns answers 200 and writes a package-less row the item read never serves; the draft save already resolves the owning package (#11087 / #22128) #23165 (closed: the package-less direct save) and 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 (this corner's parent).
None is this.
Dedupe words: draft save no active binding inherit one package draft; resolveWriteHead never published; draft discard receipt package.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 15, 2026
Triage: first grade ·
bug·priority:p1·target:v18·domain:engine·pm:queue· land after PR #23654 · triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-15T11:54ZRead fresh. An item with no active binding and one draft bound to package Q gets a second, package-unbound draft from a draft save naming no package (
resolveWriteHeadreads the address from active rows, and there are none). On PR #23654's head, the publish and discard naming none act on Q's. So the doors disagree in this corner. Measured on a real stack by #23495's dev.Grade p1 +
target:v18: the fix (option A, endorsed by #23654's contract review) refuses a save that answers 200 today (409 RESOURCE_CONFLICTwith the?package=remedy when two or more packages' drafts exist). That narrows a/metastore door's answer, the protocol class (filing-gate.md〈定级判据〉).Lane
domain:engine(packages/metadata-protocol).Order: it lands after PR #23654 (#23495, open), which sets the publish's and discard's address this card aligns the save to.
Scope: the card's Wanted and its four pins, plus the discard receipt that names the package (the same family).
findingleaves on grading.objectstack-fleet commentedon Oct 15, 2026
Claim: PM loop round 2 · 2026-10-15T16:01Z
Session:
session_01GP3comALHRcRBixS2TEnLF(the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)Account:
sunhaolin(the seat's linked user asGET /useranswers it; always the card's assignee)Branch:
claude/issue-23657-draft-save-inherits-one-packageWorktree:
objectstack-issue-23657Domain:
domain:engineSeat:
domain:engine#1(seat post #6367)File surface:
packages/metadata-protocol/src/sys-metadata-repository.ts, the write-address resolution only (resolveWriteHeadand its draft-save path), andpackages/metadata-protocol/src/protocol.ts's draft-discard receipt (the "Draft discarded —" text) plus, if needed,saveMetaItem's draft-save call into that resolution. Plus new real-stack pins and one changeset.changeset/23657-*.md. Amended 2026-10-15T17:29Z after contract review 6198192488: alsopackages/spec/src/api/protocol.zod.ts'sSaveMetaItemRequestSchema.packageIddescription and the artifacts its generators regenerate (cross-lanedomain:spec, declared on #18883), since the description states the old address. Stop on any other breach and explain it in the report.Container & model:
M,mode:cloud(the maintainer's order on seat post #6367: 「后续 重活开发 都可以单独开云卡」),model: TIER_DEFAULT(this run'sdispatch-gates.mjs --tier: "no path-derived mandate"; a door's address resolution with one narrowing arm); its review runs atCONTRACT_REVIEW_TIERonce the published schema's description enters the diff (the schema face)Clause-②: yes (narrowing)
Responsibility: the risk is produced by
metadata-protocol's draft save naming no package:resolveWriteHeadreads the address from the active rows, and an item with none gets a package-unbound draft beside package Q's, while the publish and discard naming none act on Q's | no platform path covers it: PR #23654 aligned the publish and the discard, not the save | an author'sPUT /api/v1/meta/TYPE/NAME?mode=draftover an item never published reaches it (measured on a real stack at23e689dc7d, #23495's report 6189982386open_questions[0]).Thread-read: 6191435680
Serial constraints cleared: the ordered-after PR #23654 (#23495) is on
main(57994c311f). This seat's in-flight work onprotocol.tsis in other regions: #23514 (cloud) inclassifyObjectContributionand the D9.9 call insaveMetaItem, and PR #23683 (#23630) in the promote doors. #23514's object rows always have an active binding, which this card's arm never reads. Whichever of #23514 and this card lands second mergesmainand re-runs the other's pins.Clause-②: yes (narrowing), amended fromnoafter contract review 6198192488. The two-package refusal narrows (a save refused where it answered 200), the publishedpackageIddescription changes with it, and a permission-set body naming package Q, saved naming no package, is now accepted where it was refused 400. That is the precedent of PR #23654 (#23495), whose published descriptions changed the same way. The seat owes theCONTRACT_REVIEW_TIERreview before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Dispatch ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T16:03Z:mode:cloud, the dev sessionsession_01MNCZd8XNeUr1ihWwz3ecHZ, titled "⚡ engine #23657 draft save inherits the one package's draft". It runs at the default tier on branchclaude/issue-23657-draft-save-inherits-one-package, the claim 6196478876.409 RESOURCE_CONFLICTand its?package=remedy.Fixes #23657, then anos-dev-reportcomment here. The seat reviews the narrowing before the queue.Generated by Claude Code
test(rest): pin the draft save naming no package over an item never p…
fix(metadata-protocol): the draft save naming no package, of an item …
test(metadata-protocol): seed the legacy package-unbound draft beside…
chore(changeset): declare the draft save naming no package's narrowing (
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23657, "status": "done", "branch": "claude/issue-23657-draft-save-inherits-one-package", "pr": "https://github.com/objectstack-ai/objectstack/pull/23706", "session": "session_01MNCZd8XNeUr1ihWwz3ecHZ — this cloud dev run (Claude-Session trailer on every commit); dispatched by session_01GP3comALHRcRBixS2TEnLF, claim 6196478876 (verified newest Claim: names this branch)", "premise_still_valid": true, "summary": "Option A as ruled. SysMetadataRepository.resolveWriteHead, for a draft write naming no package over an item with no active package binding, now reads the item's DRAFT rows and asks one new pure rule, unboundItemDraftAddress (sys-metadata-repository.ts, not on the package entry). The package-unbound draft keeps the address; else the one package's draft is the address (put updates it, headAt and the strict draft read's version serve it); two or more packages' drafts set ambiguousPackages, so put throws the existing packagelessAddressAmbiguityRefusal('save') — 409 RESOURCE_CONFLICT with the ?package= remedy, nothing written. protocol.ts packagelessDraftAddress's bound-to-none arm now calls the same helper, so the save, publish and discard read ONE rule (H2: they share the pure rule only; the repository reads inside its transaction through its engine and the protocol reads through findEnvironmentRows with its own unprovisioned-store discrimination, so each reads rows its own way). The draft-discard receipt names the package: 'Draft discarded (package=Q) — TYPE/NAME. [seq=N]'; the unbound draft's receipt is byte-identical (H3 confirmed: text change only). H1 confirmed (resolveWriteHead read active rows only). H4 confirmed: reproduced on the real stack at base 3bc407d30 before the fix. saveMetaItem needed no change (its draft save already delegates the address to put). Two #23495 protocol fixtures that reached [P's draft, then unbound] through the closed save now seed that legacy row directly.", "tests": "HEAD 60d1a418f. BEFORE (base 3bc407d30 + new pins, metadata-protocol dist built from base): pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/meta-draft-head-package-inheritance.test.ts -t 23657 -> 3 failed | 2 passed; corner save Received \"Saved view 'case_grid' (env-wide, state=draft) [seq=2]\" (package-unbound write); two-package save \"expected 200 to be 409\" with body {\"success\":true,...\"message\":\"Saved view 'case_grid' (env-wide, state=draft) [seq=3]\"}; discard receipt \"Draft discarded — view/case_grid. [seq=0]\". AFTER (dist rebuilt): that file 20/20 passed; metadata-protocol sys-metadata-repository.draft-package-inherit + protocol.delete-receipt-wording + protocol.packageless-save-address 90/90; @objectstack/metadata-protocol full suite Test Files 236 passed | 3 skipped, Tests 20565 passed | 19 skipped (lock VERDICT command-exit 0); @objectstack/rest full suite Test Files 290 passed, Tests 6085 passed | 330 skipped (exit 0); runtime draft-door tests (meta-draft-read-builder-gate, meta-item-query-decision, meta-list-projection-parity, meta-package-all-fold, meta-published-runtime-publish, meta-read-org-scope-parity, meta-save-preconditions-parity, dispatcher-5xx-always-logged, package-list-commits-org-scope.integration, package-revert-code-shipped-members.integration) 10 files / 848 passed; objectql (protocol-publish-package-drafts, protocol-meta, protocol-save-meta-repo-path, sys-metadata-repository, sys-metadata-repository-list-drafts, publish-meta-response-conformance, save-meta-response-conformance, protocol-adopt-orphans-object-registry, default-record-page-save-door) 9 files / 214 passed. Typecheck: @objectstack/metadata-protocol exit 0 (--listFiles counts both changed test files), @objectstack/rest exit 0 (its typecheck runs check:test-typecheck, which covers the test file; tsc program alone lists it 0 times). ABLATION: anchor ' else targetPackageId = address.key;' -> \" else targetPackageId = String('ABLATED_23657') && null;\" via scripts/ablation-replace.mjs --hold (on-disk count injected 1 / removed 0), metadata-protocol rebuilt, ablation-dist-preflight: marker hit in dist/index.cjs and dist/index.js; -t 23657 -> 1 failed | 4 passed (corner pin red, Received \"Saved view 'case_grid' (env-wide, state=draft) [seq=2]\"; the two-package arm left intact by design); restore: blob after restore == HEAD 2f2030f9d205, git diff HEAD empty; rebuilt; preflight --absent: marker absent from all 24 built files, tree clean.", "gates": "Local, at 60d1a418f: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 6 paths vs merge base 3bc407d30) -> 64 commands run, each exit captured before any pipe: 62 exit 0, 2 exit 3 PREREQUISITE NOT MET = NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone, pinned fixture commit 621a4876 unreachable), pnpm check:dual-build-cjs-loads (needs whole-repo dist/). Reconciled: dispatch-gates --repo objectstack-ai/objectstack --ran FILE (lines 'CMD :: exit N') -> exit 0, '65 derived, 63 run, 2 NOT-MEASURED, 0 UNRUN'. check:adr-0087-registration: changeset read as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). CI on PR #23706 head 60d1a418f at report time: 'No other open PR may claim the same issue' success; 'Packed-tarball smoke (opt-in)' skipped; the other 21 (Type Check · source gates/workspace/debt ledger/consumer gates, Lint · ESLint/PM dispatch-gates/CI release & engine gates/docs & code guards/repo & tooling gates/scoped ×3, Check Changeset, Check PR Size, Governed Surface Queue Guard, the claim/single-writer guards, Flag docs affected, Check Documentation Links, filter, Part-of guard) in_progress — not waited on, per role file.", "line_budget": "6 files, +306 / -14 (src: sys-metadata-repository.ts +69/-2, protocol.ts +18/-10; tests +190/-2; changeset +29)", "files_changed": [ ".changeset/23657-draft-save-one-package-draft.md", "packages/metadata-protocol/src/sys-metadata-repository.ts", "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts", "packages/metadata-protocol/src/protocol.packageless-save-address.test.ts", "packages/rest/src/meta-draft-head-package-inheritance.test.ts" ], "deviations": [ "Worktree: the primary checkout was on the designated branch at start; I detached it (no edits made there) so the branch could be created from origin/main in the linked worktree ../objectstack-issue-23657 per role-file rule 1.", "File surface: protocol.packageless-save-address.test.ts (a #23495 test file) was edited — two fixtures reached their precondition through the very save this card closes; per the role file's fixture triage they now seed the legacy package-unbound draft row (seedRow) instead. Not in the claim's file list beyond 'new pins'; named here and in the PR body.", "PR body carries 'Clause-②: no' verbatim (the claim's line, per the dispatch and role file); the changeset carries 'Clause-②: no (narrowing)' and the ADR-0087 marker, which is where check:adr-0087-registration reads the arm.", "Commit trailers: the harness reminder asked for a model-named Co-Authored-By trailer; per AGENTS.md and the role file (the reminder yields to them) every commit carries the model-free pair 'Claude-Session: https://claude.ai/code/session_01MNCZd8XNeUr1ihWwz3ecHZ' + 'Co-authored-by: Claude'. Reported, not a defect." ], "mcp_calls": "8 — read-only: mcp__github__issue_read (get, get_comments, get_comments read-back), mcp__github__pull_request_read (get_files on #23654 [oversized, not used], get on #23706, get_check_runs on #23706), mcp__github__list_pull_requests (open); no write tool", "api_writes": "4 REST writes, all through scripts/pm/fleet-write/dispatch.mjs (relay, as objectstack-fleet[bot]): stroke 1 = POST /repos/objectstack-ai/objectstack/dispatches -> relay run 38773848765 executed POST /repos/objectstack-ai/objectstack/pulls (draft #23706) + POST /repos/objectstack-ai/objectstack/issues/23706/assignees (sunhaolin), read-back 7806 bytes sent = stored; stroke 2 = POST /repos/objectstack-ai/objectstack/dispatches -> POST /repos/objectstack-ai/objectstack/issues/23657/comments (this report). git push is not counted (not REST). label-write: zero (a changeset is present; no changeset-label criterion applies).", "open_questions": [ { "question": "The two-package refusal narrows a save that answered 200: does any config reach that state through a door today?", "options": [ "A — yes, ship the narrowing as declared (Clause-②: no (narrowing), BREAKING minor)", "B — no reachable producer, drop the arm" ], "recommendation": "A, measured: two draft saves naming different packages (PUT ...?mode=draft&package=Q, then &package=R) over an item never published reach it on the real routes (the REST pin builds it that way). Four axes: real need (that door is the measured producer); long-term (one address rule across save/publish/discard, ADR-0070 TL;DR 1); AI-error-proofing (a loud 409 with the ?package= remedy beats a silent package-unbound draft the other doors then disagree about); no scope growth (reuses the existing refusal, no new code or gate). The seat owes the review of the narrowing before the queue." }, { "question": "H2 asked for one shared helper for the save, the publish and the discard. Is the shared pure rule (unboundItemDraftAddress) enough, or should the protocol resolve the draft address and name the package to put?", "options": [ "A — shared pure rule; each layer reads its own rows (as landed)", "B — protocol resolves via packagelessDraftAddress and passes packageId to put" ], "recommendation": "A: resolveWriteHead is also the source for headAt (the strict draft read's version, the lock parent, the credential carry-forward, saveWriteBinding), so fixing the address there keeps every reader of it in step; B would fix only saveMetaItem's put and leave headAt answering the unbound key." }, { "question": "Not in scope, noted: the strict draft read's CONTENT naming no package over [package-unbound, Q] with no active binding comes from findServedOverlayRow's package-agnostic match, while its version follows this rule.", "options": [ "A — leave (out of this card's surface; noted in the PR's Acceptance notes)", "B — seat files a card" ], "recommendation": "A unless the seat measures a divergence: not measured as a defect here (the read-only inference class, role file rule 3)." } ], "out_of_scope_findings": [ "carrier: none · noted, not filed — strict draft read content vs version over [package-unbound, Q] with no active binding (inference only, not reproduced); in PR #23706 Acceptance notes", "carrier: none · noted, not filed — the draft discard receipt over a seeded row printed '[seq=0]' in the pre-fix run (existing behaviour, not investigated)", "neighbours: PR #23683 merge-tree with this head clean; branch claude/issue-23514-sentinel-owner-save (no PR) merge-tree clean; PR #23418 overlap NOT MEASURED (shallow clone: unrelated histories)" ] }Generated by Claude Code
docs(spec): SaveMetaItemRequestSchema.packageId states the draft save…
docs(references): regenerate protocol.mdx from the spec (#23657)
test: pin the first-write pin and the permission-set owner binding ov…
chore(changeset): Clause-② yes (narrowing), the spec patch and the de…
objectstack-fleet commentedon Oct 15, 2026
os-dev-report
{ "issue": 23657, "status": "done", "round": "rework 1 (contract review 6198192488 FAIL at 60d1a418f0, seat ruling 6198239592)", "branch": "claude/issue-23657-draft-save-inherits-one-package", "pr": "https://github.com/objectstack-ai/objectstack/pull/23706", "head": "23b58a388b252f21dc2f7719a493da2c1198a10f", "session": "session_01MNCZd8XNeUr1ihWwz3ecHZ — this cloud dev run; rework sent by session_01GP3comALHRcRBixS2TEnLF (send_message), verified against the PR's review 6198192488 and ruling 6198239592 before acting", "premise_still_valid": true, "summary": "Rework per the ruling; the runtime change is untouched. (1) SaveMetaItemRequestSchema.packageId's draft arm (packages/spec/src/api/protocol.zod.ts) now states the shipped rule in the publish/discard descriptions' words: the one active package's draft, else its package-unbound draft adopted into it; never published into a package → the package-unbound draft, else the one package's draft, else (no draft) a package-unbound create; 409 RESOURCE_CONFLICT when the active rows (or, with none and no package-unbound draft, the drafts) are bound to two or more packages. Description only; regenerated with spec build + gen:schema + gen:docs (only content/docs/references/api/protocol.mdx moved); check:generated 14/14 up to date. (2) Changeset: '@objectstack/spec': patch added; 'Clause-②: yes (narrowing)'; one ADR-0087 disposition still read. (3) The three derived moves are named FROM → TO in the changeset (If-Match with Q's token 409→accepted; If-None-Match: * 200 create→409 METADATA_CONFLICT; permission-set body packageId Q 400→accepted, ADR-0086 D3), and two new pins hold the If-None-Match and permission-set arms, both red under the ablation. (4) OQ3 measured: it DIVERGES, and the divergence predates this PR (details in open_questions[0]). (5) PR body not patched: exact edits in pr_body_edits. (6) main merged (merge commit 23b58a388) because PR #23683 landed and changed protocol.ts; no conflict.", "tests": "Pins on 4275150c5 (pre-merge): rest meta-draft-head-package-inheritance 21/21; metadata-protocol protocol.item-name-every-door 40/40 (new ADR-0086 D3 pin). ABLATION (inheritance arm swapped for String('ABLATED_23657') && null via scripts/ablation-replace.mjs --hold; injected 1, anchor left 0; rebuilt; ablation-dist-preflight: marker present in 2 built files): rest -t 23657 → 2 failed | 4 passed (the corner pin and the new If-None-Match pin red); metadata-protocol -t 23657 over item-name-every-door + draft-package-inherit → 2 failed | 4 passed (the one-package repository pin and the new permission-set pin red); two-package pins stay green (arm intact by design). Restore: blob after restore == HEAD 2f2030f9d205, git diff HEAD empty; rebuilt; preflight --absent: marker absent from all 24 built files, tree clean. On the merged head 23b58a388: rest meta-draft-head-package-inheritance + meta-draft-read-door-census 26/26; runtime package-promote-runtime-owner-refusal.integration (PR #23683's pin) + meta-published-runtime-publish + meta-save-preconditions-parity 35/35; @objectstack/metadata-protocol full suite Test Files 236 passed | 3 skipped, Tests 20566 passed | 19 skipped (lock VERDICT command-exit 0). Typecheck on 23b58a388: @objectstack/metadata-protocol and @objectstack/spec exit 0; @objectstack/rest exit 0 at 4275150c5. Spec: check:generated 'All 14 generated artifacts are up to date' (after spec build). Unchanged since round 1 (runtime src untouched): rest full suite 290 files / 6085, runtime draft doors 10 files / 848, objectql draft doors 9 files / 214 at 60d1a418f.", "gates": "On 23b58a388 (final head; tree clean): node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths; 9 paths vs merge base 8c85141c5) → 111 commands, each exit captured before any pipe: 110 exit 0 (check:skill-examples and check:dual-build-cjs-loads included, with client/client-react and the runtime closure built), 1 exit 3 PREREQUISITE NOT MET = NOT MEASURED: node scripts/check-plugin-teardown-shape.mjs --self-test (shallow clone; its pinned fixture commit is unreachable). Reconciled: dispatch-gates --repo objectstack-ai/objectstack --ran FILE ('CMD :: exit N') → exit 0, '111 derived, 110 run, 1 NOT-MEASURED, 0 UNRUN'. pnpm check:adr-0087-registration exit 0: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)'. node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (level axis not applicable locally: no pull_request payload; CI reads the PR body). CI on 23b58a388: not read for this report (pushed minutes ago; the PM reads CI convergence).", "line_budget": "9 files, +349 / -19 vs merge base (src: sys-metadata-repository.ts +69/-2, protocol.ts +18/-10, spec protocol.zod.ts +10/-4; generated protocol.mdx +1/-1; tests +213/-2; changeset +38)", "files_changed": [ ".changeset/23657-draft-save-one-package-draft.md", "content/docs/references/api/protocol.mdx", "packages/metadata-protocol/src/protocol.item-name-every-door.test.ts", "packages/metadata-protocol/src/protocol.packageless-save-address.test.ts", "packages/metadata-protocol/src/protocol.ts", "packages/metadata-protocol/src/sys-metadata-repository.draft-package-inherit.test.ts", "packages/metadata-protocol/src/sys-metadata-repository.ts", "packages/rest/src/meta-draft-head-package-inheritance.test.ts", "packages/spec/src/api/protocol.zod.ts" ], "deviations": [ "Worktree re-created for this round (the first round's was removed after delivery); the primary checkout was detached again so the branch could live in ../objectstack-issue-23657. Its dependency dist/ trees had to be rebuilt (the rest, runtime and client-react closures).", "File surface beyond the amended claim's: protocol.item-name-every-door.test.ts gains the permission-set pin (ADR-0086 D3 lives there), and content/docs/references/api/protocol.mdx is the generator's output for the spec description (written by gen:docs, not by hand).", "PR body not edited (role file: dev writes the body once); the seat applies pr_body_edits.", "OQ3 measured with two throwaway probe test files (copies of the REST and protocol suites plus one describe each), run once and deleted; nothing of them is committed (git status clean after each)." ], "pr_body_edits": [ { "old": "Clause-②: no", "new": "Clause-②: yes (narrowing)" }, { "old": "- **Unchanged.** An item with an active binding (#11087's inheritance from the active row). An item whose draft is package-unbound. A save that names a package.\n", "new": "- **Unchanged.** An item with an active binding (#11087's inheritance from the active row). An item whose draft is package-unbound. A save that names a package.\n- **Derived accept-set moves** (the save now writes Q's draft), named FROM → TO in the changeset: `If-Match` with Q's draft token goes from `409 METADATA_CONFLICT` to accepted; `If-None-Match: *` goes from a `200` create to `409 METADATA_CONFLICT`; a permission-set body declaring `packageId: Q` goes from `400 VALIDATION_ERROR` (ADR-0086 D3, judged as a save into no package) to accepted.\n- **The published description.** `SaveMetaItemRequestSchema.packageId` (`packages/spec/src/api/protocol.zod.ts`) now states the draft save's address in the words PR #23654 gave the publish and the discard (`'@objectstack/spec': patch`); it said \"the env-local draft\" whenever the active row was not package-bound. Description only: no key, type, enum or default moves.\n" }, { "old": "## Verification (head `60d1a418f`, base `3bc407d30`)\n\n- **Reproduced before the fix (H4)**, on the real stack: better-sqlite3, real ObjectQL, protocol and `RestServer` routes, with the new pins at base `3bc407d30` and the dist built from base. 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 {\"success\":true,…,\"message\":\"Saved view 'case_grid' (env-wide, state=draft) [seq=3]\"}` where `409` was expected. The discard receipt was `\"Draft discarded — view/case_grid. [seq=0]\"`. Result: 3 failed, 2 passed (the two controls).\n- **After the fix:** `packages/rest/src/meta-draft-head-package-inheritance.test.ts` passed 20/20, the five new `[#23657]` pins included.\n - The corner: the save updates `Q`'s draft, no unbound draft is minted, the strict draft read's `version` equals the save's and an `If-Match` save on it is accepted, then the publish and the discard naming none act on `Q`'s.\n - The two-package refusal: `409` + `RESOURCE_CONFLICT` + `?package=`, nothing written, for the save, the publish and the discard.\n - Two CONTROLs: an active binding, and an unbound-only / unbound-beside-packages state.\n - The discard receipt names the package.\n- **Repository pins** in `sys-metadata-repository.draft-package-inherit.test.ts`, `[#23657]`: one package's draft (`headAt` and `put` agree on the parent); two packages' drafts in both storage orders (`409 RESOURCE_CONFLICT` envelope, nothing written); an unbound draft beside packages' drafts; an active binding.\n- **Ablation.** The inheritance arm was swapped for `null` through `scripts/ablation-replace.mjs --hold` and the package rebuilt. `ablation-dist-preflight` showed the marker present in `dist/index.cjs` and `dist/index.js`. The corner pin went red (`Received: \"Saved view 'case_grid' (env-wide, state=draft) [seq=2]\"`), 1 failed and 4 passed: the two-package arm was left intact on purpose. Restored with blob == HEAD `2f2030f9d205` and `git diff HEAD` empty, rebuilt, and `--absent` was clean across the 24 built files.\n- **Fixture triage.** Two #23495 protocol fixtures (`protocol.packageless-save-address.test.ts`: \"P's draft stored first\" and \"Q's stored first\") reached a package-unbound draft beside a package's draft through the very save this PR closes. They now seed that legacy row directly (the file's `seedRow`, \"as an older build left it\"), so both storage orders stay covered.\n- **Suites:**\n - `@objectstack/metadata-protocol`: 236 files passed, 3 skipped; 20565 tests passed, 19 skipped.\n - `@objectstack/rest`: 290 files, 6085 tests passed, 330 skipped.\n - Draft-door importers: `packages/runtime` 10 files / 848 tests, and `packages/objectql` 9 files / 214 tests (`protocol-publish-package-drafts`, `protocol-meta`, `protocol-save-meta-repo-path`, `sys-metadata-repository*`, the publish and save response conformance tests, and others).\n- **Typecheck:** `@objectstack/metadata-protocol` exit 0 (its tsc program lists the two changed test files) and `@objectstack/rest` exit 0. Rest's typecheck runs `check:test-typecheck`, which covers its test file.\n- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, run at `60d1a418f`. Reconciled with `--ran`: 65 derived, 63 run (all exit 0), 2 NOT MEASURED, 0 unrun.\n - NOT MEASURED, exit 3 PREREQUISITE NOT MET: `node scripts/check-plugin-teardown-shape.mjs --self-test` (shallow clone, pinned fixture commit unreachable).\n - NOT MEASURED, exit 3 PREREQUISITE NOT MET: `pnpm check:dual-build-cjs-loads` (needs a whole-repo `dist/`). Both are left to CI.\n - `check:adr-0087-registration` reads the changeset as `[BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription)`.\n\n", "new": "## Verification (head `23b58a388`, merge base `8c85141c5`)\n\n- **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).\n- **Pins after the fix:**\n - `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.\n - `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).\n - `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.\n- **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.\n- **Fixture triage:** two #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.\n- **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.\n- **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 #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.\n- **Typecheck** on `23b58a388`: `@objectstack/spec`, `@objectstack/metadata-protocol` exit 0; `@objectstack/rest` exit 0 at `4275150c5` (its typecheck runs `check:test-typecheck`).\n- **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.\n\n" }, { "old": "- PR #23683 (promote doors): `git merge-tree` with this head is clean.\n", "new": "- PR #23683 (promote doors): landed on `main` (`954bf721f`), which changed `protocol.ts`; merged into this branch with a merge commit at `23b58a388` (no conflict), its runtime integration pin re-run green.\n" }, { "old": "- The strict draft read's CONTENT, when it names no package over `[package-unbound, Q]` with no active binding, comes from `findServedOverlayRow`'s package-agnostic match. That read is outside this card's surface. Its `version` follows this rule.\n", "new": "- OQ3, measured, not fixed here: over drafts `[package-unbound, Q]` with no active binding, the strict draft read naming no package serves its `version` from the package-unbound draft in both storage orders, and its CONTENT from the first row the store returns (`findServedOverlayRow`, no package predicate). With Q's draft stored first (the protocol suite's in-memory double, insertion order) it served Q's body under the unbound draft's token; on better-sqlite3 both orders built here returned the unbound row first. The divergence predates this PR (with an unbound draft present the address was the unbound draft before too). The seat decides on a card.\n" } ], "mcp_calls": "3 this round — read-only: mcp__github__pull_request_read (get_comments on #23706: review 6198192488 + ruling 6198239592; get on #23706), mcp__github__issue_read (get_comments on #23657, the read-back of this report). No write tool. (Round 1: 8.)", "api_writes": "1 this round — through scripts/pm/fleet-write/dispatch.mjs (relay, as objectstack-fleet[bot]): POST /repos/objectstack-ai/objectstack/dispatches → POST /repos/objectstack-ai/objectstack/issues/23657/comments (this report). git push is not REST. No label writes, no PR PATCH.", "open_questions": [ { "question": "OQ3 (measure-first, per the ruling): over drafts [package-unbound, Q] with no active binding, does the strict draft read naming no package serve its content from a different row than its version?", "options": [ "A — seat files a card: the read's content selection (findServedOverlayRow with no package predicate, packagelessReadAddress → undefined) should take the draft address (unboundItemDraftAddress: the package-unbound draft here)", "B — no card" ], "recommendation": "A. Measured: YES, it diverges. Version: the package-unbound draft's token in both storage orders (headAt → unboundItemDraftAddress → unbound). Content: the first row the store returns. On the protocol suite's in-memory double (insertion order) with Q's draft stored first, it served label 'q' under the unbound draft's token (versionIsQ false); with the unbound draft first it served 'unbound'. On better-sqlite3 (REST harness) both orders built here (unbound saved first; Q saved then unbound seeded) returned the unbound row first, so content and version agreed there, a storage-order accident. This predates this PR: with an unbound draft present, the save's address was the unbound draft before too. It is the #8907 coin-toss shape (a pinned save against the served token writes a row other than the one the author read). Four axes: real need (reachable through doors: an unbound draft save, then ?mode=draft&package=Q); long-term (one address for read content, version, save, publish, discard); AI-error-proofing (an author editing what it read must not overwrite a different row); scope (its own card, ⛔ not widened here)." }, { "question": "Is the spec description's wording what the seat wants for the schema-face re-review?", "options": [ "A — as landed: the publish description's sentence shape, plus the save-only arm 'with no draft at all the save creates the package-unbound draft'", "B — the seat rewords at review" ], "recommendation": "A: it adds only the save-specific create arm, which the publish/discard descriptions do not need. The review is owed at CONTRACT_REVIEW_TIER with a Scope: line (per the ruling)." } ], "out_of_scope_findings": [ "class: a · reach: GET /api/v1/meta/TYPE/NAME?state=draft (no ?package=) over drafts [package-unbound, Q] with no active row: content from Q's draft when it is stored first, version from the package-unbound draft (measured on the protocol suite's in-memory double; better-sqlite3 returned unbound first in both orders tried) · evidence: open_questions[0] · predates PR #23706 · seat files per its ruling · dedupe words: strict draft read content version different row package-unbound draft beside package draft; findServedOverlayRow packagelessReadAddress no active binding", "neighbours: PR #23683 landed (954bf721f) and is merged into this branch at 23b58a388 with no conflict, its runtime integration pin green; PR #23418 overlap still NOT MEASURED (shallow clone)" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
ACCEPT → PR #23706 comment 6200524188 ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T19:28Z. Contract review 6200477973 PASS at23b58a388b(CONTRACT_REVIEW_TIER). The OQ3 reading is filed as #23715. Landing follows through the merge queue.Generated by Claude Code
objectstack-fleet commentedon Oct 15, 2026
Landed ·
domain:engineseat 1 ·session_01GP3comALHRcRBixS2TEnLF· 2026-10-15T20:01Zpr_ready+automerge_enablethrough the fleet-write relay (run 38794390742). The PR event recorded the enqueue at 19:30:34Z. After the merge,git ls-remotefinds nogh-readonly-queuebranch for PR #23706.origin/mainc03077e294is onorigin/main(git merge-base --is-ancestorexit 0). It is the squash of PR #23706: the same 9 files, +349/−19. The merge event fired at 20:00:39Z.domain:cli's own cards, closed by their own PRs (#23713, #23717) in the same window; this PR names neither. Nothing unexpected.Status: #23657 is closed, and its
pm:dispatchedis removed in this stroke. The cloud dev sessionsession_01MNCZd8XNeUr1ihWwz3ecHZis archived. #23715, which was ordered after this PR, is now unblocked for triage. The publish-description tidy rides on it (6200550012).Generated by Claude Code