Skip to content

fix(metadata-protocol)!: a delete naming one package keeps the item's other rows registered, and a package-less draft save of an item two packages hold is refused 409 - #23408

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23297-package-axis-delete-draft
Oct 14, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-23297-package-axis-delete-draft

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23297
Clause-②: yes (narrowing)

What this does

Three package-axis gaps that the review of PR #23290 left open in packages/metadata-protocol. Each was measured before the fix (item 1 on a booted showcase), pinned, and ablated.

1. The registry heal after a delete that names one package. restoreArtifactRegistryView now asks the store first. If a row of the item is still stored at the address after the delete (the environment's active rows, canonical type, the member, any package), that row is registered again through applyRegistryWriteThrough, in the boot hydration's order and with its sealed-object skip, and nothing is retired. For an object, the registry keeps one owner per name (ADR-0029 D3). So when the registered owner is the deleted row's package and no code package ships the name, that owner is unregistered first, and the remaining row can then take the name. The existing extender guard and warning are kept. Deleting the last row still retires the item.

  • Before (booted showcase, SQLite, origin/main 6eb6f7d65): after PUT /api/v1/meta/object/qa_probe_gadget?package=com.qa.p, a PUT with ?package=com.qa.q, then DELETE ...?package=com.qa.p, the receipt said "it still exists: its row bound to package 'com.qa.q' remains". GET /api/v1/meta/object/qa_probe_gadget answered 200 with Q's row. GET and POST /api/v1/data/qa_probe_gadget answered 404 OBJECT_NOT_FOUND, still 404 after a list call. The dev log showed Removed overlay entry and Unregistered object ... owner com.qa.p.
  • After (same sequence, rebuilt dist): the data door answers GET 200 and POST 201.

2. A draft save that names no package, of an item two packages hold. SysMetadataRepository.resolveWriteHead used to read one active row with a package-agnostic findOne for the draft inheritance (#11087). It now reads every active row of the address.

  • Rows bound to one package: the draft inherits that package in any storage order. A package-less row beside it is not a second package, the rule the direct save follows since 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.
  • Rows bound to two or more packages: the resolution names no package. put refuses before anything is written, with 409 RESOURCE_CONFLICT and the ?package= remedy, which is the same refusal the direct save and the delete answer. headAt answers the package-unbound draft key, so a draft read's version never names a package picked by storage order.
  • The refusal function moved from protocol.ts into the repository module, exported but not from the package entry, under the same name. It is one sentence for all three doors, and protocol.ts imports it.
  • Before (booted showcase): PUT /api/v1/meta/object/X?mode=draft with no ?package= answered 200 "(env-wide, state=draft)". The draft landed in the package whose id sorts first (SQLite's index order), whichever package was saved first. That held in five runs over two package pairs.
  • After: 409 RESOURCE_CONFLICT: "Name the package this save is for, then send it again: ... object 'qa_probe_draft_pq' belongs to 2 packages ('com.qa.p', 'com.qa.q'), and a save that names none has no single row to write." No draft is written. The one-package control still saves into its package.

3. The delete's _lock gate takes the package the delete names, as the save gate does (assertLockAllowsDelete now carries packageId through to getEffectiveLock).

Where the edits sit

  • packages/metadata-protocol/src/sys-metadata-repository.ts: resolveWriteHead and its docblock, put's refusal, and the moved packagelessAddressAmbiguityRefusal.

  • packages/metadata-protocol/src/protocol.ts: restoreArtifactRegistryView (a new tier ahead of the retirement tiers), assertLockAllowsDelete and its one call site in deleteMetaItem. Two more hunks lie outside the claim's declared regions:

    • the ./sys-metadata-repository.js import line, plus the removal of the moved refusal (formerly around line 1224);
    • one stale comment line in packagelessAddress's docblock ("resolveWriteHead, unchanged").

    None overlaps PR fix(automation,metadata-protocol,rest,spec): a stored flow the engine refuses is served beside its verdict, not as running #23358's regions or PR fix(spec,objectql)!: a permission set's body packageId that disagrees with the package shipping it is refused at the build and registration doors; both readers read the binding only #23375's (eeb95f7ff, merged in, with no conflict).

  • Tests: protocol.packageless-save-address.test.ts and sys-metadata-repository.draft-package-inherit.test.ts (item 2); objectql/src/protocol-delete-object-registry-heal.test.ts (item 1, real SchemaRegistry); objectql/src/protocol-lock-artifact-package-axis.test.ts (item 3, real SchemaRegistry). Each extends an existing file and its existing engine double, so no new double is added.

  • Two existing pins follow the change:

    • protocol.object-registry-write-through-spelling.test.ts: the applyRegistryWriteThrough call-site count goes from 6 to 7. The new caller is traced in the header: it folds through canonicalMetaType.
    • protocol.view-container-reset-expansions.test.ts: its org-scope case called the heal directly while the published row was still stored. It now removes the row the reset removes before healing, because a still-stored row is now what the heal registers.

Tests

Every reading below is from a77f32b92 (the first main merge) unless a line says otherwise. The second merge (ae6b35c16, PR #23375 and two others) is re-verified in the last bullet.

  • Before the fix, the new pins on unmodified code:
    • metadata-protocol: 8 failed of 49. These are the two-package draft refusals (both orders, door and repository), headAt naming no package (both orders), and the package-less-row-first inheritance. The package-first control and the naming-one-package case passed.
    • objectql: 7 failed of 35. These are 4 object cases (tenant and control-plane kernels, both save orders), the dashboard case, and 2 lock-prose cases (delete naming B). Case 4 of the lock suite ("delete naming A is refused as the save naming A is") and the heal CONTROL case passed before the fix.
  • After the fix:
    • pnpm --filter @objectstack/metadata-protocol test: 235 files passed (3 skipped), 28375 tests passed.
    • pnpm --filter @objectstack/objectql test: 414 files, 8083 tests passed, run against rebuilt metadata-protocol dist.
    • pnpm --filter @objectstack/rest test: 281 files, 5845 passed (326 skipped).
    • pnpm --filter @objectstack/client test: 51 files, 663 passed.
    • pnpm --filter @objectstack/metadata-protocol typecheck: OK. --listFiles shows all four changed test files are compiled.
    • pnpm --filter @objectstack/objectql typecheck (including check:test-typecheck): OK.
  • Ablations: each run on a committed fix through node scripts/ablation-replace.mjs in wrap mode. The anchor went 1 to 0 and the blob changed. The tool restored the file and proved blob == HEAD with an empty git diff HEAD.
    • Item 3 (the delete gate drops the package): rebuilt; ablation-dist-preflight found the marker in 2 dist files; 2 red (delete naming B under both orders).
    • Item 1 (the remaining-rows tier switched off): rebuilt; marker in dist; 5 red; CONTROL green.
    • Item 2 (put's refusal switched off): source-resolved suites, so no dist step; 4 red (both orders, door and repository).
    • Restore legs: rebuilt, then --absent confirmed the marker gone from all 24 dist files with a clean tree. Everything green again: 20 and 15 objectql tests, 49 metadata-protocol tests.
  • Gate battery: node scripts/pm/dispatch-gates.mjs --commands with no paths (67 commands) at a77f32b92. 66 exit 0, among them check:adr-0087-registration (exemption no-migration-prescription noted), check:changeset-no-major, check:empty-changeset, check:durability-log-level (89 read seams, none invents an answer), check:engine-double-contract, check:nul-bytes, check:test-source-alias and check:cross-package-test-inputs. NOT MEASURED: type-check-debt, reason: it exceeded the 420 s per-gate budget of the local battery (exit 124); CI runs it.
  • Merged tree ae6b35c16 (main with PR fix(spec,objectql)!: a permission set's body packageId that disagrees with the package shipping it is refused at the build and registration doors; both readers read the binding only #23375, PR feat(service-storage): the file-attachments service, a server-side verb that stores bytes and attaches them to a record #23386 and PR fix(spec): the two step-18 flow-binding D3 entries name only a text-slot hole and a CEL expression as the reads to rename #23384; no conflict, and none touches the edited regions):
    • Re-derived gate list: the same 67 commands; all 67 exit 0. That includes check:type-check-debt (re-measure OK, 250.8 s, "none above its recorded number") and the rest of the list above.
    • dispatch-gates --ran reconciliation: 67 derived, 67 run, 0 unrun.
    • pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.
    • The four suite re-runs (metadata-protocol, objectql, rest, client) are in progress behind the shared verify lock. Their verdicts go in the os-dev-report comment on the card, not here.

Acceptance notes

  • Item 1, a visible side effect on objects: after the per-package delete, a read naming the deleted package (GET /api/v1/meta/object/X?package=P) now falls through to the registry's entry and serves Q's object with 200, where it answered 404. A boot would produce the same, because the registry holds one object per name.
  • Item 2, refusal order: a draft save carrying an If-Match taken from a ?package=P draft read, but sent without ?package=, meets the METADATA_CONFLICT token check before the repository's RESOURCE_CONFLICT. Likewise, a permission set whose body names a package meets the owner-binding judge first. Both are refused either way.
  • Item 2, flow credentials: a flow draft with an explicit credential stores it in the write-only channel before put refuses. That is the channel's documented put-failure behavior.
  • Draft receipts: a package-less draft save's receipt still reads "(env-wide, state=draft)" without naming the inherited package.
  • Observations read from source and not measured: rollbackMetaItem and restoreVersion read the active row with no package predicate, as does the legacy raw-engine delete path. These are not filed.
  • Measured, not fixed here (reported to the seat as a finding): DELETE /api/v1/meta/object/qa_probe_drop?package=com.qa.p&dropStorage=true, on an object whose com.qa.q row remains, answered 200 "and it still exists: its row bound to package 'com.qa.q' remains". It still dropped the table: the record created before the delete is gone, GET /api/v1/data/qa_probe_drop answers 500 DATABASE_ERROR, and POST answers 404 OBJECT_NOT_FOUND. shouldDropStorage does not ask whether a row of the object remains. Two remedies are possible (skip the drop and say so, or refuse the delete), so it is not a mechanical in-place fix.
  • Measured, not fixed here (reported to the seat as a finding): PUT /api/v1/meta/object/X?package=Q of a runtime object that package P already owns answers 200 "Saved object 'X' (env-wide, package=Q)", while the registry refuses Q's layer: the dev log says registerObject failed ... is already owned by package "com.qa.p", at warn. ADR-0029 D9.9's producer refusal (OBJECT_OVERLAY_PACKAGE_MISMATCH) only asks about a packaged (code) owner.

Generated by Claude Code

…the delete and draft doors

Pins written before the fix, so the unmodified code gives each one's
failing leg: the per-package delete's registry heal, the draft save's
package-less address with two packages, and the delete lock gate's
package address.

Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy
Co-authored-by: Claude <noreply@anthropic.com>
… draft doors

- The registry heal after a delete asks the store first: a row of the item
  still stored at the address (another package's, or the package-less one)
  is registered again and nothing is retired, so data CRUD and the registry
  lookups keep serving an item the item read serves.
- A draft save that names no package reads every active row of the address:
  bound to one package it inherits it in any storage order, bound to two or
  more it is refused 409 RESOURCE_CONFLICT with the ?package= remedy, the
  refusal the direct save and the delete answer (moved into the repository
  module so the three doors share one sentence).
- The delete's _lock gate takes the package the delete names, as the save
  gate does: the refusal carries that package's reason.

Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy
Co-authored-by: Claude <noreply@anthropic.com>
…s remaining-rows read

Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy
Co-authored-by: Claude <noreply@anthropic.com>
…s follow the new write-through caller

The registry heal's remaining-rows tier reads and registers under the
canonical key the repository stores rows under. The applyRegistryWriteThrough
call-site pin traces the seventh caller, and the view-container heal fixture
removes the row the reset removes before healing.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 11 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 5656c5638abfcee5081571e6c7c5f5c8998a1812.

⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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 — 11 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 5656c5638abfcee5081571e6c7c5f5c8998a1812 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 5656c5638abfcee5081571e6c7c5f5c8998a1812

⚠️ 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 5656c5638abfcee5081571e6c7c5f5c8998a1812 → 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: ae6b35c1632000f5cdd3a4146960ce339b76c0de
Local-runs: none

Inputs: card #23297 (body and all five comments: triage 6149701838, unlock 6149850778, the claim 6160266617 as amended in place, the os-dev-report 6162849757, the seat ACCEPT 6162928143); PR #23408 (body, nine-file list, net diff against main at the head: +533 / -58); the 40 check-runs on the head; committed source read with git show / git grep against origin/claude/issue-23297-package-axis-delete-draft and origin/main (merge base e4ec6ec1a). Nothing built, run, re-run or ablated.

Check-runs on the head, as found (40, all completed; 0 failure, 0 in progress). The five required contexts: Lint & Repo Gates success · TypeScript Type Check success · Test Core success (shards 1/6 to 6/6 each success) · Build Core success · Governed Surface Queue Guard success. The rest, success: Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Lint · CI release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, The card this PR closes must claim this branch, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, filter. Skipped by path filter (9): Build Docs, Console Pin Gate, Dogfood Regression Gate matrix shards, Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance (live PG + MySQL).

① Derived judgments

Item 1 — the registry heal after a per-package delete (a widening of what the registry-backed and data doors serve after such a delete): RIGHT.

  • Order in deleteMetaItem on the head: repo.delete (the repository removes the row physically with engine.delete('sys_metadata', …)), then restoreArtifactRegistryView, then shouldDropStorage / dropObjectStorage. The new tier (protocol.ts 20395 to 20477) reads sys_metadata at type: canonical, name, state: 'active', organization_id: null plus the bundle-member predicate. The deleted row is gone and a draft row is not active, so the tier can re-register only rows still stored; the method returns at its top for any organization-scoped delete, so only an env-wide delete reaches it. It never resurrects the deleted item: the CONTROL pin (both rows deleted: getObject and getItem undefined, data CRUD OBJECT_NOT_FOUND 404) is in the diff and was green before and after the fix per the dev's before-leg.
  • "Only what is stored, as a boot registers it": the rows go through applyRegistryWriteThrough under canonicalMetaType, built with convertStoredItem and storedRowDocument, with the sealed-object skip spelled canonical === 'object' && isUnservedSealedOverlay — the same object-only skip the boot loop applies (loadMetaFromDb: if (normalizedType === 'object') continue;). hydrateOverlayIntoRegistry's own guards still apply on this path (an organization-scoped row is not registered; a bound view another package ships is declined, finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057), and on a kernel whose registry does not take stored rows the non-object write-through registers nothing and the tier returns without retiring — the entry was never there on such a kernel, so nothing moves. A read failure stops the walk through rethrowUnlessMetadataStoreUnprovisioned; an unprovisioned store reads as no row.
  • The stale-owner unregister: the limb runs only for canonical === 'object' && !isArtifactBacked(canonical, name); isArtifactBacked answers from the artifact-only lookup (a genuine code package _packageId, the 'sys_metadata' sentinel excluded), so a code-shipped object is never unregistered here. bindings keys a package-less row as 'sys_metadata', the sentinel classifyObjectContribution registers a package-less runtime owner under, so a package-less remaining row keeps a package-less owner and only an owner outside the remaining bindings is removed. registry.unregisterObject throws on foreign extenders; the catch warns and returns with nothing retired, the tier-3 limb's own disposition. Rows are then re-registered in the store's return order, the order a boot reads them in, and registerObject keeps one owner per name as it does at boot.
  • The side effect the PR body declares (a read naming the deleted package now falls through to the registry entry and serves the remaining row) follows from one owner per name; declared in the acceptance notes, acceptable.
  • The two existing pins that moved: the write-through call-site count is 7 on the head (counted), the seventh being this tier, traced in the pin's header; the view-container fixture now removes the active row before calling the heal directly, which is the real call order, a fixture correction and not a weakened assertion.

Item 2 — a package-less draft save of an item two or more packages hold is refused 409 RESOURCE_CONFLICT with the ?package= remedy; a one-package item still inherits: RIGHT.

  • resolveWriteHead (repository 814 to 876) reads every active row with the same activeWhere the old single-row read used (two-scope reach for an organization-scoped repository, env-wide otherwise), collects non-empty string package_id values (the same test the old code applied to the one row it read, so a package-less row beside a package's is not a binding), inherits on exactly one binding, and on two or more leaves targetPackageId null and reports ambiguousPackages sorted. put throws packagelessAddressAmbiguityRefusal('save', …) immediately after that resolution inside withTxn, ahead of the optimistic lock, the no-op check and every insert, update and history write: nothing is written (pinned: row count and history length unchanged, both storage orders, door and repository). headAt ignores ambiguousPackages and answers the package-unbound draft key with packageId: null (pinned both orders). The one-package control with a package-less active row beside it is pinned in both storage orders at the door and the repository; naming one package saves that package's draft (pinned).
  • The move of packagelessAddressAmbiguityRefusal into sys-metadata-repository.ts keeps its words: I diffed the function on main (protocol.ts 1181 to 1223) against the head (repository 567 to 588) — identical apart from indentation and the export keyword, the verb-keyed 'save' / 'delete' sentences included. protocol.ts imports it and its two call sites (the direct save at 21769, the delete at 28476) are unchanged. index.ts exports a named list from that module (SysMetadataRepository, resetEnvWritableMetadataTypes and five types) and package.json exposes only the . entry, so the function reaches no public entry: no public-surface change.
  • One scoping difference, checked and judged acceptable: the direct save's packagelessAddress reads env-wide rows only, while the draft inheritance keeps its 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 two-scope reach. For an env-wide caller the two doors read the same rows and answer the same refusal. For an organization-scoped caller the draft door now counts the organization's own bound row as a binding; that row was already one the old findOne could return, so the change is from a storage-order pick to a refusal, with no new row brought into scope — the narrowing the card asked for.

Item 3 — the delete's lock gate hands the named package to getEffectiveLock; the dev's measurement that the verdict is unchanged and only the refusal prose moves: UPHELD.

Negative controls: item 1, the heal tier off: 5 red, CONTROL green; item 2, put's refusal off: 4 red; item 3, the package dropped from the gate call: 2 red. Each is the pin that fails without its fix, as the claim required.

② Semver level

  • Changeset: @objectstack/metadata-protocol: minor, a **BREAKING** banner, title fix(metadata-protocol)!:. The package publishes (no private flag; 18.0.0-next.0), and it is the only published package whose source moves — the two packages/objectql files are tests. minor for a breaking change is the launch-window convention check-changeset-no-major.mjs enforces, and its level axis (a yes declaration must grade at least one moved published package minor or above) is met. Check Changeset concluded success on the head.
  • ADR-0087 disposition not-required (no-migration-prescription): right for this diff. No authorable spelling, Zod schema, stored row or exported declaration moves: the moved function is not on the package entry, assertLockAllowsDelete and resolveWriteHead are private, and deleteMetaItem's request already carried packageId. What narrows is a runtime write door whose remedy is a request parameter, nothing objectstack migrate meta could rewrite; what widens is the registry after a delete. The gate's refusal condition (a migration prescription in the changeset body) does not fire: the FROM / TO paragraphs describe door behaviour, not a code rewrite. The sibling precedent on main, .changeset/23165-direct-save-resolves-package.md, carries the same level, the same arm and the same disposition for the same door family.
  • Clause-②: yes (narrowing): right. scripts/pm/clause2-line.mjs defines that arm as a diff that widens one surface and narrows another, both facts read. Item 1 widens (the registry-backed and data doors serve a remaining row after a per-package delete where they answered 404), item 2 narrows (200 to 409 on the draft door), item 3 admits nothing. The claim as amended in place at ACCEPT, PR body line 2 and the changeset carry the same spelling; PR body line 1 is Fixes #23297 with no other closing keyword beside a card number.

③ Boundary flags

Dev deviations (six), each answered or informational:

  1. Clause-② read yes in the claim and yes (narrowing) in the PR body and changeset: answered — the seat amended the claim in place; all three carriers now agree (verified above).
  2. File surface beyond the claim's regions (the import line, the moved refusal, one docblock line in packagelessAddress; two existing pins; two objectql test files): answered — the seat amended the claim; the nine-file list matches the amended surface, the three protocol.ts extras are exactly those hunks, the objectql files are tests only.
  3. One local test run killed by the dev's own timeout wrapper and discarded, then re-run: informational — the head's Test Core fold and its six shards concluded success.
  4. check:type-check-debt exit 124 at a77f32b92, exit 0 at the head: answered by Type Check · debt ledger success on the head.
  5. The dropStorage probe ran with a stale cli dist: informational — metadata-protocol, objectql and spec dist were rebuilt, which is the path the measured behaviour takes; the result is filed as metadata-protocol: a delete naming one package with dropStorage=true drops the object table while another package row of the object remains — the remaining package object loses its data #23415.
  6. Model-free commit trailers: consistent with the AGENTS.md commit-message rule.

open_questions item 3 (A: keep the ruled lock rule; B: a package-addressed lock), answered A by the seat with no follow-up card: inside the card's triage direction. Triage's direction for item 3 reads, verbatim in effect, "hands packageId through to getEffectiveLock, at the save gate's parity" — which is what shipped. Triage's lane reasoning (that parity admits a request the door refuses today) was the card's premise, now measured false; the lane consequence (Clause-②: yes, contract-tier review) still holds through item 1, so nothing in the grade or lane turns on it. B would reverse #21803, a ruling of weeks ago, and restate ADR-0010 §3.3's one-answer lock per package, on the doors and the reads together — a new decision, not this card's to take, and the dev measured zero item locks declared in examples/ at the head. A keeps door and reads in lockstep; not filing B without a named need is right.

Out-of-scope findings, filed by the seat as #23415 and #23416 — neither is a defect this diff makes false or owns:

The two "carrier: none" observations (rollback, restore and the raw-engine delete read the active row with no package predicate; the package-less draft receipt does not name the inherited package) are read from source, not measured; the seat placed them in the acceptance notes, which is Prime Directive 10's disposition for a note that is not yet a filed defect.

Landing class: none of the nine paths is on the governed register (Governed Surface Queue Guard success); 591 changed lines; head repository equals base repository; author objectstack-fleet[bot]; draft, base main.

Implemented-by: claude/issue-23297-package-axis-delete-draft
Reviewed-by: session_01YRp5z4kK6vgYkrdH97ztsy

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 14, 2026 07:33
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 14, 2026 07:33
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 14, 2026
Merged via the queue into main with commit 2de9c03 Oct 14, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23297-package-axis-delete-draft branch October 14, 2026 07:58
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/l tests tooling

Projects

None yet

2 participants