Skip to content

fix(packages): POST /packages/:id/revert reverts a Studio-authored package instead of answering 404 - #22112

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22090-revert-package-stored-members
Oct 7, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-22090-revert-package-stored-members

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22090
Clause-②: yes (widening)

Summary

POST /api/v1/packages/:id/revert answered 404 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. revertStoredPackage reads the package's sys_metadata rows through the predicate listDrafts already used, refuses a never-published package with the declared 409, and otherwise removes the drafts through the per-draft step discardPackageDrafts takes. A package with no stored row is answered by the metadata service's revertPackage, exactly as before.

Where it landed, and why not in metadata-manager.ts

The dispatch expected the fix in MetadataManager.revertPackage (packages/metadata/src/metadata-manager.ts:2062). Measurement put the producer elsewhere:

  • @objectstack/metadata cannot reach the stored rows. @objectstack/metadata-protocol depends on it, not the other way round. On a real boot the manager has no sys_metadata loader either: setDataEngine / setDatabaseDriver (metadata-manager.ts:605, :635) have no caller in the tree, and the repository the plugin attaches is a FileSystemRepository (packages/metadata/src/plugin.ts:538).
  • The stored rows, the package-scoped read and the draft discard all live in @objectstack/metadata-protocol. So the seam is a protocol verb, and the door in @objectstack/runtime composes the two services.

metadata-manager.ts is unchanged.

Mechanism (measured)

  • H1: confirmed. On examples/app-showcase booted with objectstack dev --seed-admin --fresh at base b04a5295f7, the following answered 404 RESOURCE_NOT_FOUND "No metadata items found for package 'com.example.repairs'", and GET /meta/_drafts?packageId=com.example.repairs still listed the draft. The steps:

    • POST /packages creates com.example.repairs;
    • PUT /meta/object/repairs_ticket?package=com.example.repairs&mode=draft saves an object;
    • POST /packages/com.example.repairs/publish-drafts publishes it (outcome: published);
    • a second draft save edits it;
    • POST /packages/com.example.repairs/revert returns the 404.

    The same package, published and with no draft, also answered 404. Call chain at base:

    • dispatcher-plugin.ts:1380 mounts /:id/revert;
    • http-dispatcher.ts:2238 handlePackages;
    • domains/packages.ts:1882 revert branch;
    • :1887 metadataService.revertPackage(id);
    • metadata-manager.ts:2062, which collects members by meta.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 (base domains/packages.ts:1792), which calls protocol.discardPackageDrafts (base protocol.ts:23716). That reads SysMetadataRepository.listDrafts({ packageId }), which filters sys_metadata.package_id for the caller's organization plus env-wide rows (base sys-metadata-repository.ts:1220, :1259). The read path decorates the same column as _packageId. That where is now packageScopedRowWhere (sys-metadata-repository.ts:406), used by listDrafts (:1285) for drafts and by the revert (protocol.ts:23853) for published rows. It is one statement of the rule. MetadataManager.revertPackage is reachable from the protocol (the metadata service), 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 active row, and a pending change is a separate draft row. publish-drafts promotes the draft (the drafts list empties). Discard changes removes the draft through deleteMetaItem({ state: 'draft' }), in the scope the draft lives in. The revert reuses that per-draft step, now shared as discardDraftInItsScope (protocol.ts:23769). There is no new revert mechanism. A package with stored rows but no active row answers 409 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.showcase is the code-shipped control. Its two capabilities carry an authored packageId (examples/app-showcase/src/security/capabilities.ts). It has no stored row, so revertStoredPackage answers stored: false and the unchanged metadata-service branch answers. It returned 409 "Package 'com.example.showcase' has never been published" live at both base and head.

Pins: base and head answers

pin base b04a5295f7 head
(a) Studio package, published, one draft 404 RESOURCE_NOT_FOUND, draft stays 200 { success: true }, drafts [], the published body serves
(b) Studio package, never published 404 RESOURCE_NOT_FOUND 409 RESOURCE_CONFLICT, worded, draft untouched
(c) unknown id 404 RESOURCE_NOT_FOUND 404 RESOURCE_NOT_FOUND (unchanged)
(d) code-shipped com.example.showcase 409 RESOURCE_CONFLICT "has never been published" the same 409, the same sentence
published Studio package, no draft 404 200

