Repository navigation
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
Conversation
…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>
Claude-Session: https://claude.ai/code/session_01YRp5z4kK6vgYkrdH97ztsy Co-authored-by: Claude <noreply@anthropic.com>
…ckage-axis-delete-draft
…ckage-axis-delete-draft
📓 Docs Drift CheckThis PR changes 1 package(s): 17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # 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
|
Contract reviewServed-tier: Inputs: card #23297 (body and all five comments: triage Check-runs on the head, as found (40, all ① Derived judgmentsItem 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.
Item 2 — a package-less draft save of an item two or more packages hold is refused
Item 3 — the delete's lock gate hands the named package to
Negative controls: item 1, the heal tier off: 5 red, CONTROL green; item 2, ② Semver level
③ Boundary flagsDev deviations (six), each answered or informational:
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 ( Implemented-by: VERDICT: PASS Generated by Claude Code |
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.
restoreArtifactRegistryViewnow 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 throughapplyRegistryWriteThrough, in the boot hydration's order and with its sealed-object skip, and nothing is retired. For anobject, 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.origin/main6eb6f7d65): afterPUT /api/v1/meta/object/qa_probe_gadget?package=com.qa.p, aPUTwith?package=com.qa.q, thenDELETE ...?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_gadgetanswered 200 with Q's row.GETandPOST /api/v1/data/qa_probe_gadgetanswered404 OBJECT_NOT_FOUND, still 404 after a list call. The dev log showedRemoved overlay entryandUnregistered object ... owner com.qa.p.dist): the data door answersGET 200andPOST 201.2. A draft save that names no package, of an item two packages hold.
SysMetadataRepository.resolveWriteHeadused to read one active row with a package-agnosticfindOnefor the draft inheritance (#11087). It now reads every active row of the address.putrefuses before anything is written, with409 RESOURCE_CONFLICTand the?package=remedy, which is the same refusal the direct save and the delete answer.headAtanswers the package-unbound draft key, so a draft read'sversionnever names a package picked by storage order.protocol.tsinto the repository module, exported but not from the package entry, under the same name. It is one sentence for all three doors, andprotocol.tsimports it.PUT /api/v1/meta/object/X?mode=draftwith no?package=answered200 "(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.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
_lockgate takes the package the delete names, as the save gate does (assertLockAllowsDeletenow carriespackageIdthrough togetEffectiveLock).protocol-lock-artifact-package-axis.test.tscase 1 ("request naming package A ... save refused ITEM_LOCKED").'full', a delete naming B carried "Packaged lock of com.example.a.". After: "Packaged lock of com.example.b.", as the save naming B does.?package=Pwhen only Q is locked would make the delete door looser than both reads naming P, which reportlock: 'full', deletable: false. It would also reverse a ruled design, so that question goes to the maintainer through the report, not this PR.Where the edits sit
packages/metadata-protocol/src/sys-metadata-repository.ts:resolveWriteHeadand its docblock,put's refusal, and the movedpackagelessAddressAmbiguityRefusal.packages/metadata-protocol/src/protocol.ts:restoreArtifactRegistryView(a new tier ahead of the retirement tiers),assertLockAllowsDeleteand its one call site indeleteMetaItem. Two more hunks lie outside the claim's declared regions:./sys-metadata-repository.jsimport line, plus the removal of the moved refusal (formerly around line 1224);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.tsandsys-metadata-repository.draft-package-inherit.test.ts(item 2);objectql/src/protocol-delete-object-registry-heal.test.ts(item 1, realSchemaRegistry);objectql/src/protocol-lock-artifact-package-axis.test.ts(item 3, realSchemaRegistry). 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: theapplyRegistryWriteThroughcall-site count goes from 6 to 7. The new caller is traced in the header: it folds throughcanonicalMetaType.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 firstmainmerge) unless a line says otherwise. The second merge (ae6b35c16, PR #23375 and two others) is re-verified in the last bullet.metadata-protocol: 8 failed of 49. These are the two-package draft refusals (both orders, door and repository),headAtnaming 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.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 rebuiltmetadata-protocoldist.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.--listFilesshows all four changed test files are compiled.pnpm --filter @objectstack/objectql typecheck(includingcheck:test-typecheck): OK.node scripts/ablation-replace.mjsin wrap mode. The anchor went 1 to 0 and the blob changed. The tool restored the file and provedblob == HEADwith an emptygit diff HEAD.ablation-dist-preflightfound the marker in 2 dist files; 2 red (delete naming B under both orders).put's refusal switched off): source-resolved suites, so no dist step; 4 red (both orders, door and repository).--absentconfirmed the marker gone from all 24 dist files with a clean tree. Everything green again: 20 and 15 objectql tests, 49 metadata-protocol tests.node scripts/pm/dispatch-gates.mjs --commandswith no paths (67 commands) ata77f32b92. 66 exit 0, among themcheck:adr-0087-registration(exemptionno-migration-prescriptionnoted),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-aliasandcheck: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.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):check:type-check-debt(re-measure OK, 250.8 s, "none above its recorded number") and the rest of the list above.dispatch-gates --ranreconciliation: 67 derived, 67 run, 0 unrun.pnpm --filter @objectstack/metadata-protocol typecheck: exit 0.os-dev-reportcomment on the card, not here.Acceptance notes
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.If-Matchtaken from a?package=Pdraft read, but sent without?package=, meets theMETADATA_CONFLICTtoken check before the repository'sRESOURCE_CONFLICT. Likewise, a permission set whose body names a package meets the owner-binding judge first. Both are refused either way.putrefuses. That is the channel's documented put-failure behavior.rollbackMetaItemandrestoreVersionread the active row with no package predicate, as does the legacy raw-engine delete path. These are not filed.DELETE /api/v1/meta/object/qa_probe_drop?package=com.qa.p&dropStorage=true, on an object whosecom.qa.qrow 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_dropanswers500 DATABASE_ERROR, andPOSTanswers404 OBJECT_NOT_FOUND.shouldDropStoragedoes 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.PUT /api/v1/meta/object/X?package=Qof a runtime object that package P already owns answers200 "Saved object 'X' (env-wide, package=Q)", while the registry refuses Q's layer: the dev log saysregisterObject 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