Repository navigation
fix(packages): POST /packages/:id/revert reverts a Studio-authored package instead of answering 404 - #22112
Conversation
POST /packages/:id/revert asked only the metadata service, whose in-memory registry never holds a package's sys_metadata rows, so a Studio package with published items and a pending draft answered 404 "No metadata items found". The route now asks the protocol first: revertStoredPackage reads the package's stored rows through the predicate listDrafts uses (now one shared packageScopedRowWhere), refuses a never-published package with the declared 409, and otherwise discards the drafts through the per-draft step discardPackageDrafts takes. A package with no stored row falls through to the metadata service's revert, unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
…es; changesets Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi
…vert-package-stored-members
📓 Docs Drift CheckThis PR changes 2 package(s): 18 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 — 31 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 c34382ae99db560773450dd6d82e5ca4c253c9dd && git checkout c34382ae99db560773450dd6d82e5ca4c253c9dd
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin aa71c4d9d146861fc09d63363e4c8fb8286719fb 723afed2486bab9379ab1f8cd556acacd36f237d && git checkout -B drift-repro aa71c4d9d146861fc09d63363e4c8fb8286719fb && git merge --no-ff 723afed2486bab9379ab1f8cd556acacd36f237d
node scripts/docs-audit/affected-docs.mjs --json aa71c4d9d146861fc09d63363e4c8fb8286719fb
|
Contract reviewServed-tier: Inputs: card #22090 body; triage grade 6040669927; claim 6041193650 (6041180533 is its superseded duplicate, not a claim); the dev's os-dev-report 6043377779; PR #22112 body and file list (7 files, 559 additions, 38 deletions); the net diff of the head against merge-base ① Derived judgmentsPublic surface of
Membership.
The door (packages/runtime/src/domains/packages.ts, revert branch :1893 to :1925).
The 409.
Partial failure.
Unprovisioned store. RIGHT. The catch at protocol.ts:23857 to :23860 returns Accept set. No request or response schema changes. The door's response stays Tests. The door pin covers triage's pins: (a) test:198, :213, :226; (b) test:237; (c) test:279; (d) test:289 and :300; and the two organization pins, test:251 and :263. packages/runtime/src/domains/packages-vetted-org-source.test.ts:339 adds the revert door to the roster of doors that read the vetted organization, with a double at :126. The new test imports only workspace packages the runtime manifest already declares ( ② Semver level
③ Boundary flagsFrom 6043377779:
Check-runs on the head, read at 2026-10-07T17:47Z, 32 runs, no failure. Completed success: Build Core; Dogfood Regression Gate and its shards (1/3), (2/3), (3/3); Dogfood Verify CLI; Temporal Conformance (live PG + MySQL); Governed Surface Queue Guard; Type Check · source gates; Type Check · workspace; Type Check · debt ledger; Test Core (3/6); Check Changeset; Check PR Size; Auto Label; filter; Flag docs affected by code changes; Check Documentation Links; Part-of PR must not also close its card; The card this PR closes must claim this branch; No other open PR may claim the same issue; No other open PR may claim the same single-writer path. Skipped: Console Pin Gate; Build Docs; Packed-tarball smoke (opt-in). Still running at that read: Lint & Repo Gates; Type Check · consumer gates; Test Core (1/6), (2/6), (4/6), (5/6), (6/6). Of the seven required contexts, Lint & Repo Gates and Test Core had not concluded. This record judges the contract. The landing still waits for those conclusions; Not a governed surface: no path in the file list is under Implemented-by: VERDICT: PASS |
ACCEPT (seat review) — PR #22112 at head
|
Fixes #22090
Clause-②: yes (widening)
Summary
POST /api/v1/packages/:id/revertanswered404 RESOURCE_NOT_FOUND "No metadata items found for package '…'"for a Studio-authored package that held published items and a pending draft, and the draft stayed. The door asked only the metadata service, whose in-memory registry never holds a Studio package's stored rows.The door now asks the protocol first.
revertStoredPackagereads the package'ssys_metadatarows through the predicatelistDraftsalready used, refuses a never-published package with the declared409, and otherwise removes the drafts through the per-draft stepdiscardPackageDraftstakes. A package with no stored row is answered by the metadata service'srevertPackage, exactly as before.Where it landed, and why not in
metadata-manager.tsThe dispatch expected the fix in
MetadataManager.revertPackage(packages/metadata/src/metadata-manager.ts:2062). Measurement put the producer elsewhere:@objectstack/metadatacannot reach the stored rows.@objectstack/metadata-protocoldepends on it, not the other way round. On a real boot the manager has nosys_metadataloader either:setDataEngine/setDatabaseDriver(metadata-manager.ts:605,:635) have no caller in the tree, and the repository the plugin attaches is aFileSystemRepository(packages/metadata/src/plugin.ts:538).@objectstack/metadata-protocol. So the seam is a protocol verb, and the door in@objectstack/runtimecomposes the two services.metadata-manager.tsis unchanged.Mechanism (measured)
H1: confirmed. On
examples/app-showcasebooted withobjectstack dev --seed-admin --freshat baseb04a5295f7, the following answered404 RESOURCE_NOT_FOUND "No metadata items found for package 'com.example.repairs'", andGET /meta/_drafts?packageId=com.example.repairsstill listed the draft. The steps:POST /packagescreatescom.example.repairs;PUT /meta/object/repairs_ticket?package=com.example.repairs&mode=draftsaves an object;POST /packages/com.example.repairs/publish-draftspublishes it (outcome: published);POST /packages/com.example.repairs/revertreturns the 404.The same package, published and with no draft, also answered 404. Call chain at base:
dispatcher-plugin.ts:1380mounts/:id/revert;http-dispatcher.ts:2238handlePackages;domains/packages.ts:1882revert branch;:1887metadataService.revertPackage(id);metadata-manager.ts:2062, which collects members bymeta.packageId/meta.package(:2067) and throws the 404 when there are none (:2094).H2: confirmed, seam moved. Discard changes is
POST /packages/:id/discard-drafts(basedomains/packages.ts:1792), which callsprotocol.discardPackageDrafts(baseprotocol.ts:23716). That readsSysMetadataRepository.listDrafts({ packageId }), which filterssys_metadata.package_idfor the caller's organization plus env-wide rows (basesys-metadata-repository.ts:1220,:1259). The read path decorates the same column as_packageId. Thatwhereis nowpackageScopedRowWhere(sys-metadata-repository.ts:406), used bylistDrafts(:1285) for drafts and by the revert (protocol.ts:23853) for published rows. It is one statement of the rule.MetadataManager.revertPackageis reachable from the protocol (themetadataservice), but the stored rows are not reachable from the manager. So the seam is the protocol.H3: confirmed. A stored item's published version is its
activerow, and a pending change is a separatedraftrow.publish-draftspromotes the draft (the drafts list empties). Discard changes removes the draft throughdeleteMetaItem({ state: 'draft' }), in the scope the draft lives in. The revert reuses that per-draft step, now shared asdiscardDraftInItsScope(protocol.ts:23769). There is no new revert mechanism. A package with stored rows but no active row answers409 RESOURCE_CONFLICT: "Package '…' has never been published, so there is no published version to revert to. Publish the package first, or discard its drafts to drop them."H4: confirmed.
com.example.showcaseis the code-shipped control. Its two capabilities carry an authoredpackageId(examples/app-showcase/src/security/capabilities.ts). It has no stored row, sorevertStoredPackageanswersstored: falseand the unchanged metadata-service branch answers. It returned409 "Package 'com.example.showcase' has never been published"live at both base and head.Pins: base and head answers
b04a5295f7RESOURCE_NOT_FOUND, draft stays{ success: true }, drafts[], the published body servesRESOURCE_NOT_FOUNDRESOURCE_CONFLICT, worded, draft untouchedRESOURCE_NOT_FOUNDRESOURCE_NOT_FOUND(unchanged)com.example.showcaseRESOURCE_CONFLICT"has never been published"The live door gave these answers on both boots.
packages/runtime/src/package-revert-stored-members.integration.test.tspins them at the door: a realHttpDispatcher, a real ObjectQL on better-sqlite3, the real protocol and a realMetadataManager. It has 9 cases. Two of them pin the organization rule: an org caller reverts the env-wide draft, and another organization's draft is not touched.domains/packages-vetted-org-source.test.tsadds the revert door to its roster, because the door now reads the caller's vetted organization. No door-level pin underpackages/qa/dogfood/test/was needed.Reverse verification
On committed HEAD,
packages/runtime/src/domains/packages.tswas restored to base withgit restore --source=b04a5295f7 --worktree. The anchor count dropped from 4 to 0, and the file's blob equalled the base blob. Predicted before the run: 6 red, 3 green. Measured: 6 red, 3 green. Every red wasexpected 404 to be 200orexpected 404 to be 409, the base's 404. The three controls stayed green. Restored withgit checkout HEAD -- …. Proof:git hash-objectequals the HEAD blob2df234a954, the index blob equals the same,git diff HEADis empty, andgit statusis clean.Clause-② (measured on the built entry declarations)
The claim declared
no. The built@objectstack/metadata-protocoldist/index.d.ts, base against head with comments stripped, gains one public method,revertStoredPackage(request), and oneprivatemember.packageScopedRowWhereis not exported from the entry. The@objectstack/runtimedist/index.d.tsis byte-identical with comments stripped. One added public method on an exported class is a widening, as in thedeclinesStoredRowprecedent. So the line above readsyes (widening),@objectstack/metadata-protocolis gradedminor, and@objectstack/runtimeis gradedpatchwithClause-②: noin its changeset. The route's request and response contract is unchanged.Tests and gates (at
723afed248)pnpm --filter @objectstack/metadata-protocol exec vitest run: 221 files passed, 3 skipped; 28227 tests passed, 19 skipped.pnpm --filter @objectstack/runtime exec vitest run --project local: 333 files passed; 4710 tests passed, 19 skipped.typecheckfor both packages exits 0. The runtime test layer holds its ledger (27 files, 190 errors, 68 signatures, unchanged).node scripts/pm/dispatch-gates.mjs --commandsderivation at723afed248gives 65 commands, all run.--ranreconciles 65 of 65 with a derived zero NOT-MEASURED.check-partof-closing-keywordwas run with this body asPR_BODY.check-closing-target-claimandcheck-single-claim-pathsneed a PR number, so CI reads them.check-changeset-no-majorwas run with this body as the event payload. Its level axis readsClause-②: yes (widening)and finds@objectstack/metadata-protocolgradedminor. It is green.adr-symbol-anchors,scripts-symbol-anchors,spec-docblock-symbol-anchors,adr-anchors) are green.check:dual-build-cjs-loadsfirst reported PREREQUISITE NOT MET because 8 unrelated dists were missing. It is green after building them.eslint.config.mjsenables no type-aware linting, so no untouched file's verdict can move.maingained two commits after the last merge,ac9f8bd471andaa71c4d9d1. One edits ametadata-protocoltest file and one is aspecchange. CI's merge ref reads them.Acceptance notes
showcase_task.donebound tocom.example.showcase. The package now has a stored draft and no active stored row, so the revert answers409with the stored half's sentence. At base it answered409with the registry's sentence. The draft stays in both. Whether a code baseline counts as "published" belongs to the same Revert-versus-Discard follow-up.codeandstatus. Drafts discarded before it stay discarded, as withdiscard-drafts. A collected list would have reached the door's{ success: true }as a success.actor. It read no body before, and its request contract is unchanged.Generated by Claude Code