The live door gave these answers on both boots. packages/runtime/src/package-revert-stored-members.integration.test.ts pins them at the door: a real HttpDispatcher, a real ObjectQL on better-sqlite3, the real protocol and a real MetadataManager. 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.ts adds the revert door to its roster, because the door now reads the caller's vetted organization. No door-level pin under packages/qa/dogfood/test/ was needed.

Reverse verification

On committed HEAD, packages/runtime/src/domains/packages.ts was restored to base with git 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 was expected 404 to be 200 or expected 404 to be 409, the base's 404. The three controls stayed green. Restored with git checkout HEAD -- …. Proof: git hash-object equals the HEAD blob 2df234a954, the index blob equals the same, git diff HEAD is empty, and git status is clean.

Clause-② (measured on the built entry declarations)

The claim declared no. The built @objectstack/metadata-protocol dist/index.d.ts, base against head with comments stripped, gains one public method, revertStoredPackage(request), and one private member. packageScopedRowWhere is not exported from the entry. The @objectstack/runtime dist/index.d.ts is byte-identical with comments stripped. One added public method on an exported class is a widening, as in the declinesStoredRow precedent. So the line above reads yes (widening), @objectstack/metadata-protocol is graded minor, and @objectstack/runtime is graded patch with Clause-②: no in 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.
  • typecheck for both packages exits 0. The runtime test layer holds its ledger (27 files, 190 errors, 68 signatures, unchanged).
  • The node scripts/pm/dispatch-gates.mjs --commands derivation at 723afed248 gives 65 commands, all run. --ran reconciles 65 of 65 with a derived zero NOT-MEASURED.
  • The 53-row artifact-roster block was run too. 51 rows are green; check-partof-closing-keyword was run with this body as PR_BODY. check-closing-target-claim and check-single-claim-paths need a PR number, so CI reads them.
  • check-changeset-no-major was run with this body as the event payload. Its level axis reads Clause-②: yes (widening) and finds @objectstack/metadata-protocol graded minor. It is green.
  • The four symbol-anchor sweeps (adr-symbol-anchors, scripts-symbol-anchors, spec-docblock-symbol-anchors, adr-anchors) are green.
  • check:dual-build-cjs-loads first reported PREREQUISITE NOT MET because 8 unrelated dists were missing. It is green after building them.
  • ESLint was run as a declared narrowing. The population is the 5 changed TypeScript files. The JSON output reports 5 files, 0 errors and 0 warnings. eslint.config.mjs enables no type-aware linting, so no untouched file's verdict can move.
  • main gained two commits after the last merge, ac9f8bd471 and aa71c4d9d1. One edits a metadata-protocol test file and one is a spec change. CI's merge ref reads them.

Acceptance notes

  • An item created after the last publish is removed by the revert. It is a draft with no published row, so it was not part of the published state. Discard changes removes it too. For a stored package the two buttons now do the same thing once the package has been published. The objectui wording follow-up triage foresaw (merge them, or keep both with distinct meanings) is unchanged.
  • Code-shipped package with an org overlay draft (boundary, unchanged answer class). This was measured live with an org-scoped draft of showcase_task.done bound to com.example.showcase. The package now has a stored draft and no active stored row, so the revert answers 409 with the stored half's sentence. At base it answered 409 with 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.
  • A refused draft fails the call. The error carries the refusal's own code and status. Drafts discarded before it stay discarded, as with discard-drafts. A collected list would have reached the door's { success: true } as a success.
  • The door does not forward a body actor. It read no body before, and its request contract is unchanged.

Generated by Claude Code

claude added 3 commits October 7, 2026 15:58
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
@github-actions github-actions Bot added the size/l label Oct 7, 2026
@github-actions

github-actions Bot commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

18 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 aa71c4d9d146861fc09d63363e4c8fb8286719fb.

