You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit a543e24
Browse filesBrowse the repository at this point in the historyBrowse files
fix(packages): POST /packages/:id/revert reverts a Studio-authored package instead of answering 404 (#22112)
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](https://claude.ai/code/session_017ErfyP2Rx7XWHJA27QjyUi)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
`ObjectStackProtocolImplementation.revertStoredPackage({ packageId, organizationId?, actor? })` reverts a package's stored members to their published version
6
+
7
+
Clause-②: yes (widening)
8
+
9
+
- A Studio-authored package's members are `sys_metadata` rows bound by `package_id`. The method reads them through the same predicate `listDrafts` uses, which now has one statement shared by both reads.
10
+
- No stored row bound to the package: it answers `{ stored: false, discarded: [] }` and touches nothing. The package's members, if it has any, belong to the metadata service. An unprovisioned `sys_metadata` answers the same way.
11
+
- Stored rows, none of them published: it refuses with `RESOURCE_CONFLICT` / 409, "Package '…' has never been published, so there is no published version to revert to." Nothing is touched.
12
+
- Otherwise it removes every draft of the package, each in the scope it lives in, through the same per-draft step `discardPackageDrafts` takes. Each item then serves its published (active) row again. An item created after the last publish has no published row and is removed. A refused draft fails the call with that refusal's own code and status.
13
+
- The only change to the public surface is this one added method. `discardPackageDrafts` and `listDrafts` answer as before.
`POST /packages/:id/revert` reverts a Studio-authored package instead of answering 404 "No metadata items found"
6
+
7
+
Clause-②: no
8
+
9
+
- The door asked only the metadata service, whose in-memory registry never holds a Studio package's stored rows. So a package with published items and a pending draft answered `404 RESOURCE_NOT_FOUND`, and the draft stayed.
10
+
- The door now asks the protocol's `revertStoredPackage` first, with the caller's active organization. A package with stored rows answers `200 { success: true }` with its drafts removed, or `409 RESOURCE_CONFLICT` when it has never been published.
11
+
- A package with no stored row is answered by the metadata service's `revertPackage`, 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`.
12
+
- The request and the response shape are unchanged.
0 commit comments