⛔ 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 — 31 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 aa71c4d9d146861fc09d63363e4c8fb8286719fb → packageMentionDocs.

Which tree this was computed on

This run read content/docs from c34382ae99db560773450dd6d82e5ca4c253c9dd — the merge of head 723afed2486bab9379ab1f8cd556acacd36f237d into base aa71c4d9d146861fc09d63363e4c8fb8286719fb, 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 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

⚠️ 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 aa71c4d9d146861fc09d63363e4c8fb8286719fb → 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: 723afed2486bab9379ab1f8cd556acacd36f237d
Local-runs: none

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 bafb58bb08, read from git objects without a checkout; the check-runs on the head, read at 17:40Z and once more at 17:47Z. The dispatching seat's own conclusions were not read as input. Nothing was built, run or re-run. Nothing was posted to GitHub.

① Derived judgments

Public surface of @objectstack/metadata-protocol.

  • RIGHT. ObjectStackProtocolImplementation.revertStoredPackage (packages/metadata-protocol/src/protocol.ts:23837) is a new public async method on a class the entry exports (packages/metadata-protocol/src/index.ts:3). The class is reached from the package's exports["."] types (packages/metadata-protocol/package.json). Adding it widens the published surface.
  • RIGHT. packageScopedRowWhere (packages/metadata-protocol/src/sys-metadata-repository.ts:406) stays off the entry. index.ts re-exports only SysMetadataRepository, resetEnvWritableMetadataTypes and five types from that module (index.ts:153 to :160). There is no star re-export of it anywhere in index.ts. The docblock's claim at sys-metadata-repository.ts:403 holds. discardDraftInItsScope is private (protocol.ts:23769).
  • RIGHT. The changeset sentence "The only change to the public surface is this one added method" (.changeset/22090-metadata-protocol-revert-stored-package.md:13) matches the diff.

Membership.

  • RIGHT. listDrafts now builds its where through packageScopedRowWhere(this.organizationId, 'draft', filter) (sys-metadata-repository.ts:1285). The function sets, in order, state, then $or of the caller's organization and organization_id: null when the organization is non-null, else organization_id: null, then type, then package_id (sys-metadata-repository.ts:406 to :431). The base listDrafts built exactly those keys in exactly that order with the same values (merge-base bafb58bb08, sys-metadata-repository.ts:1241 to :1259). So discardPackageDrafts (protocol.ts:23717) reads the same drafts as before. Its per-draft delete is the same deleteMetaItem call with the same five arguments, now inside discardDraftInItsScope (protocol.ts:23769 to :23786). discardPackageDrafts answers as before.
  • RIGHT. The active-row read packageScopedRowWhere(orgId, 'active', { packageId }) (protocol.ts:23853) cannot see rows another organization owns. A non-null orgId yields $or of that organization and env-wide. A null orgId yields env-wide only. No arm matches a third organization. The door pin at packages/runtime/src/package-revert-stored-members.integration.test.ts:263 holds this at the door.
  • NOTE, not wrong. The revert acts on the drafts the caller's organization sees, not on every draft of the package in storage. Another organization's draft of the same package stays. That is the rule discardPackageDrafts has always had, and the docblock states it (protocol.ts:23812 to :23815). The changeset's "every draft of the package" (metadata-protocol changeset:12) is bounded by its first bullet, "the same predicate listDrafts uses". Accurate as scoped.

The door (packages/runtime/src/domains/packages.ts, revert branch :1893 to :1925).

  • RIGHT. requireManageMetadata runs at :1894, before resolveProtocol (:1907) and before any read.
  • RIGHT. The organization is deps.resolveActiveOrganizationId(_context) (:1910), the same source the discard-drafts door reads (:1809). Both spread it only when truthy.
  • RIGHT. The revert passes packageId and organizationId only (:1911 to :1914). It reads no body actor. Discard-drafts does forward body?.actor (:1813). The revert door read no body at base, so its request contract is unchanged (.changeset/22090-runtime-revert-studio-package.md:12).
  • RIGHT. stored: false falls through to metadataService.revertPackage(id) (:1920 to :1923), the base text unchanged. An unknown id answers the manager's own 404 (packages/metadata/src/metadata-manager.ts:2092 to :2099 at base; test:279). A code-shipped package with no stored row answers the manager's own verdict (test:289 and :300). The typeof guard at :1908 keeps a protocol without the verb on the base path.
  • ESCALATED, not blocking. A code-shipped package with an org overlay draft bound to it in sys_metadata. The dev measured 409 at base and at head with different sentences (6043377779 out_of_scope_findings second entry; PR body acceptance note 2). I confirm that reading for a code package the registry has never published: base answers the manager's 409 (metadata-manager.ts:2103 to :2110), head answers the protocol's 409 (protocol.ts:23864 to :23871). There is a variant the dev did not measure. The same package after POST /packages/:id/publish (packages.ts:1459 to :1466, which calls MetadataManager.publishPackage) has publishedDefinition on its registry members. At base, revert reached revertPackage, found them published, restored each member and answered 200 (metadata-manager.ts:2112 to :2121). At head, the protocol sees one stored draft and no active stored row, throws the 409 at protocol.ts:23864 to :23871 before the manager is asked, restores nothing, and the sentence "has never been published" is false for that package. So one reachable combination changes status, 200 to 409. The precondition is narrow: the manager collects only members carrying an authored packageId or package key (metadata-manager.ts:2067), so a stamp-only code package never reached the 200 arm at base either (the dev's finding (a) in 6043377779). The combination is a hybrid, registry members plus a stored overlay draft, which neither triage's four pins (6040669927) nor the card's reproduction covers, and its semantics is the question triage deferred to its foreseen follow-up ("whether a code baseline counts as published"). It does not block this PR. It must be named on that follow-up card with the reproduction above when the card is filed. The dispatching seat owns the filing.

The 409.

  • RIGHT. RESOURCE_CONFLICT is a member of the standard catalog (packages/spec/src/api/errors.zod.ts:95) and is the standard code for status 409 (errors.zod.ts:189). It is the same code and status MetadataManager.revertPackage throws (metadata-manager.ts:2107 to :2108 at base). No ledger row and no sweep entry is owed: packages/runtime/src/dispatcher-error-vocabulary.ts carries no row for the manager's identical producer, because a registered code drops out of the sweep (scripts/check-dispatcher-error-vocabulary.mjs header, "which makes the site disappear from the scan").
  • RIGHT. The door catches with deps.errorFromThrown(e, 500) (packages.ts:1917). HttpDispatcher.errorFromThrown (packages/runtime/src/http-dispatcher.ts:1112) calls resolveThrownHttpError (packages/types/src/thrown-http-error.ts, that function), which reads .status first and a registered .code second, and withholds nothing below 500. So the wire carries 409 and RESOURCE_CONFLICT with the sentence. Test:237 pins status, code and the sentence prefix at the door.

Partial failure.

  • RIGHT. A refused draft propagates out of the loop (protocol.ts:23875 to :23878) after the drafts before it were deleted. The docblock states it in plain words: "Drafts discarded before the refusal stay discarded, as they do in discardPackageDrafts" (protocol.ts:23834 to :23835). The TSDoc ships in the entry declarations, so the method's consumer reads it there.
  • NOTE. The metadata-protocol changeset states the failure mode, "A refused draft fails the call with that refusal's own code and status" (:12), but not that earlier drafts stay discarded. The PR body's acceptance note 3 does. One clause short. Not false.

Unprovisioned store. RIGHT. The catch at protocol.ts:23857 to :23860 returns stored: false only when rethrowUnlessMetadataStoreUnprovisioned returns, and that helper returns only for isMissingTableError; every other failure is rethrown classified. The read-seam discriminator is the declared one.

Accept set. No request or response schema changes. The door's response stays { success: true } (packages.ts:1915 and :1923). PackagesDomainProtocol gains an optional revertStoredPackage typed from the producer's own method (packages.ts:198, import at :56). That type is not exported from packages/runtime/src/index.ts, which exports nothing from domains/packages, so the runtime's published declarations do not move. The runtime's new import is import type and is erased at build.

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 (@objectstack/driver-sql, @objectstack/metadata, @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/objectql). No filesystem read escapes the package.

② Semver level

  • @objectstack/metadata-protocol: minor, Clause-②: yes (widening). RIGHT. One added public method on an exported class is "a purely additive widening of a published package's public surface" and "takes at least minor" (.github/workflows/pr-automation.yml:754 to :759; AGENTS.md Post-Task Checklist step 3, "yes takes at least minor"). The same act was graded minor with yes (widening) in .changeset/21986-metadata-protocol-declines-stored-row-public.md.
  • @objectstack/runtime: patch, Clause-②: no. RIGHT. The runtime's entry declarations do not move (① Accept set). "A fix( that changes no public surface stays patch" (pr-automation.yml:758 to :759).
  • @objectstack/metadata: no changeset. RIGHT. The diff does not touch it (file list, 7 files; metadata-manager.ts unchanged). Triage's "Patch changeset" (6040669927) and the claim's ".changeset/22090-*.md (@objectstack/metadata patch)" (6041193650) presupposed a landing in metadata-manager.ts. The measured producer is elsewhere, so that package publishes nothing here.
  • PR body line 2, Clause-②: yes (widening). RIGHT. The declaration is PR-scoped and names no package (pr-automation.yml:785 to :788). A PR that widens one published package declares yes. The level axis reads it against the two levels and reaches discharged, one moved package at minor and one at patch, exit 0 (scripts/check-changeset-no-major.mjs:1515 to :1528). Check Changeset on the head: success.
  • The claim's and triage's Clause-②: no described a metadata-manager.ts fix that was never written. The diff is a different act. The dev measured the built declarations base against head (6043377779 clause_2); I confirm the same reading from the source entry. Option A of the open question is right. Option B would grade a widening patch and declare it no, which pr-automation.yml:754 to :759 forbids.
  • Changesets, sentence by sentence. Metadata-protocol changeset: the title matches the signature at protocol.ts:23837 to :23844; bullet 1 matches the shared predicate (sys-metadata-repository.ts:406, :1285; protocol.ts:23853); bullet 2 matches protocol.ts:23857 to :23862, the unprovisioned arm included; bullet 3 matches :23864 to :23871, and "Nothing is touched" holds because the throw precedes the loop; bullet 4 matches :23873 to :23879 with the two notes in ① (scope of "every draft", the partial-state clause); bullet 5 matches ① Public surface and Membership. Runtime changeset: the title matches the card and test:198; bullet 1 matches the base call chain (packages.ts:1922, metadata-manager.ts:2064 to :2071 and :2092 to :2099 at base); bullet 2 matches packages.ts:1907 to :1917; bullet 3, "A package with no stored row ... exactly as before. That covers a code-shipped package and an unknown id (404). It is also what happens when the protocol does not provide revertStoredPackage", matches packages.ts:1908 and :1920 to :1923 as scoped by "no stored row"; the hybrid in ① has a stored row and is outside that sentence; bullet 4 matches the unchanged request and the { success: true } response.

③ Boundary flags

From 6043377779:

  • Deviation 1, landing moved off metadata-manager.ts: ACCEPTED. @objectstack/metadata-protocol depends on @objectstack/metadata (packages/metadata-protocol/package.json dependencies), not the reverse, and the stored rows are reached only through the protocol. The PR body states the reason.
  • Deviation 2, changeset packages differ from the claim: ACCEPTED. See ②.
  • Deviation 3 and the one open question, the Clause-② conflict: ANSWERED. Keep yes (widening) with @objectstack/metadata-protocol at minor, option A. See ②.
  • Deviation 4, commit trailer reworded to the model-free pair: ACCEPTED. AGENTS.md Multi-agent discipline requires that pair. No pushed history was rewritten.
  • Deviation 5, the PR body's gates bullet: ACCEPTED as reported. The two PR-number gates have check-runs on the head: "The card this PR closes must claim this branch", success; "No other open PR may claim the same single-writer path", success.
  • Deviations 6 and 7, base drift and the door pin's first run before the main merge: ACCEPTED. CI's merge ref reads both later commits. The head's check-runs are the verdict, below.
  • Out-of-scope finding (a), revert and publish answer 404 for a stamp-only code package: ESCALATED for a card. A reproducible defect at a named location (metadata-manager.ts:2067 and :1840), not this PR's. The dispatching seat files it.
  • Out-of-scope finding (b), a code-shipped package with an org overlay draft, and the Revert versus Discard wording: ESCALATED into the follow-up triage foresaw (6040669927), with the hybrid variant from ① added to it.

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; in_progress is not a pass (AGENTS.md Multi-agent discipline 7).

Not a governed surface: no path in the file list is under docs/adr/, docs/NORTH-STAR.md, .claude/, skills/, AGENTS.md or CLAUDE.md. 597 changed lines. Head repo equals base repo. The PR is a draft.

Implemented-by: claude/issue-22090-revert-package-stored-members
Reviewed-by: session_017ErfyP2Rx7XWHJA27QjyUi

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

ACCEPT (seat review) — PR #22112 at head 723afed248

domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-07T18:09Z. The dev's report is os-dev-report 6043377779 on #22090. It builds triage's direction (6040669927): a revert finds a Studio package's members the way Discard changes reads them.

  • The change. The seat read the diff in full.
    • metadata-protocol: the new public revertStoredPackage reads the package's drafts through listDrafts, and its active rows through packageScopedRowWhere, the one statement of the organization-plus-env-wide rule that listDrafts now also calls.
      • Stored rows with no active row: it refuses with RESOURCE_CONFLICT / 409, and nothing is touched.
      • Otherwise it discards the drafts through discardDraftInItsScope, the per-draft step it now shares with discardPackageDrafts.
      • No stored row: it answers stored: false.
    • The runtime door: it checks requireManageMetadata first, asks the protocol with the active organization, and on stored: false falls through to the metadata service's unchanged revertPackage.
    • metadata-manager.ts is untouched. The landing moved to the producer the dev measured, which the order allowed. The reason is in H2: @objectstack/metadata cannot reach the stored rows.
  • Pins, each against triage's four:
    • (a) a published Studio package with one draft: 200, and the draft is gone;
    • (b) a never-published Studio package: 409, worded;
    • (c) an unknown id: 404, unchanged;
    • (d) a code-shipped package: unchanged.
    • Two organization pins are included, and the revert door joins the vetted-organization roster.
    • Reverse verification restored the door at base: 6 red and 3 green, as predicted, and it was restored by blob equality.
  • Clause-②: yes (widening), revised from the claim's no.
    • The dev measured the built declarations. One added public method is a widening, so @objectstack/metadata-protocol is minor and @objectstack/runtime is patch.
    • The claim's line is revised in place (6041193650).
    • The contract review is owed, and is on record: 6043636057, at tier, on 723afed248, PASS. It answered the dev's open question with option A.
  • Changesets, sentence by sentence: the contract review read both changesets against the diff, line by line, in ②. The seat re-read the five metadata-protocol bullets and the four runtime bullets and agrees. One clause is short: the metadata-protocol changeset does not say that drafts discarded before a refusal stay discarded. The method's docblock does say it, and the docblock ships in the declarations. Noted, not reworked.
  • CI on 723afed248: 31 success, and the 3 skips are in the roster (check-expected-skips exit 0).
  • Readings against main at 4935c66bc9: NOT governed (0 of 7 paths), 597 changed lines, and git merge-tree is clean.
  • Filed and noted (PR Acceptance notes):

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 7, 2026 18:10
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 7, 2026 18:10
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit a543e24 Oct 7, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22090-revert-package-stored-members branch October 7, 2026 18:32
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

Development

Successfully merging this pull request may close these issues.

packages: POST /packages/{id}/revert answers 404 "No metadata items found" for a Studio-authored package that has published items

2 participants