diff --git a/.changeset/sdk-response-contracts-bound.md b/.changeset/sdk-response-contracts-bound.md
new file mode 100644
index 0000000000..f80c8267b3
--- /dev/null
+++ b/.changeset/sdk-response-contracts-bound.md
@@ -0,0 +1,72 @@
+---
+"@objectstack/spec": minor
+"@objectstack/client": minor
+"@objectstack/runtime": patch
+"@objectstack/rest": patch
+---
+
+feat(spec,client): bind published response contracts for the 17 unbound client-SDK methods; retire the false `PackageRollbackResponseSchema` (#12038, ruling 1C · 2C · 3A · 4A · 5A)
+
+
+
+**BREAKING** export removal, landing after the v17.0.0 cut (the lockstep
+launch-window convention ships it as `minor`; the prescription is registered
+under protocol major 18 — `RETIRED_DEFS_BY_MAJOR[18]` `api/PackageRollbackResponse`
+plus the D3 semantic entry `package-rollback-response-retired` — where
+`os migrate meta` users will look).
+
+FROM → TO:
+
+- `PackageRollbackResponseSchema` / `PackageRollbackResponse` /
+ `PackageRollbackResponseParsed` → `RollbackToPackageCommitResponseSchema` /
+ `RollbackToPackageCommitResponse` (`@objectstack/spec/api`). The retired
+ schema declared a VERSION rollback (`{ success, restoredVersion?,
+ message? }`) while the live `POST /packages/:id/rollback` route posts
+ `{ commitId }` and answers the ADR-0067 COMMIT rollback —
+ `{ success, revertedCommits: string[], failed: [{ commitId, error }] }`.
+ Read `revertedCommits` / `failed`; there is no `restoredVersion`.
+- `PackageApiContracts.rollbackPackage` → *(removed)* — it bound the
+ wrong-operation schema to the exact live path. No route registration or
+ SDK generation ever consumed it (zero consumers measured across
+ objectstack, objectui and cloud; only its own unit test and the #11925
+ compile-time guard, both updated in this PR).
+
+One-line fix: replace any import of `PackageRollbackResponse(Schema)` with
+`RollbackToPackageCommitResponse(Schema)` and read `revertedCommits` /
+`failed` instead of `restoredVersion`. `PackageRollbackRequestSchema` stays
+published (ruled out of the retirement), bound to no route.
+
+The rest of the change is additive — the recorded five-part maintainer
+ruling (2026-08-27) for the 17 client-SDK methods that had no published
+response contract:
+
+- **12 describe-only transcriptions** into `@objectstack/spec/api`, each
+ from the return type its producer already declares inline (no wire byte
+ changes): `ListDraftsResponseSchema`, `GetMetaDiagnosticsResponseSchema`,
+ `FindReferencesToMetaResponseSchema`, `RollbackMetaItemResponseSchema`,
+ `DiffMetaItemResponseSchema`, `ResolvedBookSchema` (authored beside its
+ interfaces in `system/book.zod.ts`), `DiscardPackageDraftsResponseSchema`,
+ `ListPackageCommitsResponseSchema` (the `{ commits }` wrapper declared as
+ the handler's own), `RevertPackageCommitResponseSchema`,
+ `RollbackToPackageCommitResponseSchema`,
+ `ReassignOrphanedMetadataResponseSchema`, `DuplicatePackageResponseSchema`.
+- **Ruling 1C**: `GetPublishedMetaItemResponseSchema` is deliberately opaque
+ (`z.unknown()`) — the route answers an arbitrary metadata item body, never
+ a union frozen against the type registry.
+- **Ruling 2C**: `meta.migrateStored` stays UNBOUND, documented at its two
+ ledger rows and in the SDK — `StoredMigrationReport` lives in
+ `@objectstack/metadata-protocol`, and a second declaration would drift.
+- **Ruling 4A**: `PackageExportManifestSchema` pins the four fixed keys
+ (`id`, `name`, `version`, `label?`) and stays honestly open for the
+ registry-derived plural keys.
+- **Ruling 5A**: `PackagePublishResultSchema` and the `ResolvedBook` family
+ are re-exported into `@objectstack/spec/api` (the namespace the
+ route-ledger resolver searches) — never a second copy.
+- The 18 boundable route-ledger rows in `@objectstack/runtime` and
+ `@objectstack/rest` now name their `responseSchema`, each stating which
+ surface's envelope it describes; every named schema carries conformance
+ coverage (the #3877 rule).
+- The client SDK binds 16 of the 17 methods to the published payload types,
+ replaces four invented test mocks with producer-true shapes, and pins the
+ `unwrapResponse` mis-unwrap hazard so no bound payload can declare both a
+ boolean `success` and a `data` key.
diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx
index 7804c3cb9d..243c3f3f32 100644
--- a/content/docs/getting-started/quick-reference.mdx
+++ b/content/docs/getting-started/quick-reference.mdx
@@ -127,7 +127,7 @@ AI/ML capabilities - agents, skills, tools, MCP exposure, RAG, and cost tracking
| **[Usage](/docs/references/ai/usage)** | `usage.zod.ts` | AIUsageRecord, TokenUsage | AI usage and cost tracking |
| **[Solution Blueprint](/docs/references/ai/solution-blueprint)** | `solution-blueprint.zod.ts` | BlueprintObject, BlueprintApp | Blueprint format for AI app generation |
-## API Protocol (17 of 29 schemas)
+## API Protocol (17 of 31 schemas)
REST endpoints, real-time subscriptions, and discovery.
diff --git a/content/docs/references/api/index.mdx b/content/docs/references/api/index.mdx
index 080770244d..3d3e6c9364 100644
--- a/content/docs/references/api/index.mdx
+++ b/content/docs/references/api/index.mdx
@@ -22,8 +22,10 @@ This section contains all protocol schemas for the api layer of ObjectStack.
+
+
diff --git a/content/docs/references/api/meta.json b/content/docs/references/api/meta.json
index e333f891f8..69b8a4dd37 100644
--- a/content/docs/references/api/meta.json
+++ b/content/docs/references/api/meta.json
@@ -33,6 +33,8 @@
"storage",
"---More---",
"error-code-ledger",
+ "misc",
+ "package-lifecycle",
"sortability"
]
}
\ No newline at end of file
diff --git a/content/docs/references/api/misc.mdx b/content/docs/references/api/misc.mdx
new file mode 100644
index 0000000000..33aa19468e
--- /dev/null
+++ b/content/docs/references/api/misc.mdx
@@ -0,0 +1,82 @@
+---
+title: Misc
+description: Misc protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+## TypeScript Usage
+
+```typescript
+import { ResolvedBookSchema, ResolvedEntrySchema, ResolvedGroupSchema } from '@objectstack/spec/api';
+import type { ResolvedBook, ResolvedEntry, ResolvedGroup } from '@objectstack/spec/api';
+
+// Validate data
+const result = ResolvedBookSchema.parse(data);
+```
+
+---
+
+## ResolvedBook
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | The book's machine name. |
+| **label** | `string` | optional | The book's display label, when it declares one. |
+| **groups** | `{ key: string; label: string; entries: object[] }[]` | ✅ | The resolved groups, in render order. |
+
+### Nested Shape: `ResolvedBook.groups[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **key** | `string` | ✅ | The group's key (from the spine, or `uncategorized`). |
+| **label** | `string` | ✅ | The group's display label. |
+| **entries** | `{ doc?: string; href?: string; label?: string; description?: string; … }[]` | ✅ | The group's resolved entries, in render order. |
+
+
+---
+
+## ResolvedEntry
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **doc** | `string` | optional | Doc name, or undefined for an external link / separator. |
+| **href** | `string` | optional | External link target, when the entry is a link. |
+| **label** | `string` | optional | Display label, when one resolved. |
+| **description** | `string` | optional | Doc description, when one resolved. |
+| **badge** | `string` | optional | Badge text (e.g. "beta"), when declared. |
+| **icon** | `string` | optional | Icon name, when declared. |
+| **separator** | `boolean` | optional | True for a `---` separator node. |
+
+
+---
+
+## ResolvedGroup
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **key** | `string` | ✅ | The group's key (from the spine, or `uncategorized`). |
+| **label** | `string` | ✅ | The group's display label. |
+| **entries** | `{ doc?: string; href?: string; label?: string; description?: string; … }[]` | ✅ | The group's resolved entries, in render order. |
+
+### Nested Shape: `ResolvedGroup.entries[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **doc** | `string` | optional | Doc name, or undefined for an external link / separator. |
+| **href** | `string` | optional | External link target, when the entry is a link. |
+| **label** | `string` | optional | Display label, when one resolved. |
+| **description** | `string` | optional | Doc description, when one resolved. |
+| **badge** | `string` | optional | Badge text (e.g. "beta"), when declared. |
+| **icon** | `string` | optional | Icon name, when declared. |
+| **separator** | `boolean` | optional | True for a `---` separator node. |
+
+
+---
+
diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx
index 3d70ea305d..8d2259d99b 100644
--- a/content/docs/references/api/package-api.mdx
+++ b/content/docs/references/api/package-api.mdx
@@ -28,8 +28,8 @@ DELETE /api/v1/packages/:packageId — Uninstall a package
## TypeScript Usage
```typescript
-import { GetInstalledPackageRequestSchema, GetInstalledPackageResponseSchema, ListInstalledPackagesRequestSchema, ListInstalledPackagesResponseSchema, PackageApiErrorCode, PackageInstallRequestSchema, PackageInstallResponseSchema, PackagePathParamsSchema, PackageRollbackRequestSchema, PackageRollbackResponseSchema, PackageUpgradeRequestSchema, PackageUpgradeResponseSchema, ResolveDependenciesRequestSchema, ResolveDependenciesResponseSchema, UninstallPackageApiRequestSchema, UninstallPackageApiResponseSchema, UploadArtifactRequestSchema, UploadArtifactResponseSchema } from '@objectstack/spec/api';
-import type { GetInstalledPackageRequest, GetInstalledPackageResponse, ListInstalledPackagesRequest, ListInstalledPackagesResponse, PackageApiErrorCode, PackageInstallRequest, PackageInstallResponse, PackagePathParams, PackageRollbackRequest, PackageRollbackResponse, PackageUpgradeRequest, PackageUpgradeResponse, ResolveDependenciesRequest, ResolveDependenciesResponse, UninstallPackageApiRequest, UninstallPackageApiResponse, UploadArtifactRequest, UploadArtifactResponse } from '@objectstack/spec/api';
+import { GetInstalledPackageRequestSchema, GetInstalledPackageResponseSchema, ListInstalledPackagesRequestSchema, ListInstalledPackagesResponseSchema, PackageApiErrorCode, PackageInstallRequestSchema, PackageInstallResponseSchema, PackagePathParamsSchema, PackageRollbackRequestSchema, PackageUpgradeRequestSchema, PackageUpgradeResponseSchema, ResolveDependenciesRequestSchema, ResolveDependenciesResponseSchema, UninstallPackageApiRequestSchema, UninstallPackageApiResponseSchema, UploadArtifactRequestSchema, UploadArtifactResponseSchema } from '@objectstack/spec/api';
+import type { GetInstalledPackageRequest, GetInstalledPackageResponse, ListInstalledPackagesRequest, ListInstalledPackagesResponse, PackageApiErrorCode, PackageInstallRequest, PackageInstallResponse, PackagePathParams, PackageRollbackRequest, PackageUpgradeRequest, PackageUpgradeResponse, ResolveDependenciesRequest, ResolveDependenciesResponse, UninstallPackageApiRequest, UninstallPackageApiResponse, UploadArtifactRequest, UploadArtifactResponse } from '@objectstack/spec/api';
// Validate data
const result = GetInstalledPackageRequestSchema.parse(data);
@@ -287,43 +287,6 @@ Rollback package request
| **rollbackCustomizations** | `boolean` | optional (default: `true`) | Whether to restore pre-upgrade customizations |
----
-
-## PackageRollbackResponse
-
-Rollback package response
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **success** | `boolean` | ✅ | Operation success status |
-| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false |
-| **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata |
-| **data** | `{ success: boolean; restoredVersion?: string; message?: string }` | ✅ | |
-
-### Nested Shape: `PackageRollbackResponse.error`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
-| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112, #9106) |
-| **message** | `string` | ✅ | Readable error message |
-| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim (#9934). Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution (#3821) for anything unmarked. Status-agnostic; never replaces `message`. |
-| **category** | `string` | optional | Error category (e.g. validation, authorization) |
-| **httpStatus** | `integer` | optional | HTTP status of the response carrying this error |
-| **details** | `any` | optional | Additional error context (e.g. field validation errors) |
-| **requestId** | `string` | optional | Request ID for tracking |
-
-### Nested Shape: `PackageRollbackResponse.data`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **success** | `boolean` | ✅ | Whether the rollback succeeded |
-| **restoredVersion** | `string` | optional | Restored version |
-| **message** | `string` | optional | Rollback status message |
-
-
---
## PackageUpgradeRequest
diff --git a/content/docs/references/api/package-lifecycle.mdx b/content/docs/references/api/package-lifecycle.mdx
new file mode 100644
index 0000000000..1564ca24c7
--- /dev/null
+++ b/content/docs/references/api/package-lifecycle.mdx
@@ -0,0 +1,251 @@
+---
+title: Package Lifecycle
+description: Package Lifecycle protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+## Package lifecycle response contracts (#12038)
+
+Response payloads for the dispatcher-served `packages.*` lifecycle routes —
+the ADR-0067 commit timeline, the ADR-0033 draft batch doors, the ADR-0070
+export / adopt / duplicate family — ruled on 2026-08-27 (#12038,
+1C · 2C · 3A · 4A · 5A).
+
+Every schema here is a DESCRIBE-ONLY TRANSCRIPTION of the return type its
+producer already declares inline (`@objectstack/metadata-protocol`
+`protocol.ts`, except where a schema's own docblock says otherwise) —
+authoring one changes no wire byte. All of these routes are served by the
+runtime dispatcher ONLY (no REST twin — #12038 survey §1b), which answers
+through the `{ success, data }` envelope (`http-dispatcher.ts`), so each
+schema declares the `data` payload, envelope-free — the same convention as
+`PublishPackageDraftsResponseSchema` and its ledger row.
+
+`packages.publish`'s contract is NOT here: its producer
+(`MetadataManager.publishPackage`) already has an exact published schema,
+`PackagePublishResultSchema` in `@objectstack/spec/system` — re-exported
+below into this `/api` namespace (ruling 5A: re-export, never a second
+copy) because the route-ledger resolver looks names up only in
+`@objectstack/spec/api`.
+
+The retired `PackageRollbackResponseSchema` and its
+`PackageApiContracts.rollbackPackage` binding (see `./package-api.zod.ts`)
+declared a VERSION rollback against the live COMMIT-rollback path;
+`RollbackToPackageCommitResponseSchema` below is the true contract,
+authored after that retirement per the ruling's sequencing (3A).
+
+
+**Source:** `packages/spec/src/api/package-lifecycle.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { DiscardPackageDraftsResponseSchema, DuplicatePackageResponseSchema, ListPackageCommitsResponseSchema, PackageExportManifestSchema, PackagePublishResultSchema, ReassignOrphanedMetadataResponseSchema, RevertPackageCommitResponseSchema, RollbackToPackageCommitResponseSchema } from '@objectstack/spec/api';
+import type { DiscardPackageDraftsResponse, DuplicatePackageResponse, ListPackageCommitsResponse, PackageExportManifest, PackagePublishResult, ReassignOrphanedMetadataResponse, RevertPackageCommitResponse, RollbackToPackageCommitResponse } from '@objectstack/spec/api';
+
+// Validate data
+const result = DiscardPackageDraftsResponseSchema.parse(data);
+```
+
+---
+
+## DiscardPackageDraftsResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | True exactly when nothing failed. |
+| **discardedCount** | `number` | ✅ | How many drafts were discarded. |
+| **failedCount** | `number` | ✅ | How many drafts could not be discarded. |
+| **discarded** | `{ type: string; name: string }[]` | ✅ | Every draft that was discarded. |
+| **failed** | `{ type: string; name: string; error: string; code?: string }[]` | ✅ | Every draft the discard could not remove. |
+
+### Nested Shape: `DiscardPackageDraftsResponse.discarded[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the discarded draft. |
+| **name** | `string` | ✅ | Name of the discarded draft. |
+
+### Nested Shape: `DiscardPackageDraftsResponse.failed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the failing draft. |
+| **name** | `string` | ✅ | Name of the failing draft. |
+| **error** | `string` | ✅ | Why the discard failed. |
+| **code** | `string` | optional | Machine-readable failure code, when one was recorded. |
+
+
+---
+
+## DuplicatePackageResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | The duplicate's own verdict: true exactly when nothing failed AND at least one item was copied. |
+| **copiedCount** | `number` | ✅ | How many items were copied. |
+| **failedCount** | `number` | ✅ | How many items could not be copied. |
+| **targetPackageId** | `string` | ✅ | The new package the base was cloned into. |
+| **copied** | `{ type: string; name: string }[]` | ✅ | Every item that was copied. |
+| **failed** | `{ type: string; name: string; error: string }[]` | ✅ | Every item the clone could not copy. |
+
+### Nested Shape: `DuplicatePackageResponse.copied[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the copied item. |
+| **name** | `string` | ✅ | Name of the copied item. |
+
+### Nested Shape: `DuplicatePackageResponse.failed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the failing item. |
+| **name** | `string` | ✅ | Name of the failing item. |
+| **error** | `string` | ✅ | Why copying it failed. |
+
+
+---
+
+## ListPackageCommitsResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **commits** | `{ id: string; operation: Enum<'apply' \| 'revert'>; message?: string; actor?: string; … }[]` | ✅ | The commit timeline, newest first. |
+
+### Nested Shape: `ListPackageCommitsResponse.commits[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Commit id. |
+| **operation** | `Enum<'apply' \| 'revert'>` | ✅ | Whether the commit applied changes or reverted an earlier commit. |
+| **message** | `string` | optional | Commit message, when one was recorded. |
+| **actor** | `string` | optional | Who made the commit, when recorded. |
+| **aiModel** | `string` | optional | AI model that authored the change, when recorded. |
+| **parentCommitId** | `string` | optional | The commit this one chains from, when recorded. |
+| **itemCount** | `number` | ✅ | How many items the commit touched. |
+| **items** | `{ type: string; name: string; existedBefore: boolean; prevVersion: number \| null }[]` | ✅ | The items the commit touched. |
+| **createdAt** | `string` | optional | When the commit was made (ISO-8601 string), when recorded. |
+
+
+---
+
+## PackageExportManifest
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | The exported package's id. |
+| **name** | `string` | ✅ | The exported package's machine name. |
+| **version** | `string` | ✅ | The exported package's version. |
+| **label** | `string` | optional | Display label, when the package declares one. |
+
+
+---
+
+## PackagePublishResult
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | Whether the publish succeeded |
+| **packageId** | `string` | ✅ | The package ID that was published |
+| **version** | `integer` | ✅ | New version number after publish |
+| **publishedAt** | `string` | ✅ | Publish timestamp |
+| **itemsPublished** | `integer` | ✅ | Total metadata items published |
+| **validationErrors** | `{ type: string; name: string; message: string }[]` | optional | Validation errors if publish failed |
+
+### Nested Shape: `PackagePublishResult.validationErrors[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type that failed validation |
+| **name** | `string` | ✅ | Item name that failed validation |
+| **message** | `string` | ✅ | Validation error message |
+
+
+---
+
+## ReassignOrphanedMetadataResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | Whether the reassignment ran. |
+| **reassignedCount** | `number` | ✅ | How many orphaned items were adopted. |
+| **reassigned** | `{ type: string; name: string }[]` | ✅ | Every item that was adopted. |
+| **targetPackageId** | `string` | ✅ | The package the items were adopted into. |
+
+### Nested Shape: `ReassignOrphanedMetadataResponse.reassigned[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the adopted item. |
+| **name** | `string` | ✅ | Name of the adopted item. |
+
+
+---
+
+## RevertPackageCommitResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | True exactly when nothing failed. |
+| **revertedCount** | `number` | ✅ | How many items were reverted. |
+| **failedCount** | `number` | ✅ | How many items could not be reverted. |
+| **reverted** | `{ type: string; name: string; action: Enum<'removed' \| 'restored'> }[]` | ✅ | Every item the revert touched. |
+| **failed** | `{ type: string; name: string; error: string; code?: string }[]` | ✅ | Every item the revert could not touch. |
+| **revertCommitId** | `string` | optional | Id of the commit the revert itself created, when one was written. |
+
+### Nested Shape: `RevertPackageCommitResponse.reverted[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the reverted item. |
+| **name** | `string` | ✅ | Name of the reverted item. |
+| **action** | `Enum<'removed' \| 'restored'>` | ✅ | What the revert did to the item — removed what the commit created, or restored what it overwrote. |
+
+### Nested Shape: `RevertPackageCommitResponse.failed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the failing item. |
+| **name** | `string` | ✅ | Name of the failing item. |
+| **error** | `string` | ✅ | Why the revert failed for this item. |
+| **code** | `string` | optional | Machine-readable failure code, when one was recorded. |
+
+
+---
+
+## RollbackToPackageCommitResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | True exactly when nothing failed. |
+| **revertedCommits** | `string[]` | ✅ | Ids of the commits that were rolled back, in the order they were reverted. |
+| **failed** | `{ commitId: string; error: string }[]` | ✅ | Every commit the rollback could not revert. |
+
+### Nested Shape: `RollbackToPackageCommitResponse.failed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **commitId** | `string` | ✅ | The commit that could not be reverted. |
+| **error** | `string` | ✅ | Why reverting it failed. |
+
+
+---
+
diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx
index 8cfcfb8ab2..4c6ab840a3 100644
--- a/content/docs/references/api/protocol.mdx
+++ b/content/docs/references/api/protocol.mdx
@@ -12,8 +12,8 @@ description: Protocol protocol schemas
## TypeScript Usage
```typescript
-import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api';
-import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api';
+import { AiAgentCapabilitiesSchema, AiAgentChatRequestSchema, AiAgentSummarySchema, AiAgentsResponseSchema, AiChatRequestSchema, AiChatResponseSchema, AiCompleteRequestSchema, AiConversationSchema, AiMessageSchema, AiModelsResponseSchema, AiPendingActionSchema, AiPendingActionStatusSchema, AiStreamChunkSchema, ApproveAiPendingActionResponseSchema, AuditMetaItemRequestSchema, AuditMetaItemResponseSchema, AutomationActionsResponseSchema, AutomationTriggerRequestSchema, AutomationTriggerResponseSchema, BatchDataRequestSchema, BatchDataResponseSchema, CheckPermissionRequestSchema, CheckPermissionResponseSchema, CloneDataResponseSchema, CreateAiConversationRequestSchema, CreateDataRequestSchema, CreateDataResponseSchema, CreateManyDataRequestSchema, CreateManyDataResponseSchema, DeleteDataRequestSchema, DeleteDataResponseSchema, DeleteManyDataRequestSchema, DeleteManyDataResponseSchema, DeleteMetaItemRequestSchema, DeleteMetaItemResponseSchema, DiffMetaItemResponseSchema, DisablePackageRequestSchema, DisablePackageResponseSchema, EnablePackageRequestSchema, EnablePackageResponseSchema, FindDataRequestSchema, FindDataResponseSchema, FindReferencesToMetaResponseSchema, GetDataRequestSchema, GetDataResponseSchema, GetDiscoveryRequestSchema, GetDiscoveryResponseSchema, GetEffectivePermissionsRequestSchema, GetEffectivePermissionsResponseSchema, GetFieldLabelsRequestSchema, GetFieldLabelsResponseSchema, GetLocalesRequestSchema, GetLocalesResponseSchema, GetMetaDiagnosticsResponseSchema, GetMetaItemCachedRequestSchema, GetMetaItemCachedResponseSchema, GetMetaItemLayeredRequestSchema, GetMetaItemLayeredResponseSchema, GetMetaItemRequestSchema, GetMetaItemResponseSchema, GetMetaItemsRequestSchema, GetMetaItemsResponseSchema, GetMetaTypesRequestSchema, GetMetaTypesResponseSchema, GetNotificationPreferencesRequestSchema, GetNotificationPreferencesResponseSchema, GetObjectPermissionsRequestSchema, GetObjectPermissionsResponseSchema, GetPackageRequestSchema, GetPackageResponseSchema, GetPresenceRequestSchema, GetPresenceResponseSchema, GetPublishedMetaItemResponseSchema, GetTranslationsRequestSchema, GetTranslationsResponseSchema, GetUiViewRequestSchema, GetUiViewResponseSchema, HttpFindQueryParamsSchema, InstallPackageRequestSchema, InstallPackageResponseSchema, ListAiConversationsRequestSchema, ListAiConversationsResponseSchema, ListAiPendingActionsRequestSchema, ListAiPendingActionsResponseSchema, ListDraftsResponseSchema, ListNotificationsRequestSchema, ListNotificationsResponseSchema, ListPackagesRequestSchema, ListPackagesResponseSchema, MarkAllNotificationsReadRequestSchema, MarkAllNotificationsReadResponseSchema, MarkNotificationsReadRequestSchema, MarkNotificationsReadResponseSchema, NotificationSchema, NotificationPreferencesSchema, PublishMetaItemRequestSchema, PublishMetaItemResponseSchema, PublishPackageDraftsResponseSchema, RealtimeConnectRequestSchema, RealtimeConnectResponseSchema, RealtimeDisconnectRequestSchema, RealtimeDisconnectResponseSchema, RealtimeSubscribeRequestSchema, RealtimeSubscribeResponseSchema, RealtimeUnsubscribeRequestSchema, RealtimeUnsubscribeResponseSchema, RegisterDeviceRequestSchema, RegisterDeviceResponseSchema, RejectAiPendingActionResponseSchema, RollbackMetaItemResponseSchema, RuntimeAuthoringIssueSchema, SaveMetaItemRequestSchema, SaveMetaItemResponseSchema, SearchAllHitSchema, SearchAllResponseSchema, SetPresenceRequestSchema, SetPresenceResponseSchema, UninstallPackageRequestSchema, UninstallPackageResponseSchema, UnregisterDeviceRequestSchema, UnregisterDeviceResponseSchema, UpdateAiConversationRequestSchema, UpdateDataRequestSchema, UpdateDataResponseSchema, UpdateManyDataRequestSchema, UpdateManyDataResponseSchema, UpdateNotificationPreferencesRequestSchema, UpdateNotificationPreferencesResponseSchema, ValidateDataIssueSchema, ValidateDataRequestSchema, ValidateDataResponseSchema } from '@objectstack/spec/api';
+import type { AiAgentCapabilities, AiAgentChatRequest, AiAgentSummary, AiAgentsResponse, AiChatRequest, AiChatResponse, AiCompleteRequest, AiConversation, AiMessage, AiModelsResponse, AiPendingAction, AiPendingActionStatus, AiStreamChunk, ApproveAiPendingActionResponse, AuditMetaItemRequest, AuditMetaItemResponse, AutomationActionsResponse, AutomationTriggerRequest, AutomationTriggerResponse, BatchDataRequest, BatchDataResponse, CheckPermissionRequest, CheckPermissionResponse, CloneDataResponse, CreateAiConversationRequest, CreateDataRequest, CreateDataResponse, CreateManyDataRequest, CreateManyDataResponse, DeleteDataRequest, DeleteDataResponse, DeleteManyDataRequest, DeleteManyDataResponse, DeleteMetaItemRequest, DeleteMetaItemResponse, DiffMetaItemResponse, DisablePackageRequest, DisablePackageResponse, EnablePackageRequest, EnablePackageResponse, FindDataRequest, FindDataResponse, FindReferencesToMetaResponse, GetDataRequest, GetDataResponse, GetDiscoveryRequest, GetDiscoveryResponse, GetEffectivePermissionsRequest, GetEffectivePermissionsResponse, GetFieldLabelsRequest, GetFieldLabelsResponse, GetLocalesRequest, GetLocalesResponse, GetMetaDiagnosticsResponse, GetMetaItemCachedRequest, GetMetaItemCachedResponse, GetMetaItemLayeredRequest, GetMetaItemLayeredResponse, GetMetaItemRequest, GetMetaItemResponse, GetMetaItemsRequest, GetMetaItemsResponse, GetMetaTypesRequest, GetMetaTypesResponse, GetNotificationPreferencesRequest, GetNotificationPreferencesResponse, GetObjectPermissionsRequest, GetObjectPermissionsResponse, GetPackageRequest, GetPackageResponse, GetPresenceRequest, GetPresenceResponse, GetPublishedMetaItemResponse, GetTranslationsRequest, GetTranslationsResponse, GetUiViewRequest, GetUiViewResponse, InstallPackageRequest, InstallPackageResponse, ListAiConversationsRequest, ListAiConversationsResponse, ListAiPendingActionsRequest, ListAiPendingActionsResponse, ListDraftsResponse, ListNotificationsRequest, ListNotificationsResponse, ListPackagesRequest, ListPackagesResponse, MarkAllNotificationsReadRequest, MarkAllNotificationsReadResponse, MarkNotificationsReadRequest, MarkNotificationsReadResponse, Notification, NotificationPreferences, PublishMetaItemRequest, PublishMetaItemResponse, PublishPackageDraftsResponse, RealtimeConnectRequest, RealtimeConnectResponse, RealtimeDisconnectRequest, RealtimeDisconnectResponse, RealtimeSubscribeRequest, RealtimeSubscribeResponse, RealtimeUnsubscribeRequest, RealtimeUnsubscribeResponse, RegisterDeviceRequest, RegisterDeviceResponse, RejectAiPendingActionResponse, RollbackMetaItemResponse, RuntimeAuthoringIssue, SaveMetaItemRequest, SaveMetaItemResponse, SearchAllHit, SearchAllResponse, SetPresenceRequest, SetPresenceResponse, UninstallPackageRequest, UninstallPackageResponse, UnregisterDeviceRequest, UnregisterDeviceResponse, UpdateAiConversationRequest, UpdateDataRequest, UpdateDataResponse, UpdateManyDataRequest, UpdateManyDataResponse, UpdateNotificationPreferencesRequest, UpdateNotificationPreferencesResponse, ValidateDataIssue, ValidateDataRequest, ValidateDataResponse } from '@objectstack/spec/api';
// Validate data
const result = AiAgentCapabilitiesSchema.parse(data);
@@ -691,6 +691,45 @@ A write-path strip event: caller-supplied fields legally dropped from the payloa
| **message** | `string` | optional | |
+---
+
+## DiffMetaItemResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the diffed item. |
+| **name** | `string` | ✅ | Name of the diffed item. |
+| **fromVersion** | `number \| null` | ✅ | The older side's history version, `null` when that side is absent (e.g. the item had no earlier version). |
+| **toVersion** | `number \| null` | ✅ | The newer side's history version, `null` when that side is absent. |
+| **added** | `{ path: string; value: any }[]` | ✅ | Members present in `to` and absent in `from`. |
+| **removed** | `{ path: string; value: any }[]` | ✅ | Members present in `from` and absent in `to`. |
+| **changed** | `{ path: string; from: any; to: any }[]` | ✅ | Members present on both sides with different values. |
+
+### Nested Shape: `DiffMetaItemResponse.added[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **path** | `string` | ✅ | Dot path of the added member. |
+| **value** | `any` | ✅ | The added value. |
+
+### Nested Shape: `DiffMetaItemResponse.removed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **path** | `string` | ✅ | Dot path of the removed member. |
+| **value** | `any` | ✅ | The removed value. |
+
+### Nested Shape: `DiffMetaItemResponse.changed[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **path** | `string` | ✅ | Dot path of the changed member. |
+| **from** | `any` | ✅ | The older side's value. |
+| **to** | `any` | ✅ | The newer side's value. |
+
+
---
## DisablePackageRequest
@@ -828,6 +867,27 @@ Enable package response
| **hasMore** | `boolean` | optional | True if there are more records available (pagination). |
+---
+
+## FindReferencesToMetaResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **references** | `{ type: string; name: string; label?: string; path: string; … }[]` | ✅ | Every found reference to the addressed item. |
+
+### Nested Shape: `FindReferencesToMetaResponse.references[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the REFERRING item. |
+| **name** | `string` | ✅ | Name of the referring item. |
+| **label** | `string` | optional | Display label of the referring item, when it has one. |
+| **path** | `string` | ✅ | Where inside the referring item the reference sits (dot path). |
+| **kind** | `string` | ✅ | What kind of reference this is (e.g. which key carries it). |
+
+
---
## GetDataRequest
@@ -1057,6 +1117,37 @@ Enable package response
| **isDefault** | `boolean` | optional (default: `false`) | Whether this is the default locale |
+---
+
+## GetMetaDiagnosticsResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **entries** | `{ type: string; name: string; diagnostics: object }[]` | ✅ | One entry per item that failed validation (after filters). |
+| **total** | `number` | ✅ | Number of entries in this answer. |
+| **scannedTypes** | `number` | ✅ | How many metadata types the sweep visited. |
+| **scannedItems** | `number` | ✅ | How many items the sweep visited. |
+| **stats** | `Record` | ✅ | Per-type aggregate stats, keyed by metadata type — computed in the same sweep so a directory page renders tile counts and a package filter in one round-trip. |
+
+### Nested Shape: `GetMetaDiagnosticsResponse.entries[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type of the failing item. |
+| **name** | `string` | ✅ | Name of the failing item. |
+| **diagnostics** | `{ valid: boolean; errors?: object[]; warnings?: object[] }` | ✅ | The spec-validation verdict for the item — the same `MetadataValidationResult` the write path answers. |
+
+### Nested Shape: `GetMetaDiagnosticsResponse.stats[string]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **count** | `number` | ✅ | Items of this type present. |
+| **locked** | `number` | ✅ | Items of this type currently lock-protected. |
+| **packages** | `string[]` | ✅ | Packages contributing items of this type. |
+
+
---
## GetMetaItemCachedRequest
@@ -1427,6 +1518,13 @@ Get package response
| **metadata** | `Record` | optional | Custom presence data (e.g., current page, custom status) |
+---
+
+## GetPublishedMetaItemResponse
+
+The published metadata item body, opaque by ruling (#12038 1C). Shape is the item's own metadata-type schema, resolved at read time — never frozen into this contract.
+
+
---
## GetTranslationsRequest
@@ -1868,6 +1966,28 @@ Install package response
| **decided_at** | `string` | optional | Decision timestamp (ISO 8601) |
+---
+
+## ListDraftsResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **drafts** | `{ type: string; name: string; organizationId: string \| null; packageId: string \| null; … }[]` | ✅ | Every pending draft visible to the caller, one row per item. |
+
+### Nested Shape: `ListDraftsResponse.drafts[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **type** | `string` | ✅ | Metadata type name (canonical singular). |
+| **name** | `string` | ✅ | Item name. |
+| **organizationId** | `string \| null` | ✅ | Owning organization of the draft row, `null` for an environment-wide draft. |
+| **packageId** | `string \| null` | ✅ | Package the draft is bound to, `null` for a package-less draft. |
+| **updatedAt** | `string \| null` | ✅ | Last-touch timestamp of the draft row (ISO-8601 string), `null` when the row recorded none. |
+| **updatedBy** | `string \| null` | ✅ | Who last touched the draft, `null` when the row recorded none. |
+
+
---
## ListNotificationsRequest
@@ -2308,6 +2428,21 @@ Installed package with runtime lifecycle state
| **id** | `string` | ✅ | The rejected action id |
+---
+
+## RollbackMetaItemResponse
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **success** | `boolean` | ✅ | Whether the rollback landed. |
+| **version** | `string` | ✅ | The new live row's ADR-0008 optimistic-concurrency token — the same carrier `saveItem` returns; pass it back as `options.ifMatch`. |
+| **seq** | `number` | ✅ | The new live row's history sequence number. |
+| **restoredFromVersion** | `number` | ✅ | Which history version was restored. |
+| **message** | `string` | optional | Rollback note, when one was recorded. |
+
+
---
## RuntimeAuthoringIssue
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index ada56d08bf..56e5e99f8e 100644
--- a/content/docs/references/index.mdx
+++ b/content/docs/references/index.mdx
@@ -1,6 +1,6 @@
---
title: Protocol Reference
-description: Every schema published by @objectstack/spec — 1586 schemas across 14 protocol modules
+description: Every schema published by @objectstack/spec — 1605 schemas across 14 protocol modules
---
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with
| Module | Pages | Schemas | Description |
| :--- | ---: | ---: | :--- |
| [AI Protocol](/docs/references/ai) | 11 | 66 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
-| [API Protocol](/docs/references/api) | 29 | 423 | REST contracts, endpoints, routing, realtime, batch, discovery. |
+| [API Protocol](/docs/references/api) | 31 | 439 | REST contracts, endpoints, routing, realtime, batch, discovery. |
| [Automation Protocol](/docs/references/automation) | 13 | 68 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
| [Cloud Protocol](/docs/references/cloud) | 11 | 94 | Environments, packages and versions, marketplace, developer portal, tenancy. |
| [Data Protocol](/docs/references/data) | 29 | 166 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
@@ -31,9 +31,9 @@ counts are sums of the rows they head. Regenerate with
| [Security Protocol](/docs/references/security) | 5 | 29 | Permission sets, row-level security, sharing rules, tenancy posture. |
| [Shared Protocol](/docs/references/shared) | 8 | 32 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
-| [System Protocol](/docs/references/system) | 36 | 288 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
+| [System Protocol](/docs/references/system) | 36 | 291 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
-| **Total** | **199** | **1586** | 14 protocol modules |
+| **Total** | **201** | **1605** | 14 protocol modules |
---
@@ -61,7 +61,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
## API Protocol
-**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **29 pages, 423 schemas**
+**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 439 schemas**
REST contracts, endpoints, routing, realtime, batch, discovery.
@@ -83,10 +83,12 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
| [`export.zod.ts`](/docs/references/api/export) | `CreateExportJobRequest`, `CreateExportJobResponse`, `CreateImportJobRequest`, `CreateImportJobResponse`, `DeduplicationStrategy`, `ExportFormat`, `ExportImportTemplate`, `ExportJobProgress`, `ExportJobStatus`, `ExportJobSummary`, `FieldMappingEntry`, `GetExportJobDownloadRequest`, `GetExportJobDownloadResponse`, `ImportJobProgress`, `ImportJobResults`, `ImportJobStatus`, `ImportJobSummary`, `ImportMapping`, `ImportRequest`, `ImportResponse`, `ImportRowResult`, `ImportValidationConfig`, `ImportValidationMode`, `ImportValidationResult`, `ImportWriteMode`, `ListExportJobsRequest`, `ListExportJobsResponse`, `ListImportJobsRequest`, `ListImportJobsResponse`, `ScheduleExportRequest`, `ScheduleExportResponse`, `ScheduledExport`, `UndoImportJobResponse` |
| [`http-cache.zod.ts`](/docs/references/api/http-cache) | `CacheControl`, `CacheDirective`, `CacheInvalidationRequest`, `CacheInvalidationResponse`, `CacheInvalidationTarget`, `ETag`, `MetadataCacheRequest`, `MetadataCacheResponse` |
| [`metadata.zod.ts`](/docs/references/api/metadata) | `AppDefinitionResponse`, `ConceptListResponse`, `MetadataBulkRegisterRequest`, `MetadataBulkResponse`, `MetadataBulkUnregisterRequest`, `MetadataDeleteResponse`, `MetadataDependenciesResponse`, `MetadataDependentsResponse`, `MetadataEffectiveResponse`, `MetadataExistsResponse`, `MetadataExportRequest`, `MetadataExportResponse`, `MetadataImportRequest`, `MetadataImportResponse`, `MetadataItemResponse`, `MetadataListResponse`, `MetadataNamesResponse`, `MetadataOverlayResponse`, `MetadataOverlaySaveRequest`, `MetadataQueryRequest`, `MetadataQueryResponse`, `MetadataRegisterRequest`, `MetadataTypeInfoResponse`, `MetadataTypesResponse`, `MetadataValidateRequest`, `MetadataValidateResponse`, `ObjectDefinitionResponse` |
+| [`misc`](/docs/references/api/misc) *(no single source file)* | `ResolvedBook`, `ResolvedEntry`, `ResolvedGroup` |
| [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` |
-| [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageRollbackResponse`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` |
+| [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` |
+| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
| [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `HandlerStatus`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `RouteCoverageEntry`, `RouteCoverageReport`, `ValidationMode` |
-| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
+| [`protocol.zod.ts`](/docs/references/api/protocol) | `AiAgentCapabilities`, `AiAgentChatRequest`, `AiAgentSummary`, `AiAgentsResponse`, `AiChatRequest`, `AiChatResponse`, `AiCompleteRequest`, `AiConversation`, `AiMessage`, `AiModelsResponse`, `AiPendingAction`, `AiPendingActionStatus`, `AiStreamChunk`, `ApproveAiPendingActionResponse`, `AuditMetaItemRequest`, `AuditMetaItemResponse`, `AutomationActionsResponse`, `AutomationTriggerRequest`, `AutomationTriggerResponse`, `BatchDataRequest`, `BatchDataResponse`, `CheckPermissionRequest`, `CheckPermissionResponse`, `CloneDataResponse`, `CreateAiConversationRequest`, `CreateDataRequest`, `CreateDataResponse`, `CreateManyDataRequest`, `CreateManyDataResponse`, `DeleteDataRequest`, `DeleteDataResponse`, `DeleteManyDataRequest`, `DeleteManyDataResponse`, `DeleteMetaItemRequest`, `DeleteMetaItemResponse`, `DiffMetaItemResponse`, `DisablePackageRequest`, `DisablePackageResponse`, `EnablePackageRequest`, `EnablePackageResponse`, `FindDataRequest`, `FindDataResponse`, `FindReferencesToMetaResponse`, `GetDataRequest`, `GetDataResponse`, `GetDiscoveryRequest`, `GetDiscoveryResponse`, `GetEffectivePermissionsRequest`, `GetEffectivePermissionsResponse`, `GetFieldLabelsRequest`, `GetFieldLabelsResponse`, `GetLocalesRequest`, `GetLocalesResponse`, `GetMetaDiagnosticsResponse`, `GetMetaItemCachedRequest`, `GetMetaItemCachedResponse`, `GetMetaItemLayeredRequest`, `GetMetaItemLayeredResponse`, `GetMetaItemRequest`, `GetMetaItemResponse`, `GetMetaItemsRequest`, `GetMetaItemsResponse`, `GetMetaTypesRequest`, `GetMetaTypesResponse`, `GetNotificationPreferencesRequest`, `GetNotificationPreferencesResponse`, `GetObjectPermissionsRequest`, `GetObjectPermissionsResponse`, `GetPackageRequest`, `GetPackageResponse`, `GetPresenceRequest`, `GetPresenceResponse`, `GetPublishedMetaItemResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `Notification`, `NotificationPreferences`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsResponse`, `RealtimeConnectRequest`, `RealtimeConnectResponse`, `RealtimeDisconnectRequest`, `RealtimeDisconnectResponse`, `RealtimeSubscribeRequest`, `RealtimeSubscribeResponse`, `RealtimeUnsubscribeRequest`, `RealtimeUnsubscribeResponse`, `RegisterDeviceRequest`, `RegisterDeviceResponse`, `RejectAiPendingActionResponse`, `RollbackMetaItemResponse`, `RuntimeAuthoringIssue`, `SaveMetaItemRequest`, `SaveMetaItemResponse`, `SearchAllHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
| [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` |
| [`realtime.zod.ts`](/docs/references/api/realtime) | `RealtimeConfig`, `RealtimeEvent`, `RealtimeEventType`, `RealtimePresence`, `Subscription`, `SubscriptionEvent`, `TransportProtocol` |
| [`realtime-shared.zod.ts`](/docs/references/api/realtime-shared) | `BasePresence`, `PresenceStatus`, `RealtimeRecordAction` |
@@ -318,7 +320,7 @@ Studio designer metadata — the authoring surfaces for the protocols above.
## System Protocol
-**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **36 pages, 288 schemas**
+**Source:** `packages/spec/src/system/` · **Import:** `@objectstack/spec/system` · **36 pages, 291 schemas**
The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance.
@@ -326,7 +328,7 @@ The runtime environment — logging, jobs, cache, metrics, notifications, i18n a
| :--- | :--- |
| [`app-install.zod.ts`](/docs/references/system/app-install) | `AppCompatibilityCheck`, `AppInstallRequest`, `AppInstallResult`, `AppManifest` |
| [`auth-config.zod.ts`](/docs/references/system/auth-config) | `AdvancedAuthConfig`, `AudienceConfig`, `AuthConfig`, `AuthPluginConfig`, `AuthProviderConfig`, `EmailAndPasswordConfig`, `EmailVerificationConfig`, `MutualTLSConfig`, `OidcProviderConfig`, `OidcProvidersConfig`, `SocialProviderConfig` |
-| [`book.zod.ts`](/docs/references/system/book) | `Book`, `BookAudience`, `BookGroup`, `BookInclude`, `BookNode` |
+| [`book.zod.ts`](/docs/references/system/book) | `Book`, `BookAudience`, `BookGroup`, `BookInclude`, `BookNode`, `ResolvedBook`, `ResolvedEntry`, `ResolvedGroup` |
| [`cache.zod.ts`](/docs/references/system/cache) | `CacheAvalanchePrevention`, `CacheConfig`, `CacheConsistency`, `CacheInvalidation`, `CacheStrategy`, `CacheTier`, `CacheWarmup`, `DistributedCacheConfig` |
| [`change-management.zod.ts`](/docs/references/system/change-management) | `ChangeImpact`, `ChangePriority`, `ChangeRequest`, `ChangeStatus`, `ChangeType`, `RollbackPlan` |
| [`collaboration.zod.ts`](/docs/references/system/collaboration) | `AwarenessEvent`, `AwarenessSession`, `AwarenessUpdate`, `AwarenessUserState`, `CRDTMergeResult`, `CRDTState`, `CRDTType`, `CollaborationMode`, `CollaborationSession`, `CollaborationSessionConfig`, `CollaborativeCursor`, `CounterOperation`, `CursorColorPreset`, `CursorSelection`, `CursorStyle`, `CursorUpdate`, `GCounter`, `LWWRegister`, `ORSet`, `ORSetElement`, `OTComponent`, `OTOperation`, `OTOperationType`, `OTTransformResult`, `PNCounter`, `TextCRDTOperation`, `TextCRDTState`, `UserActivityStatus`, `VectorClock` |
diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx
index eed264a5c3..73f4b56fde 100644
--- a/content/docs/references/system/book.mdx
+++ b/content/docs/references/system/book.mdx
@@ -30,8 +30,8 @@ only per-doc storage is the scalar `doc.order`, which merges cleanly.
## TypeScript Usage
```typescript
-import { BookSchema, BookAudienceSchema, BookGroupSchema, BookIncludeSchema, BookNodeSchema } from '@objectstack/spec/system';
-import type { Book, BookAudience, BookGroup, BookInclude, BookNode } from '@objectstack/spec/system';
+import { BookSchema, BookAudienceSchema, BookGroupSchema, BookIncludeSchema, BookNodeSchema, ResolvedBookSchema, ResolvedEntrySchema, ResolvedGroupSchema } from '@objectstack/spec/system';
+import type { Book, BookAudience, BookGroup, BookInclude, BookNode, ResolvedBook, ResolvedEntry, ResolvedGroup } from '@objectstack/spec/system';
// Validate data
const result = BookSchema.parse(data);
@@ -192,3 +192,66 @@ Type: `string`
---
+## ResolvedBook
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **name** | `string` | ✅ | The book's machine name. |
+| **label** | `string` | optional | The book's display label, when it declares one. |
+| **groups** | `{ key: string; label: string; entries: object[] }[]` | ✅ | The resolved groups, in render order. |
+
+### Nested Shape: `ResolvedBook.groups[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **key** | `string` | ✅ | The group's key (from the spine, or `uncategorized`). |
+| **label** | `string` | ✅ | The group's display label. |
+| **entries** | `{ doc?: string; href?: string; label?: string; description?: string; … }[]` | ✅ | The group's resolved entries, in render order. |
+
+
+---
+
+## ResolvedEntry
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **doc** | `string` | optional | Doc name, or undefined for an external link / separator. |
+| **href** | `string` | optional | External link target, when the entry is a link. |
+| **label** | `string` | optional | Display label, when one resolved. |
+| **description** | `string` | optional | Doc description, when one resolved. |
+| **badge** | `string` | optional | Badge text (e.g. "beta"), when declared. |
+| **icon** | `string` | optional | Icon name, when declared. |
+| **separator** | `boolean` | optional | True for a `---` separator node. |
+
+
+---
+
+## ResolvedGroup
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **key** | `string` | ✅ | The group's key (from the spine, or `uncategorized`). |
+| **label** | `string` | ✅ | The group's display label. |
+| **entries** | `{ doc?: string; href?: string; label?: string; description?: string; … }[]` | ✅ | The group's resolved entries, in render order. |
+
+### Nested Shape: `ResolvedGroup.entries[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **doc** | `string` | optional | Doc name, or undefined for an external link / separator. |
+| **href** | `string` | optional | External link target, when the entry is a link. |
+| **label** | `string` | optional | Display label, when one resolved. |
+| **description** | `string` | optional | Doc description, when one resolved. |
+| **badge** | `string` | optional | Badge text (e.g. "beta"), when declared. |
+| **icon** | `string` | optional | Icon name, when declared. |
+| **separator** | `boolean` | optional | True for a `---` separator node. |
+
+
+---
+
diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
index 822d858d9e..628dfb2c8a 100644
--- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
+++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md
@@ -257,11 +257,11 @@ directory rather than per file.
| Dir | Sites |
|---|---|
| `ai/` | 77 |
-| `api/` | 416 |
+| `api/` | 444 |
| `cloud/` | 83 |
| `identity/` | 32 |
| `integration/` | 10 |
| `kernel/` | 272 |
| `qa/` | 6 |
| `shared/` | 20 |
-| `system/` | 361 |
+| `system/` | 364 |
diff --git a/packages/client/exported-any-returns.json b/packages/client/exported-any-returns.json
index aeeefea169..5d3ea7d5e2 100644
--- a/packages/client/exported-any-returns.json
+++ b/packages/client/exported-any-returns.json
@@ -1,26 +1,11 @@
{
"$comment": "Exported callables of @objectstack/client whose AWAITED return type resolves to `any` (#11927). Judged against the BUILT dist by `pnpm --filter @objectstack/client check:exported-any-returns`, because the erasure is invisible in source text when a method carries no return annotation. SHRINK-ONLY and EXACT in both directions: a site here that no longer resolves to `any` is RED until its entry is deleted, and a site NOT here that resolves to `any` is RED — that unlisted case is the everyday one and the reason this file exists. There is deliberately NO --update flag: every entry is debt with a name on it, and a reason a tool wrote is a silencer rather than a worklist. SCOPE, and the one exclusion worth stating out loud: a return type that CONTAINS `any` (`{ packages: any[]; total: number }`, `Promise>`) is not listed, because it is not flagged — the gate asks whether the type IS `any`, the same line packages/spec's check:exported-any draws, and admitting the broader question costs the gate its zero-false-positive property. That is why 21 of #11925's 38 unannotated methods are absent here: they are `any`-CONTAINING, and they remain #11925's to close. Nothing is silently absorbed in either direction. A caller-supplied `` is likewise never listed: the record type and the action payload really are the caller's, and flagging them is the pressure that turns a correct generic into a wrong concrete type.",
"entries": {
- "ObjectStackClient.meta.getPublished": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.listDrafts": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.meta.migrateStored": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.getDiagnostics": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.getReferences": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.getBookTree": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.getAudit": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.rollbackItem": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.meta.diffItem": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.analytics.query": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.analytics.meta": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.analytics.explain": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.analytics.queryDataset": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.publish": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.discardDrafts": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.revertCommit": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.rollback": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.export": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.adoptOrphans": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
- "ObjectStackClient.packages.duplicate": "#11925 — no return annotation; the published type comes from `this.unwrapResponse(res)`. Invisible to a `Promise<` grep because the text never appears in the method. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.organizations.create": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.organizations.update": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
"ObjectStackClient.organizations.setActive": "#12104 — no return annotation; `return res.json()`, and lib.dom declares `Response.json(): Promise`. Invisible to every grep #8140's census and #11925 used: the method names neither `any` nor `Promise` nor `unwrapResponse`. Bind the contract the route actually answers, minding the envelope.",
diff --git a/packages/client/src/client.test.ts b/packages/client/src/client.test.ts
index 72ca94c7fd..8d203ac7b1 100644
--- a/packages/client/src/client.test.ts
+++ b/packages/client/src/client.test.ts
@@ -260,7 +260,13 @@ describe('ObjectStackClient', () => {
});
it('meta.getDiagnostics pins GET /meta/diagnostics with its query params', async () => {
- const { client, fetchMock } = createMockClient({ success: true, data: { items: [] } });
+ // [#12038] Producer-true mock: `getMetaDiagnostics` answers
+ // `{ entries, total, scannedTypes, scannedItems, stats }` — the old
+ // `{ items: [] }` mock was an invented shape the route never answered.
+ const { client, fetchMock } = createMockClient({
+ success: true,
+ data: { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} },
+ });
await client.meta.getDiagnostics();
expect(String(fetchMock.mock.calls[0][0])).toBe(
'http://localhost:3000/api/v1/meta/diagnostics',
@@ -280,7 +286,13 @@ describe('ObjectStackClient', () => {
});
it('meta.getBookTree pins GET /meta/book/:name/tree', async () => {
- const { client, fetchMock } = createMockClient({ success: true, data: { tree: [] } });
+ // [#12038] Producer-true mock: the route answers `resolveBookTree()`'s
+ // `ResolvedBook` (`{ name, label?, groups }`) — the old `{ tree: [] }`
+ // mock was an invented shape the route never answered.
+ const { client, fetchMock } = createMockClient({
+ success: true,
+ data: { name: 'handbook', groups: [] },
+ });
await client.meta.getBookTree('handbook', { packageId: 'com.example.docs' });
expect(String(fetchMock.mock.calls[0][0])).toBe(
'http://localhost:3000/api/v1/meta/book/handbook/tree?package=com.example.docs',
@@ -305,7 +317,14 @@ describe('ObjectStackClient', () => {
});
it('meta.rollbackItem pins POST /meta/:type/:name/rollback with toVersion', async () => {
- const { client, fetchMock } = createMockClient({ success: true, data: { restored: true } });
+ // [#12038] Producer-true mock: `rollbackMetaItem` answers
+ // `{ success, version, seq, restoredFromVersion, message? }` — the old
+ // `{ restored: true }` mock was an invented shape the route never
+ // answered.
+ const { client, fetchMock } = createMockClient({
+ success: true,
+ data: { success: true, version: 'W/"4"', seq: 4, restoredFromVersion: 3 },
+ });
await client.meta.rollbackItem('object', 'customer', 3);
const [url, init] = fetchMock.mock.calls[0];
expect(String(url)).toBe('http://localhost:3000/api/v1/meta/object/customer/rollback');
@@ -314,7 +333,14 @@ describe('ObjectStackClient', () => {
});
it('meta.diffItem pins GET /meta/:type/:name/diff with from/to', async () => {
- const { client, fetchMock } = createMockClient({ success: true, data: { changes: [] } });
+ // [#12038] Producer-true mock: `diffMetaItem` answers
+ // `{ type, name, fromVersion, toVersion, added, removed, changed }` —
+ // the old `{ changes: [] }` mock was an invented shape the route never
+ // answered.
+ const { client, fetchMock } = createMockClient({
+ success: true,
+ data: { type: 'object', name: 'customer', fromVersion: 2, toVersion: 5, added: [], removed: [], changed: [] },
+ });
await client.meta.diffItem('object', 'customer', { from: 2, to: 5 });
expect(String(fetchMock.mock.calls[0][0])).toBe(
'http://localhost:3000/api/v1/meta/object/customer/diff?from=2&to=5',
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index 073f14e578..322eb60703 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -80,6 +80,27 @@ import {
// [#11924] The GLOBAL cross-object search body — NOT the per-object
// `SearchResult` in `@objectstack/spec/contracts` (the #8140 near-miss trap).
SearchAllResponse,
+ // [#12038] The meta history/diagnostics and package lifecycle response
+ // contracts, bound under the recorded ruling (1C/2C/3A/4A/5A). Each is the
+ // PAYLOAD the route answers — the value after `unwrapResponse` strips the
+ // dispatcher's `{ success, data }` envelope, and the whole bare body where
+ // the REST server answers without one — so one type is true on both
+ // surfaces (survey §8.1).
+ GetPublishedMetaItemResponse,
+ ListDraftsResponse,
+ GetMetaDiagnosticsResponse,
+ FindReferencesToMetaResponse,
+ AuditMetaItemResponse,
+ RollbackMetaItemResponse,
+ DiffMetaItemResponse,
+ PackagePublishResult,
+ DiscardPackageDraftsResponse,
+ ListPackageCommitsResponse,
+ RevertPackageCommitResponse,
+ RollbackToPackageCommitResponse,
+ PackageExportManifest,
+ ReassignOrphanedMetadataResponse,
+ DuplicatePackageResponse,
} from '@objectstack/spec/api';
import type {
ApprovalRequestRow,
@@ -121,6 +142,10 @@ import type {
} from '@objectstack/spec/automation';
import type { ExternalCatalog } from '@objectstack/spec/data';
import type { InstalledPackage } from '@objectstack/spec/kernel';
+// [#12038] The resolved book tree `meta.getBookTree` answers — declared beside
+// its resolver in spec `system/book.zod.ts` (its schema is re-exported into
+// `/api` for the route-ledger resolver; the type lives on the system entry).
+import type { ResolvedBook } from '@objectstack/spec/system';
import type { ConnectorDescriptor } from '@objectstack/spec/integration';
import type { ExplainDecision } from '@objectstack/spec/security';
import type { InvitationStatus } from '@objectstack/spec/identity';
@@ -1076,24 +1101,20 @@ export class ObjectStackClient {
/* [#3563 PR-5] The three meta routes that had no SDK expression. */
/**
- * ⛔ [#11925] The nine `meta.*` history / diagnostics methods below keep
- * `unwrapResponse< any >` DELIBERATELY — Class C, a missing contract
- * rather than a missing annotation (#12038).
+ * [#12038] Eight of the nine `meta.*` history / diagnostics methods below
+ * are BOUND — schema first (`@objectstack/spec/api`), ledger row second,
+ * annotation last, the #7294 order — under the recorded five-part ruling
+ * (1C · 2C · 3A · 4A · 5A). Each annotation names the PAYLOAD: the value
+ * after `unwrapResponse` strips the dispatcher envelope, and the whole
+ * bare body where the REST server answers without one, so one type is
+ * true on both surfaces.
*
- * `getPublished`, `listDrafts`, `migrateStored`, `getDiagnostics`,
- * `getReferences`, `getBookTree`, `getAudit`, `rollbackItem`, `diffItem`.
- *
- * Nothing in `@objectstack/spec` declares any of these nine route
- * responses: every ledger row reports `responseSchema=None`, and the
- * producers hand back whatever the protocol service returns. The one named
- * type that exists — `StoredMigrationReport`, which `migrateStored`'s
- * docblock points at — lives in `@objectstack/metadata-protocol`, which is
- * not a dependency of this package.
- *
- * `publishItem` below is the counter-example and the precedent: it is
- * annotated only because #7294 declared `PublishMetaItemResponseSchema`
- * first. Same order applies here — schema, then ledger row, then
- * annotation. ⛔ Do not reach for a same-named neighbour instead.
+ * The exception is `migrateStored` (ruling 2C): it stays
+ * `unwrapResponse< any >` DELIBERATELY — its report's only named type,
+ * `StoredMigrationReport`, lives in `@objectstack/metadata-protocol`,
+ * which is not a dependency of this package, and a second declaration in
+ * spec would drift against the CLI that renders the same report. Its two
+ * ledger rows carry the same note. ⛔ Do not re-declare it here.
*/
/**
@@ -1107,24 +1128,28 @@ export class ObjectStackClient {
* slash-bearing name is refused at the publish door (#12194), so there is
* one spelling and one door.
*/
- getPublished: async (type: string, name: string) => {
+ getPublished: async (type: string, name: string): Promise => {
+ // [#12038 ruling 1C] `GetPublishedMetaItemResponse` is `unknown` BY
+ // RULING, not by omission: the route answers an arbitrary metadata
+ // item body, and freezing a union over today's type registry was
+ // explicitly refused. Callers narrow with their own type knowledge.
const route = this.getRoute('metadata');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/published`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
* ADR-0033: pending drafts — metadata authored (e.g. by an AI) but not
* yet published, which the active-only item lists hide.
*/
- listDrafts: async (opts?: { packageId?: string; type?: string }) => {
+ listDrafts: async (opts?: { packageId?: string; type?: string }): Promise => {
const route = this.getRoute('metadata');
const params = new URLSearchParams();
if (opts?.packageId) params.set('packageId', opts.packageId);
if (opts?.type) params.set('type', opts.type);
const qs = params.toString();
const res = await this.fetch(`${this.baseUrl}${route}/_drafts${qs ? `?${qs}` : ''}`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
@@ -1140,6 +1165,12 @@ export class ObjectStackClient {
*
* Requires the `manage_metadata` capability (403 otherwise) — it rewrites
* every eligible row in the deployment, not one item.
+ *
+ * [#12038 ruling 2C] DELIBERATELY UNBOUND — the one method in this family
+ * that keeps `unwrapResponse< any >`: `StoredMigrationReport` lives in
+ * `@objectstack/metadata-protocol`, which this package does not depend
+ * on, and a second declaration in spec would drift against the CLI that
+ * renders the same report. Both of its ledger rows carry the same note.
*/
migrateStored: async (opts?: { apply?: boolean; types?: string[] }) => {
const route = this.getRoute('metadata');
@@ -1173,7 +1204,7 @@ export class ObjectStackClient {
* registered Zod schema. Powers governance dashboards and doctor-style
* checks. 501s on kernels without `getMetaDiagnostics`.
*/
- getDiagnostics: async (opts?: { type?: string; severity?: 'error' | 'warning'; packageId?: string }) => {
+ getDiagnostics: async (opts?: { type?: string; severity?: 'error' | 'warning'; packageId?: string }): Promise => {
const route = this.getRoute('metadata');
const params = new URLSearchParams();
if (opts?.type) params.set('type', opts.type);
@@ -1181,28 +1212,28 @@ export class ObjectStackClient {
if (opts?.packageId) params.set('package', opts.packageId);
const qs = params.toString();
const res = await this.fetch(`${this.baseUrl}${route}/diagnostics${qs ? `?${qs}` : ''}`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
* Reverse references: metadata items that reference `type`/`name`.
* `{ references: [] }` on kernels without reference tracking.
*/
- getReferences: async (type: string, name: string) => {
+ getReferences: async (type: string, name: string): Promise => {
const route = this.getRoute('metadata');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/references`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
* ADR-0046 §6: resolve a book spine against the docs that exist now.
* An unknown name is treated as a package id (implicit per-package book).
*/
- getBookTree: async (name: string, opts?: { packageId?: string }) => {
+ getBookTree: async (name: string, opts?: { packageId?: string }): Promise => {
const route = this.getRoute('metadata');
const qs = opts?.packageId ? `?package=${encodeURIComponent(opts.packageId)}` : '';
const res = await this.fetch(`${this.baseUrl}${route}/book/${encodeURIComponent(name)}/tree${qs}`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
@@ -1210,11 +1241,11 @@ export class ObjectStackClient {
* save/publish/rollback/delete/reset attempts, allowed and denied.
* `{ events: [] }` where the audit table is not provisioned.
*/
- getAudit: async (type: string, name: string, opts?: { limit?: number }) => {
+ getAudit: async (type: string, name: string, opts?: { limit?: number }): Promise => {
const route = this.getRoute('metadata');
const qs = opts?.limit !== undefined ? `?limit=${opts.limit}` : '';
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs}`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
@@ -1253,27 +1284,27 @@ export class ObjectStackClient {
/**
* Restore the body at history version `toVersion` as the new live row.
*/
- rollbackItem: async (type: string, name: string, toVersion: number, opts?: { message?: string }) => {
+ rollbackItem: async (type: string, name: string, toVersion: number, opts?: { message?: string }): Promise => {
const route = this.getRoute('metadata');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`, {
method: 'POST',
body: JSON.stringify({ toVersion, ...(opts?.message ? { message: opts.message } : {}) }),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
* Structural diff between two history versions (`from`/`to`); omit both
* for previous-vs-current.
*/
- diffItem: async (type: string, name: string, opts?: { from?: number; to?: number }) => {
+ diffItem: async (type: string, name: string, opts?: { from?: number; to?: number }): Promise => {
const route = this.getRoute('metadata');
const params = new URLSearchParams();
if (opts?.from !== undefined) params.set('from', String(opts.from));
if (opts?.to !== undefined) params.set('to', String(opts.to));
const qs = params.toString();
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ''}`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
}
};
@@ -1604,32 +1635,38 @@ export class ObjectStackClient {
},
/**
- * ⛔ [#11925] The eight methods from here down — `publish`,
- * `discardDrafts`, `listCommits`, `revertCommit`, `rollback`, `export`,
- * `adoptOrphans`, `duplicate` — keep `unwrapResponse< any >` on purpose:
- * Class C, a missing contract (#12038). Their handlers reach the protocol
- * service through an `any` cast (`(protocol as any).listCommits`,
- * `.revertCommit`, `.rollbackToPackageCommit`, `.duplicatePackage`, …), so
- * there is no declared type anywhere on the path to lift, and every ledger
- * row reports `responseSchema=None`.
+ * [#12038] The eight methods from here down — `publish`, `discardDrafts`,
+ * `listCommits`, `revertCommit`, `rollback`, `export`, `adoptOrphans`,
+ * `duplicate` — are BOUND under the recorded five-part ruling. The
+ * handlers' `(protocol as any)` casts erase types their producers declare
+ * completely a few files away, so every one of these annotations is a
+ * describe-only transcription of that producer shape, published in
+ * `@objectstack/spec/api` with conformance coverage first, then named by
+ * the route-ledger row, then annotated here (the #7294 order). All eight
+ * routes are dispatcher-only; each type is the payload `unwrapResponse`
+ * yields after stripping the `{ success, data }` envelope.
*
- * ⭐ `rollback` in particular: `PackageRollbackResponse` sits one import
- * away in `@objectstack/spec/api` and is the WRONG type for it. That
- * schema declares `{ success, restoredVersion?, message? }` — a VERSION
- * rollback — while this method posts `{ commitId }` and the dispatcher
- * routes it to `rollbackToPackageCommit`, the ADR-0067 COMMIT rollback.
- * Binding it would compile and be false. `return-type-precision.test.ts`
- * holds a compile-time guard against that substitution.
+ * ⭐ `rollback`: the near-miss `PackageRollbackResponse` — a VERSION
+ * rollback declaration the spec had bound to this route's exact live path
+ * — is RETIRED (ruling 3A, ADR-0087 discipline); the binding below is the
+ * true COMMIT-rollback contract, `RollbackToPackageCommitResponse`.
+ * `return-type-precision.test.ts` guards the shape distinction.
*/
- /** Publish the package's metadata snapshot. */
- publish: async (id: string, opts?: Record) => {
+ /**
+ * Publish the package's metadata snapshot. The one method in this family
+ * whose producer (`MetadataManager.publishPackage`) already had an exact
+ * published schema — `PackagePublishResultSchema`, declared in
+ * `@objectstack/spec/system` and re-exported into `/api` (#12038 ruling
+ * 5A, never a second copy).
+ */
+ publish: async (id: string, opts?: Record): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/publish`, {
method: 'POST',
body: JSON.stringify(opts ?? {}),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
@@ -1659,40 +1696,44 @@ export class ObjectStackClient {
},
/** ADR-0033: drop every pending draft bound to the package. */
- discardDrafts: async (id: string, opts?: { actor?: string }) => {
+ discardDrafts: async (id: string, opts?: { actor?: string }): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/discard-drafts`, {
method: 'POST',
body: JSON.stringify(opts ?? {}),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
- /** ADR-0067: the package's commit timeline (newest-first). */
- listCommits: async (id: string) => {
+ /**
+ * ADR-0067: the package's commit timeline (newest-first). The `commits`
+ * wrapper is the HANDLER's (the producer returns a bare array) — declared
+ * as such in `ListPackageCommitsResponseSchema`.
+ */
+ listCommits: async (id: string): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/commits`);
- return this.unwrapResponse<{ commits: any[] }>(res);
+ return this.unwrapResponse(res);
},
/** ADR-0067: revert ONE commit (the revert is itself a commit). */
- revertCommit: async (id: string, commitId: string, opts?: { actor?: string }) => {
+ revertCommit: async (id: string, commitId: string, opts?: { actor?: string }): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(
`${this.baseUrl}${route}/${encodeURIComponent(id)}/commits/${encodeURIComponent(commitId)}/revert`,
{ method: 'POST', body: JSON.stringify(opts ?? {}) },
);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/** ADR-0067: roll back through all commits newer than `commitId`. */
- rollback: async (id: string, commitId: string, opts?: { actor?: string }) => {
+ rollback: async (id: string, commitId: string, opts?: { actor?: string }): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/rollback`, {
method: 'POST',
body: JSON.stringify({ commitId, ...(opts ?? {}) }),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/** Revert the package to its last published state. */
@@ -1708,20 +1749,24 @@ export class ObjectStackClient {
* ADR-0070: assemble the package's portable manifest (offline export) —
* the same shape `marketplace-install-local` consumes.
*/
- export: async (id: string) => {
+ export: async (id: string): Promise => {
+ // [#12038 ruling 4A] Four fixed keys (`id`, `name`, `version`,
+ // `label?`) plus an open catch-all — the remaining keys are derived
+ // from the metadata type registry at runtime and deliberately not
+ // frozen into the contract.
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/export`);
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/** ADR-0070 D5: bulk-rebind package-less (orphaned) metadata into this base. */
- adoptOrphans: async (id: string, opts?: { actor?: string }) => {
+ adoptOrphans: async (id: string, opts?: { actor?: string }): Promise => {
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/adopt-orphans`, {
method: 'POST',
body: JSON.stringify(opts ?? {}),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
/**
@@ -1733,13 +1778,17 @@ export class ObjectStackClient {
id: string,
targetPackageId: string,
opts?: { targetName?: string; targetNamespace?: string; actor?: string },
- ) => {
+ ): Promise => {
+ // ⚠ `success` on the returned payload is the OPERATION's verdict
+ // (false for a partial or empty duplicate) — the envelope-level
+ // `success` this method strips is transport-level and always true on
+ // a 200 (the objectui#6593 confusion, ended by this declaration).
const route = this.getRoute('packages');
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/duplicate`, {
method: 'POST',
body: JSON.stringify({ targetPackageId, ...(opts ?? {}) }),
});
- return this.unwrapResponse(res);
+ return this.unwrapResponse(res);
},
};
diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts
index b7da7996f4..5d35257880 100644
--- a/packages/client/src/return-type-precision.test.ts
+++ b/packages/client/src/return-type-precision.test.ts
@@ -50,7 +50,23 @@ import type {
import type { ActionDescriptor, ExecutionLog, FlowParsed } from '@objectstack/spec/automation';
import type { ExplainDecision } from '@objectstack/spec/security';
import type { InstalledPackage } from '@objectstack/spec/kernel';
-import type { PackageRollbackResponse } from '@objectstack/spec/api';
+import type {
+ ListDraftsResponse,
+ GetMetaDiagnosticsResponse,
+ FindReferencesToMetaResponse,
+ AuditMetaItemResponse,
+ RollbackMetaItemResponse,
+ DiffMetaItemResponse,
+ PackagePublishResult,
+ DiscardPackageDraftsResponse,
+ ListPackageCommitsResponse,
+ RevertPackageCommitResponse,
+ RollbackToPackageCommitResponse,
+ PackageExportManifest,
+ ReassignOrphanedMetadataResponse,
+ DuplicatePackageResponse,
+} from '@objectstack/spec/api';
+import type { ResolvedBook } from '@objectstack/spec/system';
import type { Environment } from '@objectstack/spec/cloud';
declare const client: ObjectStackClient;
@@ -285,18 +301,80 @@ export async function returnTypePrecisionPins11925(): Promise {
void wrongScopedGet;
}
+/**
+ * [#12038] The 16 bindings of the recorded five-part ruling (1C · 2C · 3A ·
+ * 4A · 5A): the meta.* history/diagnostics family and the packages.*
+ * lifecycle family, each bound to the describe-only transcription of its
+ * producer's declared return (`@objectstack/spec/api`), schema → ledger row →
+ * annotation, in that order. Type-level for the reason this file's header
+ * gives; each `toEqualTypeOf` is red while the method still returns `any`.
+ */
+export async function returnTypePrecisionPins12038(): Promise {
+ // ── the meta.* eight ──────────────────────────────────────────────────
+ expectTypeOf(await client.meta.listDrafts()).toEqualTypeOf();
+ expectTypeOf(await client.meta.getDiagnostics()).toEqualTypeOf();
+ expectTypeOf(await client.meta.getReferences('view', 'account_list')).toEqualTypeOf();
+ expectTypeOf(await client.meta.getBookTree('handbook')).toEqualTypeOf();
+ expectTypeOf(await client.meta.getAudit('view', 'account_list')).toEqualTypeOf();
+ expectTypeOf(await client.meta.rollbackItem('view', 'account_list', 3)).toEqualTypeOf();
+ expectTypeOf(await client.meta.diffItem('view', 'account_list')).toEqualTypeOf();
+ // Ruling 1C: `getPublished` is bound to `unknown` BY RULING — an
+ // arbitrary metadata item body, never a union frozen against the type
+ // registry. `unknown` (not `any`) is the binding: callers must narrow.
+ expectTypeOf(await client.meta.getPublished('view', 'account_list')).toEqualTypeOf();
+ // Ruling 2C: `migrateStored` is the one method that STAYS unbound — its
+ // report's only named type lives in `@objectstack/metadata-protocol`,
+ // which this package does not depend on. Pinned so a well-meaning sweep
+ // does not "fix" it against the ruling.
+ expectTypeOf(await client.meta.migrateStored()).toBeAny();
+
+ // ── the packages.* eight ──────────────────────────────────────────────
+ expectTypeOf(await client.packages.publish('com.acme.crm')).toEqualTypeOf();
+ expectTypeOf(await client.packages.discardDrafts('com.acme.crm')).toEqualTypeOf();
+ expectTypeOf(await client.packages.listCommits('com.acme.crm')).toEqualTypeOf();
+ expectTypeOf(await client.packages.revertCommit('com.acme.crm', 'cmt_1')).toEqualTypeOf();
+ expectTypeOf(await client.packages.rollback('com.acme.crm', 'cmt_1')).toEqualTypeOf();
+ expectTypeOf(await client.packages.export('com.acme.crm')).toEqualTypeOf();
+ expectTypeOf(await client.packages.adoptOrphans('com.acme.crm')).toEqualTypeOf();
+ expectTypeOf(await client.packages.duplicate('com.acme.crm', 'com.acme.copy')).toEqualTypeOf();
+
+ // ── direction 2: a WRONG shape must now be rejected ───────────────────
+ // Each suppression is unused — a TS2578 error — while the method still
+ // returns `any`.
+
+ // @ts-expect-error the timeline is enveloped in `{ commits }` (the handler's wrapper), not a bare array
+ const wrongCommits: ListPackageCommitsResponse['commits'] = await client.packages.listCommits('com.acme.crm');
+
+ // The 3A distinction, pinned at the binding: the COMMIT rollback answers
+ // `revertedCommits`/`failed`, never the retired version-rollback shape.
+ // @ts-expect-error the COMMIT-rollback payload carries no `restoredVersion`
+ void (await client.packages.rollback('com.acme.crm', 'cmt_1')).restoredVersion;
+
+ // @ts-expect-error a diagnostics sweep is not its entries array
+ const wrongDiagnostics: GetMetaDiagnosticsResponse['entries'] = await client.meta.getDiagnostics();
+
+ // Ruling 1C's other half: `unknown` is not `any` — an unnarrowed
+ // published body must not be treated as an arbitrary record.
+ // @ts-expect-error the published body is `unknown` and must be narrowed before member access
+ void (await client.meta.getPublished('view', 'account_list')).columns;
+
+ void wrongCommits;
+ void wrongDiagnostics;
+}
+
/**
* ⚠️ GREEN IN BOTH STATES — regression guards, recorded as such rather than
* counted as evidence that this card's change was needed. Each pins a
- * near-miss in a DEPENDENCY that the next sweep would otherwise reach for.
+ * near-miss trap the next sweep would otherwise reach for.
*
- * 1. `PackageRollbackResponse` sits one import away from
- * `client.packages.rollback` and is the wrong type for it: it declares the
- * VERSION rollback (`{ success, restoredVersion?, message? }`, per its
- * file header `POST /api/v1/packages/:packageId/rollback — Rollback a
- * package`), while the client method posts `{ commitId }` and the
- * dispatcher routes it to `rollbackToPackageCommit` — the ADR-0067 COMMIT
- * rollback. Binding it would compile and be false.
+ * 1. [#12038 3A] The near-miss this guard used to pin from the other
+ * direction — `PackageRollbackResponse`, the VERSION-rollback declaration
+ * the spec had bound to the live COMMIT-rollback path — is RETIRED
+ * (ADR-0087 discipline; `package-api.test.ts` pins the absence). The
+ * guard now pins the NEW truth: `RollbackToPackageCommitResponse` is the
+ * COMMIT rollback, and it must never grow the version-rollback vocabulary
+ * (`restoredVersion`) whose false declaration this family just paid to
+ * remove.
*
* 2. `Environment` is the obvious-looking binding for `client.projects.*` and
* is camelCase, while the `/api/v1/cloud/*` control plane those methods
@@ -304,12 +382,12 @@ export async function returnTypePrecisionPins11925(): Promise {
* `p.display_name`, `p.organization_id`, `p.is_default`). Binding it would
* typecheck, be false, and break those callers.
*/
-declare const versionRollbackPayload: PackageRollbackResponse['data'];
+declare const commitRollbackPayload: RollbackToPackageCommitResponse;
declare const specEnvironmentRow: Environment;
-export function packageRollbackResponseIsNotTheCommitRollbackShape(): void {
- // @ts-expect-error the VERSION-rollback payload carries no commit identity
- void versionRollbackPayload.commitId;
+export function commitRollbackResponseIsNotTheVersionRollbackShape(): void {
+ // @ts-expect-error the COMMIT-rollback payload carries no version identity
+ void commitRollbackPayload.restoredVersion;
}
export function environmentIsNotTheCloudWireRow(): void {
@@ -327,7 +405,8 @@ describe('client SDK return-type precision (#8140)', () => {
expect(typeof returnTypePrecisionPins).toBe('function');
expect(typeof searchResultIsNotTheGlobalSearchShape).toBe('function');
expect(typeof returnTypePrecisionPins11925).toBe('function');
- expect(typeof packageRollbackResponseIsNotTheCommitRollbackShape).toBe('function');
+ expect(typeof returnTypePrecisionPins12038).toBe('function');
+ expect(typeof commitRollbackResponseIsNotTheVersionRollbackShape).toBe('function');
expect(typeof environmentIsNotTheCloudWireRow).toBe('function');
});
diff --git a/packages/client/src/unwrap-misfire.pin.test.ts b/packages/client/src/unwrap-misfire.pin.test.ts
new file mode 100644
index 0000000000..801e0da492
--- /dev/null
+++ b/packages/client/src/unwrap-misfire.pin.test.ts
@@ -0,0 +1,115 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * [#12038 §8.2] The `unwrapResponse` mis-unwrap hazard, pinned.
+ *
+ * `unwrapResponse` (`./index.ts`) strips an envelope exactly when the body
+ * carries BOTH a boolean `success` AND a `data` key, and passes everything
+ * else through. On the REST surface the bound routes answer their payload
+ * BARE, so the heuristic runs against the payload itself: a payload that ever
+ * grew both keys would be silently unwrapped to its `data` member and every
+ * annotation this family added would become false — with no type error and no
+ * failing runtime test anywhere.
+ *
+ * None of the bound producer payloads carries both keys today (several carry
+ * boolean `success`; none carries `data` beside it — survey §8.2 measured
+ * exactly this). This suite pins that fact at the CONTRACT: it reads each
+ * bound schema's declared key set and refuses the `success`+`data`
+ * combination, so the hazard cannot re-enter through a schema edit. Adding a
+ * `data` key to one of these payloads is not automatically wrong — but it
+ * cannot be done without meeting this pin and deciding what the SDK's unwrap
+ * should do about it.
+ *
+ * Deliberately OUTSIDE the pin:
+ * - `GetPublishedMetaItemResponseSchema` — opaque by ruling (1C): the body is
+ * an arbitrary authored metadata item, so no key set exists to pin. A
+ * published item body carrying both keys would be mis-unwrapped on the REST
+ * surface; that exposure is inherent to the ruled opacity and is recorded
+ * here rather than hidden.
+ * - `PackageExportManifestSchema`'s catch-all keys — registry-derived plural
+ * metadata type names. The companion assertion below pins that the plural
+ * vocabulary contains neither `success` nor `data`, so the open half cannot
+ * produce the combination either.
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ ListDraftsResponseSchema,
+ GetMetaDiagnosticsResponseSchema,
+ FindReferencesToMetaResponseSchema,
+ AuditMetaItemResponseSchema,
+ RollbackMetaItemResponseSchema,
+ DiffMetaItemResponseSchema,
+ ResolvedBookSchema,
+ PackagePublishResultSchema,
+ DiscardPackageDraftsResponseSchema,
+ ListPackageCommitsResponseSchema,
+ RevertPackageCommitResponseSchema,
+ RollbackToPackageCommitResponseSchema,
+ PackageExportManifestSchema,
+ ReassignOrphanedMetadataResponseSchema,
+ DuplicatePackageResponseSchema,
+} from '@objectstack/spec/api';
+import { PLURAL_TO_SINGULAR } from '@objectstack/spec/shared';
+
+/** Every bound object-shaped payload schema, labelled for the failure message. */
+const BOUND_PAYLOAD_SCHEMAS: ReadonlyArray = [
+ ['ListDraftsResponseSchema', ListDraftsResponseSchema],
+ ['GetMetaDiagnosticsResponseSchema', GetMetaDiagnosticsResponseSchema],
+ ['FindReferencesToMetaResponseSchema', FindReferencesToMetaResponseSchema],
+ ['AuditMetaItemResponseSchema', AuditMetaItemResponseSchema],
+ ['RollbackMetaItemResponseSchema', RollbackMetaItemResponseSchema],
+ ['DiffMetaItemResponseSchema', DiffMetaItemResponseSchema],
+ ['ResolvedBookSchema', ResolvedBookSchema],
+ ['PackagePublishResultSchema', PackagePublishResultSchema],
+ ['DiscardPackageDraftsResponseSchema', DiscardPackageDraftsResponseSchema],
+ ['ListPackageCommitsResponseSchema', ListPackageCommitsResponseSchema],
+ ['RevertPackageCommitResponseSchema', RevertPackageCommitResponseSchema],
+ ['RollbackToPackageCommitResponseSchema', RollbackToPackageCommitResponseSchema],
+ ['PackageExportManifestSchema', PackageExportManifestSchema],
+ ['ReassignOrphanedMetadataResponseSchema', ReassignOrphanedMetadataResponseSchema],
+ ['DuplicatePackageResponseSchema', DuplicatePackageResponseSchema],
+];
+
+/** The predicate under pin: would `unwrapResponse`'s heuristic fire on this declared key set? */
+function declaresTheEnvelopeShape(keys: readonly string[]): boolean {
+ return keys.includes('success') && keys.includes('data');
+}
+
+/** Declared top-level keys of a (lazySchema-proxied) z.object — the AuditMetaItemRequest pattern. */
+function declaredKeys(schema: unknown): string[] {
+ const shape = (schema as { shape?: Record }).shape;
+ expect(shape && typeof shape === 'object').toBe(true);
+ return Object.keys(shape as Record);
+}
+
+describe('no bound payload can trip the unwrapResponse heuristic (#12038 §8.2)', () => {
+ it.each(BOUND_PAYLOAD_SCHEMAS.map(([name, schema]) => ({ name, schema })))(
+ '$name never declares boolean `success` beside `data`',
+ ({ schema }) => {
+ const keys = declaredKeys(schema);
+ // Anti-vacuity half: the shape really was read (every bound payload
+ // declares at least one key).
+ expect(keys.length).toBeGreaterThan(0);
+ expect(declaresTheEnvelopeShape(keys)).toBe(false);
+ },
+ );
+
+ it('the predicate itself is live — a payload declaring both keys WOULD be refused', () => {
+ // Negative control: drive the same predicate a real schema goes through
+ // with the exact key set the heuristic fires on. A guard whose failure
+ // path never executes is a guard nobody has seen fail.
+ expect(declaresTheEnvelopeShape(['success', 'data'])).toBe(true);
+ expect(declaresTheEnvelopeShape(['success', 'revertedCommits', 'failed'])).toBe(false);
+ });
+
+ it('the export manifest\'s OPEN half cannot produce the combination either', () => {
+ // `PackageExportManifestSchema`'s catch-all keys come from the plural
+ // metadata-type vocabulary (`manifest[plural] = …` in the handler). Pin
+ // that the vocabulary can never contribute the heuristic's key pair.
+ const plurals = Object.keys(PLURAL_TO_SINGULAR);
+ expect(plurals.length).toBeGreaterThan(0);
+ expect(plurals).not.toContain('success');
+ expect(plurals).not.toContain('data');
+ });
+});
diff --git a/packages/rest/src/rest-route-ledger.ts b/packages/rest/src/rest-route-ledger.ts
index eb3d6cfd51..bc119aa352 100644
--- a/packages/rest/src/rest-route-ledger.ts
+++ b/packages/rest/src/rest-route-ledger.ts
@@ -143,13 +143,21 @@ export const REST_ROUTE_LEDGER: readonly RestRouteLedgerEntry[] = [
// `/meta/zzz_not_a_type`.
{ route: 'GET /api/v1/meta/types', family: 'metadata', source: 'route-manager', disposition: 'server-only',
note: 'richer types listing consumed by Studio tooling directly; the SDK reads the same body from GET /meta (meta.getTypes). Mirrors the `GET /meta/types` row in runtime/src/route-ledger.ts' },
- { route: 'GET /api/v1/meta/diagnostics', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getDiagnostics' },
- { route: 'GET /api/v1/meta/_drafts', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.listDrafts' },
+ { route: 'GET /api/v1/meta/diagnostics', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getDiagnostics',
+ responseSchema: 'GetMetaDiagnosticsResponseSchema',
+ note: '[#12038] REST-only route; this server answers the payload BARE (`res.json(result)`, no envelope), so the named schema is the whole body on this surface. Describe-only transcription of `getMetaDiagnostics`\'s declared return; conformance: spec `api/protocol.test.ts`' },
+ { route: 'GET /api/v1/meta/_drafts', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.listDrafts',
+ responseSchema: 'ListDraftsResponseSchema',
+ note: '[#12038] on THIS surface the payload is answered BARE (`res.json(result)`); the dispatcher twin (runtime ledger row) answers the same payload through the `{ success, data }` envelope — the named schema is the PAYLOAD, true on both surfaces. Describe-only transcription of `listDrafts`\'s declared return; conformance: spec `api/protocol.test.ts`' },
{ route: 'POST /api/v1/meta/_migrate-stored', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.migrateStored',
- note: 'ADR-0087 stored-row canonicalization (#4327); gated on `manage_metadata`, preview unless { apply: true }' },
+ note: 'ADR-0087 stored-row canonicalization (#4327); gated on `manage_metadata`, preview unless { apply: true }. DELIBERATELY UNBOUND (#12038 ruling 2C) — this row would name the schema, but the report\'s only named type, `StoredMigrationReport`, lives in `@objectstack/metadata-protocol` (unreachable from the spec/api namespace this field resolves against); a second declaration in spec would drift against the CLI rendering the same report. Answered BARE on this surface, enveloped on the dispatcher twin' },
{ route: 'GET /api/v1/meta/:type', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getItems' },
- { route: 'GET /api/v1/meta/:type/:name/references', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getReferences' },
- { route: 'GET /api/v1/meta/book/:name/tree', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getBookTree' },
+ { route: 'GET /api/v1/meta/:type/:name/references', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getReferences',
+ responseSchema: 'FindReferencesToMetaResponseSchema',
+ note: '[#12038] REST-only route; payload answered BARE, so the named schema is the whole body. Describe-only transcription of `findReferencesToMeta`\'s declared return; conformance: spec `api/protocol.test.ts`' },
+ { route: 'GET /api/v1/meta/book/:name/tree', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getBookTree',
+ responseSchema: 'ResolvedBookSchema',
+ note: '[#12038] REST-only route; payload answered BARE — `resolveBookTree()`\'s `ResolvedBook`, verbatim. The schema is declared beside its interfaces in spec `system/book.zod.ts` and re-exported into `/api` (ruling 5A); conformance: spec `system/book.test.ts`' },
// [#5882] The three-layer diagnostic projection, promoted from the
// `?layers=true` flag on the row below to a path of its own so that one path
// answers one response shape. `responseSchema` is filled because this mount
@@ -177,12 +185,17 @@ export const REST_ROUTE_LEDGER: readonly RestRouteLedgerEntry[] = [
note: 'REST-only: the dispatcher /meta branch has no DELETE handling — it falls into the read path. [#7019] gated on `manage_metadata` (ADR-0066 D1), same mechanism as the PUT twins — but NOT for the ADR-0106 reason: nothing is masked or round-tripped here, this discards a customization overlay outright, and `?dropStorage=true` takes the object table with it. [#12702] same shared verdict as the PUT door: an admitted `manage_org_presentation` reset threads the caller\'s own organization, so the only row it can discard is their own org\'s overlay' },
{ route: 'GET /api/v1/meta/:type/:name/history', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getHistory',
note: 'REST-only: the dispatcher /meta branch swallows /history as a compound name and 404s' },
- { route: 'GET /api/v1/meta/:type/:name/audit', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getAudit' },
+ { route: 'GET /api/v1/meta/:type/:name/audit', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getAudit',
+ responseSchema: 'AuditMetaItemResponseSchema',
+ note: '[#12038] REST-only route; payload answered BARE, so the named schema is the whole body. The schema predates this row (#11678, exact field-for-field match of `auditMetaItem`\'s declared return); conformance: the #11678 capture suite in spec `api/protocol.test.ts`' },
{ route: 'POST /api/v1/meta/:type/:name/publish', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.publishItem',
note: 'per-item ADR-0033 publish; packages.publishDrafts remains the package-scoped flow. [#12702] gated by the shared `metaWriteCapabilityVerdict`: `manage_org_presentation` is also admitted for an org-scoped tier-A promotion — the second half of the save→publish loop, promoting only the caller\'s own org partition' },
{ route: 'POST /api/v1/meta/:type/:name/rollback', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.rollbackItem',
- note: '[#12702] gated by the shared `metaWriteCapabilityVerdict`: `manage_org_presentation` is also admitted for an org-scoped tier-A rollback, restoring only a version of the caller\'s own org overlay' },
- { route: 'GET /api/v1/meta/:type/:name/diff', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.diffItem' },
+ responseSchema: 'RollbackMetaItemResponseSchema',
+ note: '[#12702] gated by the shared `metaWriteCapabilityVerdict`: `manage_org_presentation` is also admitted for an org-scoped tier-A rollback, restoring only a version of the caller\'s own org overlay. [#12038] REST-only route; payload answered BARE, so the named schema is the whole body — describe-only transcription of `rollbackMetaItem`\'s declared return; conformance: spec `api/protocol.test.ts`' },
+ { route: 'GET /api/v1/meta/:type/:name/diff', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.diffItem',
+ responseSchema: 'DiffMetaItemResponseSchema',
+ note: '[#12038] REST-only route; payload answered BARE, so the named schema is the whole body. Describe-only transcription of `diffMetaItem`\'s declared return; conformance: spec `api/protocol.test.ts`' },
// [#7526] The two routes that were ledgered in `runtime/src/route-ledger.ts`
// and implemented in the dispatcher, but which no registrar ever mounted —
// so the SDK guard (#3642) certified them off a DECLARATION while they died
@@ -199,7 +212,8 @@ export const REST_ROUTE_LEDGER: readonly RestRouteLedgerEntry[] = [
{ route: 'GET /api/v1/meta/object/:name/state/:field', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getLegalNextStates',
note: 'ADR-0020 D3.3 legal-next-state introspection. `next: null` = no state_machine governs the field, `next: []` = a declared dead end. #9180 step 2 retired the plural `/api/v1/meta/objects/:name/state/:field` twin that used to carry this `sdk` disposition, and the SDK now spells the segment `object` — the `/meta` type segment is singular, always. The retired twin was a DECLARED registration, not a `META_URL_TO_SINGULAR` fold tolerance (this route matches a literal segment and never consulted the fold), so the boundary accept set is unchanged. ⚠ What the retirement did NOT make universal, because an author reading only this row would assume it did: the legacy dispatcher `/meta` if-chain in `packages/runtime/src/domains/meta.ts` still matches BOTH literals, so the plural is refused HERE and still answered wherever `dispatch()` fronts the request instead of this server. That is deliberate, by the maintainer re-weigh of 2026-08-17 (item 3: no new refusals beyond step 1; the external break deferred with no scheduled window), and it is recorded with its provenance on the dispatcher ledger row plus `runtime/src/domains/meta-state-plural-tolerance.test.ts` (#10179)' },
{ route: 'GET /api/v1/meta/:type/:name/published', family: 'metadata', source: 'route-manager', disposition: 'sdk', client: 'meta.getPublished',
- note: 'ADR-0033 published snapshot; 404s for a name that does not exist, which the pre-#7526 fall-through into the compound-name route structurally could not do (it answered a protection-envelope stub identical before publish and for a bogus name)' },
+ responseSchema: 'GetPublishedMetaItemResponseSchema',
+ note: 'ADR-0033 published snapshot; 404s for a name that does not exist, which the pre-#7526 fall-through into the compound-name route structurally could not do (it answered a protection-envelope stub identical before publish and for a bogus name). [#12038 ruling 1C] the named schema is DELIBERATELY OPAQUE (`z.unknown()`): the route answers an arbitrary metadata item body, BARE on this surface (enveloped on the dispatcher twin) — never a union frozen against the type registry' },
// [#12195] THREE ROWS RETIRED HERE — `GET /api/v1/meta/:type/:section/:name`,
// `PUT` on the same path, and `GET …/:section/:name/published`. They were the
diff --git a/packages/runtime/src/route-ledger.ts b/packages/runtime/src/route-ledger.ts
index 59d59a9acc..97f399c957 100644
--- a/packages/runtime/src/route-ledger.ts
+++ b/packages/runtime/src/route-ledger.ts
@@ -299,18 +299,34 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [
{ route: 'PATCH /packages/:id/enable', domain: '/packages', disposition: 'sdk', client: 'packages.enable' },
{ route: 'PATCH /packages/:id/disable', domain: '/packages', disposition: 'sdk', client: 'packages.disable' },
{ route: 'PATCH /packages/:id', domain: '/packages', disposition: 'sdk', client: 'packages.update' },
- { route: 'POST /packages/:id/publish', domain: '/packages', disposition: 'sdk', client: 'packages.publish' },
+ { route: 'POST /packages/:id/publish', domain: '/packages', disposition: 'sdk', client: 'packages.publish',
+ responseSchema: 'PackagePublishResultSchema',
+ note: '[#12038] dispatch() answers through the { success, data } envelope, so the named schema is the `data` — `MetadataManager.publishPackage`\'s declared return, whose exact schema already existed in spec `system/metadata-persistence.zod.ts` and is re-exported into `/api` by ruling 5A (never a second copy). Conformance: spec `api/package-lifecycle.test.ts`' },
{ route: 'POST /packages/:id/publish-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.publishDrafts',
responseSchema: 'PublishPackageDraftsResponseSchema',
note: '[#9406] dispatch() answers through the { success, data } envelope, so the named schema is the `data` — the protocol result AFTER the door\'s own mutations (seedApplied back-fill, ADR-0045 unhiddenApps/unhideError, rebindError). Fillable because packages-publish-drafts-response-conformance.test.ts drives THIS handler and parses the payload it answers; the producer half is pinned in objectql\'s publish-package-drafts-response-conformance.test.ts. `probes` is deliberately opaque in the declaration (#9406 ruling)' },
- { route: 'POST /packages/:id/discard-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.discardDrafts' },
- { route: 'GET /packages/:id/commits', domain: '/packages', disposition: 'sdk', client: 'packages.listCommits' },
- { route: 'POST /packages/:id/commits/:commitId/revert', domain: '/packages', disposition: 'sdk', client: 'packages.revertCommit' },
- { route: 'POST /packages/:id/rollback', domain: '/packages', disposition: 'sdk', client: 'packages.rollback' },
+ { route: 'POST /packages/:id/discard-drafts', domain: '/packages', disposition: 'sdk', client: 'packages.discardDrafts',
+ responseSchema: 'DiscardPackageDraftsResponseSchema',
+ note: '[#12038] enveloped — the named schema is the `data`: `discardPackageDrafts`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' },
+ { route: 'GET /packages/:id/commits', domain: '/packages', disposition: 'sdk', client: 'packages.listCommits',
+ responseSchema: 'ListPackageCommitsResponseSchema',
+ note: '[#12038] enveloped — the named schema is the `data`. The producer (`listCommits`) returns a BARE array; the `{ commits }` wrapper is minted at THIS handler (`domains/packages.ts`) and the schema declares it as the handler\'s own. Conformance: spec `api/package-lifecycle.test.ts`' },
+ { route: 'POST /packages/:id/commits/:commitId/revert', domain: '/packages', disposition: 'sdk', client: 'packages.revertCommit',
+ responseSchema: 'RevertPackageCommitResponseSchema',
+ note: '[#12038] enveloped — the named schema is the `data`: `revertCommit`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' },
+ { route: 'POST /packages/:id/rollback', domain: '/packages', disposition: 'sdk', client: 'packages.rollback',
+ responseSchema: 'RollbackToPackageCommitResponseSchema',
+ note: '[#12038 ruling 3A] enveloped — the named schema is the `data`: `rollbackToPackageCommit`\'s declared return, the ADR-0067 COMMIT rollback. Fillable only after the retirement of `PackageRollbackResponseSchema` + `PackageApiContracts.rollbackPackage`, the VERSION-rollback declaration the spec had falsely bound to this exact path. Conformance: spec `api/package-lifecycle.test.ts`' },
{ route: 'POST /packages/:id/revert', domain: '/packages', disposition: 'sdk', client: 'packages.revert' },
- { route: 'GET /packages/:id/export', domain: '/packages', disposition: 'sdk', client: 'packages.export' },
- { route: 'POST /packages/:id/adopt-orphans', domain: '/packages', disposition: 'sdk', client: 'packages.adoptOrphans' },
- { route: 'POST /packages/:id/duplicate', domain: '/packages', disposition: 'sdk', client: 'packages.duplicate' },
+ { route: 'GET /packages/:id/export', domain: '/packages', disposition: 'sdk', client: 'packages.export',
+ responseSchema: 'PackageExportManifestSchema',
+ note: '[#12038 ruling 4A] enveloped — the named schema is the `data`: the ADR-0070 portable manifest. Four fixed keys (`id`, `name`, `version`, `label?`) plus an OPEN catch-all — the remaining keys are registry-derived per metadata type present, deliberately not enumerated (freezes nothing). Conformance: spec `api/package-lifecycle.test.ts`' },
+ { route: 'POST /packages/:id/adopt-orphans', domain: '/packages', disposition: 'sdk', client: 'packages.adoptOrphans',
+ responseSchema: 'ReassignOrphanedMetadataResponseSchema',
+ note: '[#12038] enveloped — the named schema is the `data`: `reassignOrphanedMetadata`\'s declared return, transcribed describe-only. Conformance: spec `api/package-lifecycle.test.ts`' },
+ { route: 'POST /packages/:id/duplicate', domain: '/packages', disposition: 'sdk', client: 'packages.duplicate',
+ responseSchema: 'DuplicatePackageResponseSchema',
+ note: '[#12038] enveloped — the named schema is the `data`: `duplicatePackage`\'s declared return, transcribed describe-only. ⚠ `data.success` is the operation\'s verdict; the envelope `success` is transport-level and true even for a partial/empty duplicate (the objectui#6593 defect). Conformance: spec `api/package-lifecycle.test.ts`' },
// ── automation ────────────────────────────────────────────────────────────
{ route: 'POST /automation/trigger/:name', domain: '/automation', disposition: 'sdk', client: 'automation.trigger',
@@ -350,10 +366,14 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [
{ route: 'GET /meta/:type/:name', domain: '/meta', disposition: 'sdk', client: 'meta.getItem' },
{ route: 'PUT /meta/:type/:name', domain: '/meta', disposition: 'sdk', client: 'meta.saveItem',
note: '[#7019] gated on `manage_metadata` (ADR-0066 D1) — the dispatcher transport of the REST save door. [#12702] the gate is the shared `metaWriteCapabilityVerdict` (`@objectstack/metadata-core`): `manage_org_presentation` is also admitted, ONLY for an `allowOrgOverride: true` type written org-scoped to the caller\'s own active organization; refusals answer 403 `PERMISSION_DENIED`, this transport\'s pinned spelling' },
- { route: 'GET /meta/:type/:name/published', domain: '/meta', disposition: 'sdk', client: 'meta.getPublished' },
- { route: 'GET /meta/_drafts', domain: '/meta', disposition: 'sdk', client: 'meta.listDrafts' },
+ { route: 'GET /meta/:type/:name/published', domain: '/meta', disposition: 'sdk', client: 'meta.getPublished',
+ responseSchema: 'GetPublishedMetaItemResponseSchema',
+ note: '[#12038 ruling 1C] enveloped on THIS surface — the named schema is the `data`, and it is DELIBERATELY OPAQUE (`z.unknown()`): the route answers an arbitrary metadata item body, never a union frozen against the type registry. The REST twin (`rest-route-ledger.ts`) answers the same payload BARE' },
+ { route: 'GET /meta/_drafts', domain: '/meta', disposition: 'sdk', client: 'meta.listDrafts',
+ responseSchema: 'ListDraftsResponseSchema',
+ note: '[#12038] enveloped on THIS surface — the named schema is the `data`; the REST twin answers the same payload BARE. Describe-only transcription of `listDrafts`\'s declared return; conformance: spec `api/protocol.test.ts`' },
{ route: 'POST /meta/_migrate-stored', domain: '/meta', disposition: 'sdk', client: 'meta.migrateStored',
- note: 'ADR-0087 stored-row canonicalization (#4327); gated on `manage_metadata`, preview unless { apply: true }' },
+ note: 'ADR-0087 stored-row canonicalization (#4327); gated on `manage_metadata`, preview unless { apply: true }. DELIBERATELY UNBOUND (#12038 ruling 2C) — this row would name the schema, but the report\'s only named type, `StoredMigrationReport`, lives in `@objectstack/metadata-protocol` (unreachable from the spec/api namespace this field resolves against); a second declaration in spec would drift against the CLI rendering the same report. Enveloped on this surface, BARE on the REST twin' },
{ route: 'GET /meta/object/:name/state/:field', domain: '/meta', disposition: 'sdk', client: 'meta.getLegalNextStates',
note: '#9180 step 2 moved the SDK to the singular spelling and retired the plural REST registration; this row follows the client. DELIBERATE ASYMMETRY, not residue nobody has got to yet: the legacy if-chain branch in `domains/meta.ts` still matches BOTH literals (`objects` and `object`), so `/meta/objects/:name/state/:field` is REFUSED by a REST-fronted deployment (transport 404 — no registration left to match it) and ANSWERED wherever `dispatch()` is the front door (the `createHonoApp` catch-all, the documented embed shape). It stays by the maintainer re-weigh of the #9180 ruling, 2026-08-17 item 3: the tolerance is kept for external callers, no new refusals beyond what step 1 shipped, the external break deferred with no scheduled window — narrowing this arm is a NEW refusal on a SECOND surface and is the maintainer call, not a step of the ruling. ⛔ It is NOT the `META_URL_TO_SINGULAR` fold whose retirement was deferred: that is a map consulted for `/meta/:type`, this is a literal `||` that no request reaches through the fold — separate mechanisms under separate decisions, and conflating them is the specific error to avoid. So this row lists the canonical spelling of a branch that answers two, and `domains/meta-state-plural-tolerance.test.ts` pins BOTH halves so this note cannot quietly stop being true (#10179)' },
diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json
index fc8608882e..8f4bae3e70 100644
--- a/packages/spec/api-surface/api.json
+++ b/packages/spec/api-surface/api.json
@@ -274,10 +274,16 @@
"DeviceRequestResponseSchema (const)",
"DeviceTokenResponse (type)",
"DeviceTokenResponseSchema (const)",
+ "DiffMetaItemResponse (type)",
+ "DiffMetaItemResponseParsed (type)",
+ "DiffMetaItemResponseSchema (const)",
"DisablePackageRequest (type)",
"DisablePackageRequestSchema (const)",
"DisablePackageResponse (type)",
"DisablePackageResponseSchema (const)",
+ "DiscardPackageDraftsResponse (type)",
+ "DiscardPackageDraftsResponseParsed (type)",
+ "DiscardPackageDraftsResponseSchema (const)",
"DiscoveryEnvironment (type)",
"DiscoveryEnvironmentSchema (const)",
"DiscoveryResponse (type)",
@@ -293,6 +299,9 @@
"DispatcherRouteSchema (const)",
"DocumentState (type)",
"DocumentStateSchema (const)",
+ "DuplicatePackageResponse (type)",
+ "DuplicatePackageResponseParsed (type)",
+ "DuplicatePackageResponseSchema (const)",
"ERROR_CODE_LEDGER (const)",
"ETag (type)",
"ETagParsed (type)",
@@ -371,6 +380,9 @@
"FindDataRequestSchema (const)",
"FindDataResponse (type)",
"FindDataResponseSchema (const)",
+ "FindReferencesToMetaResponse (type)",
+ "FindReferencesToMetaResponseParsed (type)",
+ "FindReferencesToMetaResponseSchema (const)",
"FlowSummary (type)",
"FlowSummarySchema (const)",
"GeneratedApiDocumentation (type)",
@@ -419,6 +431,9 @@
"GetLocalesResponse (type)",
"GetLocalesResponseParsed (type)",
"GetLocalesResponseSchema (const)",
+ "GetMetaDiagnosticsResponse (type)",
+ "GetMetaDiagnosticsResponseParsed (type)",
+ "GetMetaDiagnosticsResponseSchema (const)",
"GetMetaItemCachedRequest (type)",
"GetMetaItemCachedRequestSchema (const)",
"GetMetaItemCachedResponse (type)",
@@ -461,6 +476,9 @@
"GetPresignedUrlRequest (type)",
"GetPresignedUrlRequestParsed (type)",
"GetPresignedUrlRequestSchema (const)",
+ "GetPublishedMetaItemResponse (type)",
+ "GetPublishedMetaItemResponseParsed (type)",
+ "GetPublishedMetaItemResponseSchema (const)",
"GetRunRequest (type)",
"GetRunRequestSchema (const)",
"GetRunResponse (type)",
@@ -529,6 +547,9 @@
"ListAiPendingActionsRequestSchema (const)",
"ListAiPendingActionsResponse (type)",
"ListAiPendingActionsResponseSchema (const)",
+ "ListDraftsResponse (type)",
+ "ListDraftsResponseParsed (type)",
+ "ListDraftsResponseSchema (const)",
"ListExportJobsRequest (type)",
"ListExportJobsRequestParsed (type)",
"ListExportJobsRequestSchema (const)",
@@ -558,6 +579,9 @@
"ListNotificationsResponse (type)",
"ListNotificationsResponseParsed (type)",
"ListNotificationsResponseSchema (const)",
+ "ListPackageCommitsResponse (type)",
+ "ListPackageCommitsResponseParsed (type)",
+ "ListPackageCommitsResponseSchema (const)",
"ListPackagesRequest (type)",
"ListPackagesRequestSchema (const)",
"ListPackagesResponse (type)",
@@ -705,6 +729,9 @@
"OperatorMappingSchema (const)",
"PackageApiContracts (const)",
"PackageApiErrorCode (type)",
+ "PackageExportManifest (type)",
+ "PackageExportManifestParsed (type)",
+ "PackageExportManifestSchema (const)",
"PackageInstallRequest (type)",
"PackageInstallRequestParsed (type)",
"PackageInstallRequestSchema (const)",
@@ -714,12 +741,11 @@
"PackagePathParams (type)",
"PackagePathParamsSchema (const)",
"PackageProtocol (interface)",
+ "PackagePublishResult (type)",
+ "PackagePublishResultSchema (const)",
"PackageRollbackRequest (type)",
"PackageRollbackRequestParsed (type)",
"PackageRollbackRequestSchema (const)",
- "PackageRollbackResponse (type)",
- "PackageRollbackResponseParsed (type)",
- "PackageRollbackResponseSchema (const)",
"PackageStatus (type)",
"PackageUpgradeRequest (type)",
"PackageUpgradeRequestParsed (type)",
@@ -786,6 +812,9 @@
"RealtimeUnsubscribeRequestSchema (const)",
"RealtimeUnsubscribeResponse (type)",
"RealtimeUnsubscribeResponseSchema (const)",
+ "ReassignOrphanedMetadataResponse (type)",
+ "ReassignOrphanedMetadataResponseParsed (type)",
+ "ReassignOrphanedMetadataResponseSchema (const)",
"RecordData (type)",
"RecordDataSchema (const)",
"RefreshTokenRequest (type)",
@@ -808,6 +837,12 @@
"ResolveDependenciesResponse (type)",
"ResolveDependenciesResponseParsed (type)",
"ResolveDependenciesResponseSchema (const)",
+ "ResolvedBook (interface)",
+ "ResolvedBookSchema (const)",
+ "ResolvedEntry (interface)",
+ "ResolvedEntrySchema (const)",
+ "ResolvedGroup (interface)",
+ "ResolvedGroupSchema (const)",
"ResponseEnvelopeConfig (type)",
"ResponseEnvelopeConfigParsed (type)",
"ResponseEnvelopeConfigSchema (const)",
@@ -831,6 +866,15 @@
"RestServerConfigParsed (type)",
"RestServerConfigSchema (const)",
"RetryStrategy (type)",
+ "RevertPackageCommitResponse (type)",
+ "RevertPackageCommitResponseParsed (type)",
+ "RevertPackageCommitResponseSchema (const)",
+ "RollbackMetaItemResponse (type)",
+ "RollbackMetaItemResponseParsed (type)",
+ "RollbackMetaItemResponseSchema (const)",
+ "RollbackToPackageCommitResponse (type)",
+ "RollbackToPackageCommitResponseParsed (type)",
+ "RollbackToPackageCommitResponseSchema (const)",
"RouteCategory (type)",
"RouteCoverageEntry (type)",
"RouteCoverageEntrySchema (const)",
diff --git a/packages/spec/api-surface/system.json b/packages/spec/api-surface/system.json
index 683577f37c..3904b32d56 100644
--- a/packages/spec/api-surface/system.json
+++ b/packages/spec/api-surface/system.json
@@ -525,9 +525,12 @@
"RenderValidationMessageOptions (interface)",
"ResolveOptions (interface)",
"ResolvedBook (interface)",
+ "ResolvedBookSchema (const)",
"ResolvedEntry (interface)",
+ "ResolvedEntrySchema (const)",
"ResolvedFieldLabel (interface)",
"ResolvedGroup (interface)",
+ "ResolvedGroupSchema (const)",
"ResolvedSettingValue (type)",
"ResolvedSettingValueSchema (const)",
"ResolverDoc (interface)",
diff --git a/packages/spec/authorable-surface/api.json b/packages/spec/authorable-surface/api.json
index 12435da942..892b89620e 100644
--- a/packages/spec/authorable-surface/api.json
+++ b/packages/spec/authorable-surface/api.json
@@ -495,9 +495,21 @@
"api/DeviceRequestResponse:expiresAt",
"api/DeviceRequestResponse:interval",
"api/DeviceRequestResponse:verificationUrl",
+ "api/DiffMetaItemResponse:added",
+ "api/DiffMetaItemResponse:changed",
+ "api/DiffMetaItemResponse:fromVersion",
+ "api/DiffMetaItemResponse:name",
+ "api/DiffMetaItemResponse:removed",
+ "api/DiffMetaItemResponse:toVersion",
+ "api/DiffMetaItemResponse:type",
"api/DisablePackageRequest:id",
"api/DisablePackageResponse:message",
"api/DisablePackageResponse:package",
+ "api/DiscardPackageDraftsResponse:discarded",
+ "api/DiscardPackageDraftsResponse:discardedCount",
+ "api/DiscardPackageDraftsResponse:failed",
+ "api/DiscardPackageDraftsResponse:failedCount",
+ "api/DiscardPackageDraftsResponse:success",
"api/Discovery:capabilities",
"api/Discovery:environment",
"api/Discovery:locale",
@@ -524,6 +536,12 @@
"api/DocumentState:documentId",
"api/DocumentState:lastModified",
"api/DocumentState:version",
+ "api/DuplicatePackageResponse:copied",
+ "api/DuplicatePackageResponse:copiedCount",
+ "api/DuplicatePackageResponse:failed",
+ "api/DuplicatePackageResponse:failedCount",
+ "api/DuplicatePackageResponse:success",
+ "api/DuplicatePackageResponse:targetPackageId",
"api/ETag:value",
"api/ETag:weak",
"api/EditMessage:messageId",
@@ -658,6 +676,7 @@
"api/FindDataResponse:object",
"api/FindDataResponse:records",
"api/FindDataResponse:total",
+ "api/FindReferencesToMetaResponse:references",
"api/FlowSummary:enabled",
"api/FlowSummary:label",
"api/FlowSummary:lastRunAt",
@@ -724,6 +743,11 @@
"api/GetInstalledPackageResponse:meta",
"api/GetInstalledPackageResponse:success",
"api/GetLocalesResponse:locales",
+ "api/GetMetaDiagnosticsResponse:entries",
+ "api/GetMetaDiagnosticsResponse:scannedItems",
+ "api/GetMetaDiagnosticsResponse:scannedTypes",
+ "api/GetMetaDiagnosticsResponse:stats",
+ "api/GetMetaDiagnosticsResponse:total",
"api/GetMetaItemCachedRequest:cacheRequest",
"api/GetMetaItemCachedRequest:locale",
"api/GetMetaItemCachedRequest:name",
@@ -958,6 +982,7 @@
"api/ListAiPendingActionsRequest:status",
"api/ListAiPendingActionsResponse:items",
"api/ListAiPendingActionsResponse:total",
+ "api/ListDraftsResponse:drafts",
"api/ListExportJobsRequest:cursor",
"api/ListExportJobsRequest:limit",
"api/ListExportJobsRequest:object",
@@ -994,6 +1019,7 @@
"api/ListNotificationsResponse:cursor [RETIRED]",
"api/ListNotificationsResponse:notifications",
"api/ListNotificationsResponse:unreadCount",
+ "api/ListPackageCommitsResponse:commits",
"api/ListPackagesRequest:enabled",
"api/ListPackagesRequest:status",
"api/ListPackagesRequest:type",
@@ -1237,6 +1263,10 @@
"api/OperatorMapping:odata",
"api/OperatorMapping:operator",
"api/OperatorMapping:rest",
+ "api/PackageExportManifest:id",
+ "api/PackageExportManifest:label",
+ "api/PackageExportManifest:name",
+ "api/PackageExportManifest:version",
"api/PackageInstallRequest:artifactRef",
"api/PackageInstallRequest:enableOnInstall",
"api/PackageInstallRequest:manifest",
@@ -1247,13 +1277,15 @@
"api/PackageInstallResponse:meta",
"api/PackageInstallResponse:success",
"api/PackagePathParams:packageId",
+ "api/PackagePublishResult:itemsPublished",
+ "api/PackagePublishResult:packageId",
+ "api/PackagePublishResult:publishedAt",
+ "api/PackagePublishResult:success",
+ "api/PackagePublishResult:validationErrors",
+ "api/PackagePublishResult:version",
"api/PackageRollbackRequest:packageId",
"api/PackageRollbackRequest:rollbackCustomizations",
"api/PackageRollbackRequest:snapshotId",
- "api/PackageRollbackResponse:data",
- "api/PackageRollbackResponse:error",
- "api/PackageRollbackResponse:meta",
- "api/PackageRollbackResponse:success",
"api/PackageUpgradeRequest:createSnapshot",
"api/PackageUpgradeRequest:dryRun",
"api/PackageUpgradeRequest:manifest",
@@ -1362,6 +1394,10 @@
"api/RealtimeSubscribeResponse:subscriptionId",
"api/RealtimeUnsubscribeRequest:subscriptionId",
"api/RealtimeUnsubscribeResponse:success",
+ "api/ReassignOrphanedMetadataResponse:reassigned",
+ "api/ReassignOrphanedMetadataResponse:reassignedCount",
+ "api/ReassignOrphanedMetadataResponse:success",
+ "api/ReassignOrphanedMetadataResponse:targetPackageId",
"api/RefreshTokenRequest:refreshToken",
"api/RegisterDeviceRequest:deviceId",
"api/RegisterDeviceRequest:name",
@@ -1390,6 +1426,19 @@
"api/ResolveDependenciesResponse:error",
"api/ResolveDependenciesResponse:meta",
"api/ResolveDependenciesResponse:success",
+ "api/ResolvedBook:groups",
+ "api/ResolvedBook:label",
+ "api/ResolvedBook:name",
+ "api/ResolvedEntry:badge",
+ "api/ResolvedEntry:description",
+ "api/ResolvedEntry:doc",
+ "api/ResolvedEntry:href",
+ "api/ResolvedEntry:icon",
+ "api/ResolvedEntry:label",
+ "api/ResolvedEntry:separator",
+ "api/ResolvedGroup:entries",
+ "api/ResolvedGroup:key",
+ "api/ResolvedGroup:label",
"api/ResponseEnvelopeConfig:customMetadata",
"api/ResponseEnvelopeConfig:enabled",
"api/ResponseEnvelopeConfig:includeDuration",
@@ -1458,6 +1507,20 @@
"api/RestServerConfig:metadata",
"api/RestServerConfig:openApi31 [RETIRED]",
"api/RestServerConfig:routes",
+ "api/RevertPackageCommitResponse:failed",
+ "api/RevertPackageCommitResponse:failedCount",
+ "api/RevertPackageCommitResponse:revertCommitId",
+ "api/RevertPackageCommitResponse:reverted",
+ "api/RevertPackageCommitResponse:revertedCount",
+ "api/RevertPackageCommitResponse:success",
+ "api/RollbackMetaItemResponse:message",
+ "api/RollbackMetaItemResponse:restoredFromVersion",
+ "api/RollbackMetaItemResponse:seq",
+ "api/RollbackMetaItemResponse:success",
+ "api/RollbackMetaItemResponse:version",
+ "api/RollbackToPackageCommitResponse:failed",
+ "api/RollbackToPackageCommitResponse:revertedCommits",
+ "api/RollbackToPackageCommitResponse:success",
"api/RouteCoverageEntry:category",
"api/RouteCoverageEntry:handlerStatus",
"api/RouteCoverageEntry:healthCheckPassed",
diff --git a/packages/spec/authorable-surface/system.json b/packages/spec/authorable-surface/system.json
index 4e3485ace6..cb468d1d75 100644
--- a/packages/spec/authorable-surface/system.json
+++ b/packages/spec/authorable-surface/system.json
@@ -969,6 +969,19 @@
"system/RenameObjectOperation:newName",
"system/RenameObjectOperation:oldName",
"system/RenameObjectOperation:type",
+ "system/ResolvedBook:groups",
+ "system/ResolvedBook:label",
+ "system/ResolvedBook:name",
+ "system/ResolvedEntry:badge",
+ "system/ResolvedEntry:description",
+ "system/ResolvedEntry:doc",
+ "system/ResolvedEntry:href",
+ "system/ResolvedEntry:icon",
+ "system/ResolvedEntry:label",
+ "system/ResolvedEntry:separator",
+ "system/ResolvedGroup:entries",
+ "system/ResolvedGroup:key",
+ "system/ResolvedGroup:label",
"system/ResolvedSettingValue:cascadeChain",
"system/ResolvedSettingValue:locked",
"system/ResolvedSettingValue:lockedReason",
diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json
index 24b9317546..5586449faa 100644
--- a/packages/spec/export-origins/api.json
+++ b/packages/spec/export-origins/api.json
@@ -274,10 +274,16 @@
"DeviceRequestResponseSchema": "src/api/auth-endpoints.zod.ts#DeviceRequestResponseSchema (const)",
"DeviceTokenResponse": "src/api/auth-endpoints.zod.ts#DeviceTokenResponse (type)",
"DeviceTokenResponseSchema": "src/api/auth-endpoints.zod.ts#DeviceTokenResponseSchema (const)",
+ "DiffMetaItemResponse": "src/api/protocol.zod.ts#DiffMetaItemResponse (type)",
+ "DiffMetaItemResponseParsed": "src/api/protocol.zod.ts#DiffMetaItemResponseParsed (type)",
+ "DiffMetaItemResponseSchema": "src/api/protocol.zod.ts#DiffMetaItemResponseSchema (const)",
"DisablePackageRequest": "src/kernel/package-registry.zod.ts#DisablePackageRequest (type)",
"DisablePackageRequestSchema": "src/kernel/package-registry.zod.ts#DisablePackageRequestSchema (const)",
"DisablePackageResponse": "src/kernel/package-registry.zod.ts#DisablePackageResponse (type)",
"DisablePackageResponseSchema": "src/kernel/package-registry.zod.ts#DisablePackageResponseSchema (const)",
+ "DiscardPackageDraftsResponse": "src/api/package-lifecycle.zod.ts#DiscardPackageDraftsResponse (type)",
+ "DiscardPackageDraftsResponseParsed": "src/api/package-lifecycle.zod.ts#DiscardPackageDraftsResponseParsed (type)",
+ "DiscardPackageDraftsResponseSchema": "src/api/package-lifecycle.zod.ts#DiscardPackageDraftsResponseSchema (const)",
"DiscoveryEnvironment": "src/api/discovery.zod.ts#DiscoveryEnvironment (type)",
"DiscoveryEnvironmentSchema": "src/api/discovery.zod.ts#DiscoveryEnvironmentSchema (const)",
"DiscoveryResponse": "src/api/discovery.zod.ts#DiscoveryResponse (type)",
@@ -293,6 +299,9 @@
"DispatcherRouteSchema": "src/api/dispatcher.zod.ts#DispatcherRouteSchema (const)",
"DocumentState": "src/api/websocket.zod.ts#DocumentState (type)",
"DocumentStateSchema": "src/api/websocket.zod.ts#DocumentStateSchema (const)",
+ "DuplicatePackageResponse": "src/api/package-lifecycle.zod.ts#DuplicatePackageResponse (type)",
+ "DuplicatePackageResponseParsed": "src/api/package-lifecycle.zod.ts#DuplicatePackageResponseParsed (type)",
+ "DuplicatePackageResponseSchema": "src/api/package-lifecycle.zod.ts#DuplicatePackageResponseSchema (const)",
"ERROR_CODE_LEDGER": "src/api/error-code-ledger.zod.ts#ERROR_CODE_LEDGER (const)",
"ETag": "src/api/http-cache.zod.ts#ETag (type)",
"ETagParsed": "src/api/http-cache.zod.ts#ETagParsed (type)",
@@ -371,6 +380,9 @@
"FindDataRequestSchema": "src/api/protocol.zod.ts#FindDataRequestSchema (const)",
"FindDataResponse": "src/api/protocol.zod.ts#FindDataResponse (type)",
"FindDataResponseSchema": "src/api/protocol.zod.ts#FindDataResponseSchema (const)",
+ "FindReferencesToMetaResponse": "src/api/protocol.zod.ts#FindReferencesToMetaResponse (type)",
+ "FindReferencesToMetaResponseParsed": "src/api/protocol.zod.ts#FindReferencesToMetaResponseParsed (type)",
+ "FindReferencesToMetaResponseSchema": "src/api/protocol.zod.ts#FindReferencesToMetaResponseSchema (const)",
"FlowSummary": "src/api/automation-api.zod.ts#FlowSummary (type)",
"FlowSummarySchema": "src/api/automation-api.zod.ts#FlowSummarySchema (const)",
"GeneratedApiDocumentation": "src/api/documentation.zod.ts#GeneratedApiDocumentation (type)",
@@ -419,6 +431,9 @@
"GetLocalesResponse": "src/api/protocol.zod.ts#GetLocalesResponse (type)",
"GetLocalesResponseParsed": "src/api/protocol.zod.ts#GetLocalesResponseParsed (type)",
"GetLocalesResponseSchema": "src/api/protocol.zod.ts#GetLocalesResponseSchema (const)",
+ "GetMetaDiagnosticsResponse": "src/api/protocol.zod.ts#GetMetaDiagnosticsResponse (type)",
+ "GetMetaDiagnosticsResponseParsed": "src/api/protocol.zod.ts#GetMetaDiagnosticsResponseParsed (type)",
+ "GetMetaDiagnosticsResponseSchema": "src/api/protocol.zod.ts#GetMetaDiagnosticsResponseSchema (const)",
"GetMetaItemCachedRequest": "src/api/protocol.zod.ts#GetMetaItemCachedRequest (type)",
"GetMetaItemCachedRequestSchema": "src/api/protocol.zod.ts#GetMetaItemCachedRequestSchema (const)",
"GetMetaItemCachedResponse": "src/api/protocol.zod.ts#GetMetaItemCachedResponse (type)",
@@ -461,6 +476,9 @@
"GetPresignedUrlRequest": "src/api/storage.zod.ts#GetPresignedUrlRequest (type)",
"GetPresignedUrlRequestParsed": "src/api/storage.zod.ts#GetPresignedUrlRequestParsed (type)",
"GetPresignedUrlRequestSchema": "src/api/storage.zod.ts#GetPresignedUrlRequestSchema (const)",
+ "GetPublishedMetaItemResponse": "src/api/protocol.zod.ts#GetPublishedMetaItemResponse (type)",
+ "GetPublishedMetaItemResponseParsed": "src/api/protocol.zod.ts#GetPublishedMetaItemResponseParsed (type)",
+ "GetPublishedMetaItemResponseSchema": "src/api/protocol.zod.ts#GetPublishedMetaItemResponseSchema (const)",
"GetRunRequest": "src/api/automation-api.zod.ts#GetRunRequest (type)",
"GetRunRequestSchema": "src/api/automation-api.zod.ts#GetRunRequestSchema (const)",
"GetRunResponse": "src/api/automation-api.zod.ts#GetRunResponse (type)",
@@ -529,6 +547,9 @@
"ListAiPendingActionsRequestSchema": "src/api/protocol.zod.ts#ListAiPendingActionsRequestSchema (const)",
"ListAiPendingActionsResponse": "src/api/protocol.zod.ts#ListAiPendingActionsResponse (type)",
"ListAiPendingActionsResponseSchema": "src/api/protocol.zod.ts#ListAiPendingActionsResponseSchema (const)",
+ "ListDraftsResponse": "src/api/protocol.zod.ts#ListDraftsResponse (type)",
+ "ListDraftsResponseParsed": "src/api/protocol.zod.ts#ListDraftsResponseParsed (type)",
+ "ListDraftsResponseSchema": "src/api/protocol.zod.ts#ListDraftsResponseSchema (const)",
"ListExportJobsRequest": "src/api/export.zod.ts#ListExportJobsRequest (type)",
"ListExportJobsRequestParsed": "src/api/export.zod.ts#ListExportJobsRequestParsed (type)",
"ListExportJobsRequestSchema": "src/api/export.zod.ts#ListExportJobsRequestSchema (const)",
@@ -558,6 +579,9 @@
"ListNotificationsResponse": "src/api/protocol.zod.ts#ListNotificationsResponse (type)",
"ListNotificationsResponseParsed": "src/api/protocol.zod.ts#ListNotificationsResponseParsed (type)",
"ListNotificationsResponseSchema": "src/api/protocol.zod.ts#ListNotificationsResponseSchema (const)",
+ "ListPackageCommitsResponse": "src/api/package-lifecycle.zod.ts#ListPackageCommitsResponse (type)",
+ "ListPackageCommitsResponseParsed": "src/api/package-lifecycle.zod.ts#ListPackageCommitsResponseParsed (type)",
+ "ListPackageCommitsResponseSchema": "src/api/package-lifecycle.zod.ts#ListPackageCommitsResponseSchema (const)",
"ListPackagesRequest": "src/kernel/package-registry.zod.ts#ListPackagesRequest (type)",
"ListPackagesRequestSchema": "src/kernel/package-registry.zod.ts#ListPackagesRequestSchema (const)",
"ListPackagesResponse": "src/kernel/package-registry.zod.ts#ListPackagesResponse (type)",
@@ -705,6 +729,9 @@
"OperatorMappingSchema": "src/api/query-adapter.zod.ts#OperatorMappingSchema (const)",
"PackageApiContracts": "src/api/package-api.zod.ts#PackageApiContracts (const)",
"PackageApiErrorCode": "src/api/package-api.zod.ts#PackageApiErrorCode (type)",
+ "PackageExportManifest": "src/api/package-lifecycle.zod.ts#PackageExportManifest (type)",
+ "PackageExportManifestParsed": "src/api/package-lifecycle.zod.ts#PackageExportManifestParsed (type)",
+ "PackageExportManifestSchema": "src/api/package-lifecycle.zod.ts#PackageExportManifestSchema (const)",
"PackageInstallRequest": "src/api/package-api.zod.ts#PackageInstallRequest (type)",
"PackageInstallRequestParsed": "src/api/package-api.zod.ts#PackageInstallRequestParsed (type)",
"PackageInstallRequestSchema": "src/api/package-api.zod.ts#PackageInstallRequestSchema (const)",
@@ -714,12 +741,11 @@
"PackagePathParams": "src/api/package-api.zod.ts#PackagePathParams (type)",
"PackagePathParamsSchema": "src/api/package-api.zod.ts#PackagePathParamsSchema (const)",
"PackageProtocol": "src/api/protocol.zod.ts#PackageProtocol (interface)",
+ "PackagePublishResult": "src/system/metadata-persistence.zod.ts#PackagePublishResult (type)",
+ "PackagePublishResultSchema": "src/system/metadata-persistence.zod.ts#PackagePublishResultSchema (const)",
"PackageRollbackRequest": "src/api/package-api.zod.ts#PackageRollbackRequest (type)",
"PackageRollbackRequestParsed": "src/api/package-api.zod.ts#PackageRollbackRequestParsed (type)",
"PackageRollbackRequestSchema": "src/api/package-api.zod.ts#PackageRollbackRequestSchema (const)",
- "PackageRollbackResponse": "src/api/package-api.zod.ts#PackageRollbackResponse (type)",
- "PackageRollbackResponseParsed": "src/api/package-api.zod.ts#PackageRollbackResponseParsed (type)",
- "PackageRollbackResponseSchema": "src/api/package-api.zod.ts#PackageRollbackResponseSchema (const)",
"PackageStatus": "src/kernel/package-registry.zod.ts#PackageStatus (type)",
"PackageUpgradeRequest": "src/api/package-api.zod.ts#PackageUpgradeRequest (type)",
"PackageUpgradeRequestParsed": "src/api/package-api.zod.ts#PackageUpgradeRequestParsed (type)",
@@ -786,6 +812,9 @@
"RealtimeUnsubscribeRequestSchema": "src/api/protocol.zod.ts#RealtimeUnsubscribeRequestSchema (const)",
"RealtimeUnsubscribeResponse": "src/api/protocol.zod.ts#RealtimeUnsubscribeResponse (type)",
"RealtimeUnsubscribeResponseSchema": "src/api/protocol.zod.ts#RealtimeUnsubscribeResponseSchema (const)",
+ "ReassignOrphanedMetadataResponse": "src/api/package-lifecycle.zod.ts#ReassignOrphanedMetadataResponse (type)",
+ "ReassignOrphanedMetadataResponseParsed": "src/api/package-lifecycle.zod.ts#ReassignOrphanedMetadataResponseParsed (type)",
+ "ReassignOrphanedMetadataResponseSchema": "src/api/package-lifecycle.zod.ts#ReassignOrphanedMetadataResponseSchema (const)",
"RecordData": "src/api/contract.zod.ts#RecordData (type)",
"RecordDataSchema": "src/api/contract.zod.ts#RecordDataSchema (const)",
"RefreshTokenRequest": "src/api/auth.zod.ts#RefreshTokenRequest (type)",
@@ -808,6 +837,12 @@
"ResolveDependenciesResponse": "src/api/package-api.zod.ts#ResolveDependenciesResponse (type)",
"ResolveDependenciesResponseParsed": "src/api/package-api.zod.ts#ResolveDependenciesResponseParsed (type)",
"ResolveDependenciesResponseSchema": "src/api/package-api.zod.ts#ResolveDependenciesResponseSchema (const)",
+ "ResolvedBook": "src/system/book.zod.ts#ResolvedBook (interface)",
+ "ResolvedBookSchema": "src/system/book.zod.ts#ResolvedBookSchema (const)",
+ "ResolvedEntry": "src/system/book.zod.ts#ResolvedEntry (interface)",
+ "ResolvedEntrySchema": "src/system/book.zod.ts#ResolvedEntrySchema (const)",
+ "ResolvedGroup": "src/system/book.zod.ts#ResolvedGroup (interface)",
+ "ResolvedGroupSchema": "src/system/book.zod.ts#ResolvedGroupSchema (const)",
"ResponseEnvelopeConfig": "src/api/plugin-rest-api.zod.ts#ResponseEnvelopeConfig (type)",
"ResponseEnvelopeConfigParsed": "src/api/plugin-rest-api.zod.ts#ResponseEnvelopeConfigParsed (type)",
"ResponseEnvelopeConfigSchema": "src/api/plugin-rest-api.zod.ts#ResponseEnvelopeConfigSchema (const)",
@@ -831,6 +866,15 @@
"RestServerConfigParsed": "src/api/rest-server.zod.ts#RestServerConfigParsed (type)",
"RestServerConfigSchema": "src/api/rest-server.zod.ts#RestServerConfigSchema (const)",
"RetryStrategy": "src/api/errors.zod.ts#RetryStrategy (type)",
+ "RevertPackageCommitResponse": "src/api/package-lifecycle.zod.ts#RevertPackageCommitResponse (type)",
+ "RevertPackageCommitResponseParsed": "src/api/package-lifecycle.zod.ts#RevertPackageCommitResponseParsed (type)",
+ "RevertPackageCommitResponseSchema": "src/api/package-lifecycle.zod.ts#RevertPackageCommitResponseSchema (const)",
+ "RollbackMetaItemResponse": "src/api/protocol.zod.ts#RollbackMetaItemResponse (type)",
+ "RollbackMetaItemResponseParsed": "src/api/protocol.zod.ts#RollbackMetaItemResponseParsed (type)",
+ "RollbackMetaItemResponseSchema": "src/api/protocol.zod.ts#RollbackMetaItemResponseSchema (const)",
+ "RollbackToPackageCommitResponse": "src/api/package-lifecycle.zod.ts#RollbackToPackageCommitResponse (type)",
+ "RollbackToPackageCommitResponseParsed": "src/api/package-lifecycle.zod.ts#RollbackToPackageCommitResponseParsed (type)",
+ "RollbackToPackageCommitResponseSchema": "src/api/package-lifecycle.zod.ts#RollbackToPackageCommitResponseSchema (const)",
"RouteCategory": "src/api/router.zod.ts#RouteCategory (type)",
"RouteCoverageEntry": "src/api/plugin-rest-api.zod.ts#RouteCoverageEntry (type)",
"RouteCoverageEntrySchema": "src/api/plugin-rest-api.zod.ts#RouteCoverageEntrySchema (const)",
diff --git a/packages/spec/export-origins/system.json b/packages/spec/export-origins/system.json
index 72c3512bd7..81674d4446 100644
--- a/packages/spec/export-origins/system.json
+++ b/packages/spec/export-origins/system.json
@@ -525,9 +525,12 @@
"RenderValidationMessageOptions": "src/system/validation-message.ts#RenderValidationMessageOptions (interface)",
"ResolveOptions": "src/system/i18n-resolver.ts#ResolveOptions (interface)",
"ResolvedBook": "src/system/book.zod.ts#ResolvedBook (interface)",
+ "ResolvedBookSchema": "src/system/book.zod.ts#ResolvedBookSchema (const)",
"ResolvedEntry": "src/system/book.zod.ts#ResolvedEntry (interface)",
+ "ResolvedEntrySchema": "src/system/book.zod.ts#ResolvedEntrySchema (const)",
"ResolvedFieldLabel": "src/system/i18n-resolver.ts#ResolvedFieldLabel (interface)",
"ResolvedGroup": "src/system/book.zod.ts#ResolvedGroup (interface)",
+ "ResolvedGroupSchema": "src/system/book.zod.ts#ResolvedGroupSchema (const)",
"ResolvedSettingValue": "src/system/settings-manifest.zod.ts#ResolvedSettingValue (type)",
"ResolvedSettingValueSchema": "src/system/settings-manifest.zod.ts#ResolvedSettingValueSchema (const)",
"ResolverDoc": "src/system/book.zod.ts#ResolverDoc (interface)",
diff --git a/packages/spec/json-schema.manifest/api.json b/packages/spec/json-schema.manifest/api.json
index a5fe08bfe0..eb06f140d5 100644
--- a/packages/spec/json-schema.manifest/api.json
+++ b/packages/spec/json-schema.manifest/api.json
@@ -114,8 +114,10 @@
"api/DeleteResponse",
"api/DeviceRequestResponse",
"api/DeviceTokenResponse",
+ "api/DiffMetaItemResponse",
"api/DisablePackageRequest",
"api/DisablePackageResponse",
+ "api/DiscardPackageDraftsResponse",
"api/Discovery",
"api/DiscoveryEnvironment",
"api/DispatcherConfig",
@@ -123,6 +125,7 @@
"api/DispatcherErrorResponse",
"api/DispatcherRoute",
"api/DocumentState",
+ "api/DuplicatePackageResponse",
"api/ETag",
"api/EditMessage",
"api/EditOperation",
@@ -155,6 +158,7 @@
"api/FileUploadResponse",
"api/FindDataRequest",
"api/FindDataResponse",
+ "api/FindReferencesToMetaResponse",
"api/FlowSummary",
"api/GeneratedApiDocumentation",
"api/GeneratedEndpoint",
@@ -176,6 +180,7 @@
"api/GetInstalledPackageResponse",
"api/GetLocalesRequest",
"api/GetLocalesResponse",
+ "api/GetMetaDiagnosticsResponse",
"api/GetMetaItemCachedRequest",
"api/GetMetaItemCachedResponse",
"api/GetMetaItemLayeredRequest",
@@ -195,6 +200,7 @@
"api/GetPresenceRequest",
"api/GetPresenceResponse",
"api/GetPresignedUrlRequest",
+ "api/GetPublishedMetaItemResponse",
"api/GetRunRequest",
"api/GetRunResponse",
"api/GetTranslationsRequest",
@@ -225,6 +231,7 @@
"api/ListAiConversationsResponse",
"api/ListAiPendingActionsRequest",
"api/ListAiPendingActionsResponse",
+ "api/ListDraftsResponse",
"api/ListExportJobsRequest",
"api/ListExportJobsResponse",
"api/ListFlowsRequest",
@@ -235,6 +242,7 @@
"api/ListInstalledPackagesResponse",
"api/ListNotificationsRequest",
"api/ListNotificationsResponse",
+ "api/ListPackageCommitsResponse",
"api/ListPackagesRequest",
"api/ListPackagesResponse",
"api/ListRecordResponse",
@@ -293,11 +301,12 @@
"api/OpenApiSpec",
"api/OperatorMapping",
"api/PackageApiErrorCode",
+ "api/PackageExportManifest",
"api/PackageInstallRequest",
"api/PackageInstallResponse",
"api/PackagePathParams",
+ "api/PackagePublishResult",
"api/PackageRollbackRequest",
- "api/PackageRollbackResponse",
"api/PackageUpgradeRequest",
"api/PackageUpgradeResponse",
"api/PingMessage",
@@ -327,6 +336,7 @@
"api/RealtimeSubscribeResponse",
"api/RealtimeUnsubscribeRequest",
"api/RealtimeUnsubscribeResponse",
+ "api/ReassignOrphanedMetadataResponse",
"api/RecordData",
"api/RefreshTokenRequest",
"api/RegisterDeviceRequest",
@@ -336,6 +346,9 @@
"api/RequestValidationConfig",
"api/ResolveDependenciesRequest",
"api/ResolveDependenciesResponse",
+ "api/ResolvedBook",
+ "api/ResolvedEntry",
+ "api/ResolvedGroup",
"api/ResponseEnvelopeConfig",
"api/RestApiConfig",
"api/RestApiEndpoint",
@@ -345,6 +358,9 @@
"api/RestQueryAdapter",
"api/RestServerConfig",
"api/RetryStrategy",
+ "api/RevertPackageCommitResponse",
+ "api/RollbackMetaItemResponse",
+ "api/RollbackToPackageCommitResponse",
"api/RouteCategory",
"api/RouteCoverageEntry",
"api/RouteCoverageReport",
diff --git a/packages/spec/json-schema.manifest/system.json b/packages/spec/json-schema.manifest/system.json
index 356752993b..ae479dc174 100644
--- a/packages/spec/json-schema.manifest/system.json
+++ b/packages/spec/json-schema.manifest/system.json
@@ -200,6 +200,9 @@
"system/RegistryUpstream",
"system/RemoveFieldOperation",
"system/RenameObjectOperation",
+ "system/ResolvedBook",
+ "system/ResolvedEntry",
+ "system/ResolvedGroup",
"system/ResolvedSettingValue",
"system/RetryPolicy",
"system/RollbackPlan",
diff --git a/packages/spec/llms.txt b/packages/spec/llms.txt
index a4f16e8259..ed42d56099 100644
--- a/packages/spec/llms.txt
+++ b/packages/spec/llms.txt
@@ -77,7 +77,7 @@ const query = {
---
-## 3. Schema Inventory by Domain (207 schemas)
+## 3. Schema Inventory by Domain (208 schemas)
Counted as `*.zod.ts` modules under `packages/spec/src//` — the sources
that ship in this tarball (`files` includes `src/**/*.zod.ts`), so every number
@@ -88,7 +88,7 @@ here is verifiable from the installed package.
| system | 36 | Auth, Cache, Compliance, Encryption, HTTP Server, License, Logging, Metrics |
| kernel | 32 | Plugin, Manifest, Events (6 sub-modules), Feature, Context, Package Registry |
| data | 30 | Object, Field, Query, Filter, Driver (SQL/NoSQL/Memory/Mongo/Postgres), Cube |
-| api | 29 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope |
+| api | 30 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope, Package Lifecycle |
| ui | 18 | View, App, Action, Dashboard, Page, Chart, Component, Animation |
| automation | 13 | Flow, Approval, BPMN Interop, Control Flow, State Machine, Webhook |
| shared | 13 | Enums, HTTP, Identifiers, Mapping, Metadata Types, Connector Auth, Retry Policy |
diff --git a/packages/spec/src/api/index.ts b/packages/spec/src/api/index.ts
index d17481a7bf..f164145ccd 100644
--- a/packages/spec/src/api/index.ts
+++ b/packages/spec/src/api/index.ts
@@ -79,3 +79,16 @@ export * from './query-adapter.zod';
export * from './export.zod';
export * from './automation-api.zod';
export * from './package-api.zod';
+// #12038 — the package lifecycle response contracts (ADR-0067 commit
+// timeline, draft batch doors, ADR-0070 export/adopt/duplicate), including
+// the ruling-5A `/api` re-export of `PackagePublishResultSchema`.
+export * from './package-lifecycle.zod';
+// Ruling 5A (#12038): the book-tree response contract is declared beside its
+// resolver in `../system/book.zod` — re-exported here (never a second copy)
+// so the route-ledger resolver, which searches only `@objectstack/spec/api`,
+// can name it.
+export { ResolvedEntrySchema, ResolvedGroupSchema, ResolvedBookSchema } from '../system/book.zod';
+// …with their existing types (the interfaces `resolveBookTree` is typed by,
+// pinned type-identical to the schemas in `system/book.test.ts`) — the same
+// single-declaration re-export, so the `/api` page's import line works.
+export type { ResolvedEntry, ResolvedGroup, ResolvedBook } from '../system/book.zod';
diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts
index bc247d6362..fd90f2fea0 100644
--- a/packages/spec/src/api/package-api.test.ts
+++ b/packages/spec/src/api/package-api.test.ts
@@ -14,7 +14,6 @@ import {
UploadArtifactRequestSchema,
UploadArtifactResponseSchema,
PackageRollbackRequestSchema,
- PackageRollbackResponseSchema,
UninstallPackageApiRequestSchema,
UninstallPackageApiResponseSchema,
PackageApiErrorCode,
@@ -356,20 +355,11 @@ describe('PackageRollbackRequestSchema', () => {
});
});
-describe('PackageRollbackResponseSchema', () => {
- it('should accept a successful rollback response', () => {
- const result = PackageRollbackResponseSchema.parse({
- success: true,
- data: {
- success: true,
- restoredVersion: '1.0.0',
- message: 'Rolled back to version 1.0.0',
- },
- });
- expect(result.data.success).toBe(true);
- expect(result.data.restoredVersion).toBe('1.0.0');
- });
-});
+// `PackageRollbackResponseSchema` is RETIRED (#12038 3A) — it declared a
+// VERSION rollback against the live COMMIT-rollback route. Its retirement pin
+// (runtime namespace probes, the registry-retirement.test.ts pattern) lives in
+// the `package-rollback-response retirement` block at the end of this file;
+// the live route's true contract is covered in package-lifecycle.test.ts.
// ==========================================
// Uninstall Package
@@ -425,7 +415,6 @@ describe('PackageApiContracts', () => {
expect(PackageApiContracts.upgradePackage).toBeDefined();
expect(PackageApiContracts.resolveDependencies).toBeDefined();
expect(PackageApiContracts.uploadArtifact).toBeDefined();
- expect(PackageApiContracts.rollbackPackage).toBeDefined();
expect(PackageApiContracts.uninstallPackage).toBeDefined();
});
@@ -436,7 +425,6 @@ describe('PackageApiContracts', () => {
expect(PackageApiContracts.upgradePackage.method).toBe('POST');
expect(PackageApiContracts.resolveDependencies.method).toBe('POST');
expect(PackageApiContracts.uploadArtifact.method).toBe('POST');
- expect(PackageApiContracts.rollbackPackage.method).toBe('POST');
expect(PackageApiContracts.uninstallPackage.method).toBe('DELETE');
});
@@ -447,7 +435,6 @@ describe('PackageApiContracts', () => {
expect(PackageApiContracts.upgradePackage.path).toBe('/api/v1/packages/upgrade');
expect(PackageApiContracts.resolveDependencies.path).toBe('/api/v1/packages/resolve-dependencies');
expect(PackageApiContracts.uploadArtifact.path).toBe('/api/v1/packages/upload');
- expect(PackageApiContracts.rollbackPackage.path).toBe('/api/v1/packages/:packageId/rollback');
expect(PackageApiContracts.uninstallPackage.path).toBe('/api/v1/packages/:packageId');
});
@@ -460,3 +447,32 @@ describe('PackageApiContracts', () => {
});
});
});
+
+// ==========================================
+// package-rollback-response retirement (#12038 3A)
+// ==========================================
+
+describe('package-rollback-response retirement (#12038 3A)', () => {
+ // Runtime namespace probes, the registry-retirement.test.ts pattern: a
+ // removed export cannot be imported by name (would not compile), so the pin
+ // asks the namespace object. Anti-vacuity guard: a neighbour that stayed.
+ it('`PackageRollbackResponseSchema` is no longer exported from `@objectstack/spec/api`', async () => {
+ const api = await import('./index');
+ const ns = api as unknown as Record;
+ expect(Object.prototype.hasOwnProperty.call(ns, 'PackageRollbackResponseSchema')).toBe(false);
+ // Anti-vacuity: the sibling that deliberately stayed still resolves.
+ expect(Object.prototype.hasOwnProperty.call(ns, 'PackageRollbackRequestSchema')).toBe(true);
+ });
+
+ it('`PackageApiContracts` no longer binds any schema to the live commit-rollback path', () => {
+ const entries = Object.entries(PackageApiContracts) as Array<[string, { path: string }]>;
+ expect('rollbackPackage' in PackageApiContracts).toBe(false);
+ // The load-bearing half: no contract-map entry claims the LIVE path the
+ // dispatcher serves with `rollbackToPackageCommit` (#12038 §5.2 — the
+ // retired entry bound the version-rollback schema to exactly this path).
+ const claimants = entries.filter(([, c]) => c.path === '/api/v1/packages/:packageId/rollback');
+ expect(claimants).toEqual([]);
+ // Anti-vacuity: the map still carries its surviving entries.
+ expect(entries.length).toBeGreaterThan(0);
+ });
+});
diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts
index 6efdaf7c6c..2b4dd050ff 100644
--- a/packages/spec/src/api/package-api.zod.ts
+++ b/packages/spec/src/api/package-api.zod.ts
@@ -325,19 +325,22 @@ export type PackageRollbackRequest = z.input;
-/**
- * Response after rolling back a package.
- */
-export const PackageRollbackResponseSchema = lazySchema(() => BaseResponseSchema.extend({
- data: z.object({
- success: z.boolean().describe('Whether the rollback succeeded'),
- restoredVersion: z.string().optional().describe('Restored version'),
- message: z.string().optional().describe('Rollback status message'),
- }),
-}).describe('Rollback package response'));
-export type PackageRollbackResponse = z.input;
-/** Post-parse shape of {@link PackageRollbackResponse} — defaults applied, transforms run (ADR-0122). */
-export type PackageRollbackResponseParsed = z.infer;
+// RETIRED (#12038, maintainer ruling 2026-08-27, sub-question 3A):
+// `PackageRollbackResponseSchema` (with its `PackageRollbackResponse` /
+// `PackageRollbackResponseParsed` types) declared a VERSION rollback —
+// `{ success, restoredVersion?, message? }` — while the live
+// `POST /packages/:id/rollback` route posts `{ commitId }` and the dispatcher
+// routes it to `rollbackToPackageCommit`, the ADR-0067 COMMIT rollback: a
+// different operation with a different result. The `PackageApiContracts`
+// `rollbackPackage` entry bound that wrong-operation schema to the exact live
+// path, so a future sweep would have read the false declaration as
+// authoritative. Both went through the ADR-0087 retirement discipline
+// (`RETIRED_DEFS_BY_MAJOR` `api/PackageRollbackResponse`, D3 semantic entry
+// `package-rollback-response-retired`). The TRUE contract for the live route
+// is `RollbackToPackageCommitResponseSchema` in `./package-lifecycle.zod`.
+// `PackageRollbackRequestSchema` above stays published as ruled — only the
+// response declaration and the contract-map binding were retired; the request
+// schema binds to no route now that the contracts entry is gone.
// ==========================================
// 9. Uninstall Package (DELETE /api/v1/packages/:packageId)
@@ -432,12 +435,11 @@ export const PackageApiContracts = {
input: UploadArtifactRequestSchema,
output: UploadArtifactResponseSchema,
},
- rollbackPackage: {
- method: 'POST' as const,
- path: '/api/v1/packages/:packageId/rollback',
- input: PackageRollbackRequestSchema,
- output: PackageRollbackResponseSchema,
- },
+ // `rollbackPackage` RETIRED (#12038 3A) — it bound the version-rollback
+ // schemas to the live `/api/v1/packages/:packageId/rollback` path, which
+ // actually serves the ADR-0067 COMMIT rollback (`rollbackToPackageCommit`).
+ // The live route's true contract is `RollbackToPackageCommitResponseSchema`
+ // (`./package-lifecycle.zod`), named by its route-ledger row.
uninstallPackage: {
method: 'DELETE' as const,
path: '/api/v1/packages/:packageId',
diff --git a/packages/spec/src/api/package-lifecycle.test.ts b/packages/spec/src/api/package-lifecycle.test.ts
new file mode 100644
index 0000000000..4abec190d7
--- /dev/null
+++ b/packages/spec/src/api/package-lifecycle.test.ts
@@ -0,0 +1,251 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * Conformance coverage for the #12038 package lifecycle response contracts —
+ * the #3877 rule's other half: no ledger row names a schema without a suite
+ * parsing a verbatim-shaped capture through it (the AuditMetaItemResponse
+ * pattern in `protocol.test.ts`). Each capture below is handwritten from the
+ * producer's declared return, NOT lifted from the client-test mocks the
+ * #12038 survey found inventing shapes these routes never answered (§3b).
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ DiscardPackageDraftsResponseSchema,
+ ListPackageCommitsResponseSchema,
+ RevertPackageCommitResponseSchema,
+ RollbackToPackageCommitResponseSchema,
+ PackageExportManifestSchema,
+ ReassignOrphanedMetadataResponseSchema,
+ DuplicatePackageResponseSchema,
+ PackagePublishResultSchema,
+} from './package-lifecycle.zod';
+import { PackagePublishResultSchema as SystemPackagePublishResultSchema } from '../system/metadata-persistence.zod';
+
+describe('the ruling-5A re-export of PackagePublishResultSchema (#12038)', () => {
+ it('is the SAME declaration as the `/system` original — a re-export, never a second copy', () => {
+ expect(PackagePublishResultSchema).toBe(SystemPackagePublishResultSchema);
+ });
+
+ it('parses a verbatim-shaped capture of a real `publishPackage` return and PRESERVES it', () => {
+ const realResponse = {
+ success: true,
+ packageId: 'com.example.crm',
+ version: 4,
+ publishedAt: '2026-08-27T10:03:12.000Z',
+ itemsPublished: 23,
+ };
+ const result = PackagePublishResultSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+});
+
+describe('DiscardPackageDraftsResponseSchema declares the discard-drafts body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `discardPackageDrafts` return (one failure). */
+ const realResponse = {
+ success: false,
+ discardedCount: 2,
+ failedCount: 1,
+ discarded: [
+ { type: 'view', name: 'account_pipeline' },
+ { type: 'object', name: 'lead_source' },
+ ],
+ failed: [{ type: 'flow', name: 'lead_convert', error: 'item is locked', code: 'ITEM_LOCKED' }],
+ };
+
+ it('parses the real discard report and PRESERVES every member', () => {
+ const result = DiscardPackageDraftsResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the honest-empty report parses — nothing to discard is a declared, legal body', () => {
+ const empty = { success: true, discardedCount: 0, failedCount: 0, discarded: [], failed: [] };
+ expect(DiscardPackageDraftsResponseSchema.safeParse(empty).success).toBe(true);
+ });
+});
+
+describe('ListPackageCommitsResponseSchema declares the commit timeline body (#12038)', () => {
+ /** A verbatim-shaped capture of the HANDLER's `{ commits }` payload (the wrapper is the handler's, not the protocol's). */
+ const realResponse = {
+ commits: [
+ {
+ id: 'cmt_02',
+ operation: 'revert' as const,
+ message: 'Revert broken publish',
+ actor: 'admin@objectos.ai',
+ parentCommitId: 'cmt_01',
+ itemCount: 1,
+ items: [{ type: 'view', name: 'account_pipeline', existedBefore: true, prevVersion: 3 }],
+ createdAt: '2026-08-27T11:41:00.000Z',
+ },
+ {
+ id: 'cmt_01',
+ operation: 'apply' as const,
+ aiModel: 'claude',
+ itemCount: 2,
+ items: [
+ { type: 'view', name: 'account_pipeline', existedBefore: false, prevVersion: null },
+ { type: 'object', name: 'lead_source', existedBefore: true, prevVersion: 1 },
+ ],
+ },
+ ],
+ };
+
+ it('parses the real timeline and PRESERVES every member', () => {
+ const result = ListPackageCommitsResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('keeps the operation vocabulary closed', () => {
+ const bad = {
+ commits: [{ ...realResponse.commits[1], operation: 'merge' }],
+ };
+ expect(ListPackageCommitsResponseSchema.safeParse(bad).success).toBe(false);
+ });
+
+ it('the honest-empty timeline parses — {commits: []} is a declared, legal body', () => {
+ expect(ListPackageCommitsResponseSchema.safeParse({ commits: [] }).success).toBe(true);
+ });
+});
+
+describe('RevertPackageCommitResponseSchema declares the revert body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `revertCommit` return. */
+ const realResponse = {
+ success: true,
+ revertedCount: 2,
+ failedCount: 0,
+ reverted: [
+ { type: 'view', name: 'account_pipeline', action: 'restored' as const },
+ { type: 'object', name: 'lead_source', action: 'removed' as const },
+ ],
+ failed: [],
+ revertCommitId: 'cmt_03',
+ };
+
+ it('parses the real revert report and PRESERVES every member', () => {
+ const result = RevertPackageCommitResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('keeps the action vocabulary closed', () => {
+ const bad = { ...realResponse, reverted: [{ type: 'view', name: 'v', action: 'skipped' }] };
+ expect(RevertPackageCommitResponseSchema.safeParse(bad).success).toBe(false);
+ });
+});
+
+describe('RollbackToPackageCommitResponseSchema declares the COMMIT-rollback body (#12038 3A)', () => {
+ /** A verbatim-shaped capture of a real `rollbackToPackageCommit` return (one commit stuck). */
+ const realResponse = {
+ success: false,
+ revertedCommits: ['cmt_05', 'cmt_04'],
+ failed: [{ commitId: 'cmt_03', error: 'commit not found' }],
+ };
+
+ it('parses the real rollback report and PRESERVES every member', () => {
+ const result = RollbackToPackageCommitResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the clean rollback parses — every commit reverted, nothing failed', () => {
+ const clean = { success: true, revertedCommits: ['cmt_05'], failed: [] };
+ expect(RollbackToPackageCommitResponseSchema.safeParse(clean).success).toBe(true);
+ });
+
+ it('does NOT declare the retired version-rollback vocabulary', () => {
+ // The retired `PackageRollbackResponseSchema` declared `restoredVersion` —
+ // a key this route has never answered. The declared key set is pinned so
+ // the wrong-operation shape cannot quietly return under the new name.
+ const parsed = RollbackToPackageCommitResponseSchema.safeParse({
+ success: true,
+ revertedCommits: [],
+ failed: [],
+ restoredVersion: '1.0.0',
+ });
+ expect(parsed.success).toBe(true);
+ if (parsed.success) expect('restoredVersion' in (parsed.data as object)).toBe(false);
+ });
+});
+
+describe('PackageExportManifestSchema declares the four fixed keys and stays open (#12038 4A)', () => {
+ /** A verbatim-shaped capture of a real `assemblePackageManifest` return. */
+ const realResponse = {
+ id: 'com.example.crm',
+ name: 'example-crm',
+ version: '2.1.0',
+ label: 'Example CRM',
+ objects: [{ name: 'customer', label: 'Customer', fields: { name: { type: 'text' } } }],
+ views: [{ name: 'customer_list', object: 'customer', type: 'grid' }],
+ };
+
+ it('parses the real manifest and PRESERVES every member — registry-derived keys included', () => {
+ const result = PackageExportManifestSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('requires exactly the four fixed keys — a manifest with no items still parses', () => {
+ const bare = { id: 'com.example.empty', name: 'empty', version: '1.0.0' };
+ expect(PackageExportManifestSchema.safeParse(bare).success).toBe(true);
+ });
+
+ it('refuses a manifest missing its identity — the fixed keys are genuinely pinned', () => {
+ expect(PackageExportManifestSchema.safeParse({ objects: [] }).success).toBe(false);
+ });
+});
+
+describe('ReassignOrphanedMetadataResponseSchema declares the adopt-orphans body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `reassignOrphanedMetadata` return. */
+ const realResponse = {
+ success: true,
+ reassignedCount: 2,
+ reassigned: [
+ { type: 'view', name: 'orphan_view' },
+ { type: 'flow', name: 'orphan_flow' },
+ ],
+ targetPackageId: 'com.example.crm',
+ };
+
+ it('parses the real adoption report and PRESERVES every member', () => {
+ const result = ReassignOrphanedMetadataResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the honest-empty report parses — no orphans to adopt is a declared, legal body', () => {
+ const empty = { success: true, reassignedCount: 0, reassigned: [], targetPackageId: 'com.example.crm' };
+ expect(ReassignOrphanedMetadataResponseSchema.safeParse(empty).success).toBe(true);
+ });
+});
+
+describe('DuplicatePackageResponseSchema declares the duplicate body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `duplicatePackage` return (one copy failure). */
+ const realResponse = {
+ success: false,
+ copiedCount: 1,
+ failedCount: 1,
+ targetPackageId: 'com.example.crm_copy',
+ copied: [{ type: 'object', name: 'customer' }],
+ failed: [{ type: 'view', name: 'customer_list', error: 'name collision in target' }],
+ };
+
+ it('parses the real duplicate report and PRESERVES every member', () => {
+ const result = DuplicatePackageResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('declares the OPERATION verdict at `success` — the objectui#6593 confusion has a declared answer', () => {
+ // On the wire this payload rides the dispatcher envelope; the envelope's
+ // `success` is transport-level and true even here. The schema declares the
+ // payload's own verdict so a consumer reading the declared shape reads the
+ // right key.
+ const result = DuplicatePackageResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect((result.data as { success: boolean }).success).toBe(false);
+ });
+});
diff --git a/packages/spec/src/api/package-lifecycle.zod.ts b/packages/spec/src/api/package-lifecycle.zod.ts
new file mode 100644
index 0000000000..97ff0976ba
--- /dev/null
+++ b/packages/spec/src/api/package-lifecycle.zod.ts
@@ -0,0 +1,238 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+/**
+ * ## Package lifecycle response contracts (#12038)
+ *
+ * Response payloads for the dispatcher-served `packages.*` lifecycle routes —
+ * the ADR-0067 commit timeline, the ADR-0033 draft batch doors, the ADR-0070
+ * export / adopt / duplicate family — ruled on 2026-08-27 (#12038,
+ * 1C · 2C · 3A · 4A · 5A).
+ *
+ * Every schema here is a DESCRIBE-ONLY TRANSCRIPTION of the return type its
+ * producer already declares inline (`@objectstack/metadata-protocol`
+ * `protocol.ts`, except where a schema's own docblock says otherwise) —
+ * authoring one changes no wire byte. All of these routes are served by the
+ * runtime dispatcher ONLY (no REST twin — #12038 survey §1b), which answers
+ * through the `{ success, data }` envelope (`http-dispatcher.ts`), so each
+ * schema declares the `data` payload, envelope-free — the same convention as
+ * `PublishPackageDraftsResponseSchema` and its ledger row.
+ *
+ * `packages.publish`'s contract is NOT here: its producer
+ * (`MetadataManager.publishPackage`) already has an exact published schema,
+ * `PackagePublishResultSchema` in `@objectstack/spec/system` — re-exported
+ * below into this `/api` namespace (ruling 5A: re-export, never a second
+ * copy) because the route-ledger resolver looks names up only in
+ * `@objectstack/spec/api`.
+ *
+ * The retired `PackageRollbackResponseSchema` and its
+ * `PackageApiContracts.rollbackPackage` binding (see `./package-api.zod.ts`)
+ * declared a VERSION rollback against the live COMMIT-rollback path;
+ * `RollbackToPackageCommitResponseSchema` below is the true contract,
+ * authored after that retirement per the ruling's sequencing (3A).
+ */
+
+import { z } from 'zod';
+import { lazySchema } from '../shared/lazy-schema';
+
+// Ruling 5A — the `/api` re-export of the one existing declaration. The
+// schema (and its type) stay declared in `system/metadata-persistence.zod.ts`
+// beside the persistence vocabulary they belong to; this line only makes the
+// name resolvable where the ledger resolver searches.
+export { PackagePublishResultSchema, type PackagePublishResult } from '../system/metadata-persistence.zod';
+
+/**
+ * `POST /packages/:id/discard-drafts` — drop every pending draft bound to
+ * the package (ADR-0033).
+ *
+ * Transcribed from `discardPackageDrafts`'s declared return.
+ */
+export const DiscardPackageDraftsResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe('True exactly when nothing failed.'),
+ discardedCount: z.number().describe('How many drafts were discarded.'),
+ failedCount: z.number().describe('How many drafts could not be discarded.'),
+ discarded: z.array(z.object({
+ type: z.string().describe('Metadata type of the discarded draft.'),
+ name: z.string().describe('Name of the discarded draft.'),
+ })).describe('Every draft that was discarded.'),
+ failed: z.array(z.object({
+ type: z.string().describe('Metadata type of the failing draft.'),
+ name: z.string().describe('Name of the failing draft.'),
+ error: z.string().describe('Why the discard failed.'),
+ code: z.string().optional().describe('Machine-readable failure code, when one was recorded.'),
+ })).describe('Every draft the discard could not remove.'),
+}));
+
+/**
+ * `GET /packages/:id/commits` — the package's ADR-0067 commit timeline,
+ * newest first.
+ *
+ * The element shape is transcribed from `listCommits`'s declared return —
+ * a BARE array. The `{ commits }` wrapper is minted AT THE HANDLER
+ * (`runtime/src/domains/packages.ts`, `success({ commits })`) and nowhere
+ * else; this schema declares the handler's payload, wrapper included, and
+ * that wrapper is declared as the handler's own, not the protocol's
+ * (#12038 survey §1b rider).
+ */
+export const ListPackageCommitsResponseSchema = lazySchema(() => z.object({
+ commits: z.array(z.object({
+ id: z.string().describe('Commit id.'),
+ operation: z.enum(['apply', 'revert']).describe(
+ 'Whether the commit applied changes or reverted an earlier commit.',
+ ),
+ message: z.string().optional().describe('Commit message, when one was recorded.'),
+ actor: z.string().optional().describe('Who made the commit, when recorded.'),
+ aiModel: z.string().optional().describe('AI model that authored the change, when recorded.'),
+ parentCommitId: z.string().optional().describe('The commit this one chains from, when recorded.'),
+ itemCount: z.number().describe('How many items the commit touched.'),
+ items: z.array(z.object({
+ type: z.string().describe('Metadata type of the touched item.'),
+ name: z.string().describe('Name of the touched item.'),
+ existedBefore: z.boolean().describe('Whether the item existed before the commit.'),
+ prevVersion: z.number().nullable().describe(
+ 'The item\'s history version before the commit, `null` when it had none.',
+ ),
+ })).describe('The items the commit touched.'),
+ createdAt: z.string().optional().describe('When the commit was made (ISO-8601 string), when recorded.'),
+ })).describe('The commit timeline, newest first.'),
+}));
+
+/**
+ * `POST /packages/:id/commits/:commitId/revert` — revert ONE commit; the
+ * revert is itself a commit (ADR-0067).
+ *
+ * Transcribed from `revertCommit`'s declared return.
+ */
+export const RevertPackageCommitResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe('True exactly when nothing failed.'),
+ revertedCount: z.number().describe('How many items were reverted.'),
+ failedCount: z.number().describe('How many items could not be reverted.'),
+ reverted: z.array(z.object({
+ type: z.string().describe('Metadata type of the reverted item.'),
+ name: z.string().describe('Name of the reverted item.'),
+ action: z.enum(['removed', 'restored']).describe(
+ 'What the revert did to the item — removed what the commit created, or '
+ + 'restored what it overwrote.',
+ ),
+ })).describe('Every item the revert touched.'),
+ failed: z.array(z.object({
+ type: z.string().describe('Metadata type of the failing item.'),
+ name: z.string().describe('Name of the failing item.'),
+ error: z.string().describe('Why the revert failed for this item.'),
+ code: z.string().optional().describe('Machine-readable failure code, when one was recorded.'),
+ })).describe('Every item the revert could not touch.'),
+ revertCommitId: z.string().optional().describe(
+ 'Id of the commit the revert itself created, when one was written.',
+ ),
+}));
+
+/**
+ * `POST /packages/:id/rollback` — roll back through ALL commits newer than
+ * `commitId` (ADR-0067) — the COMMIT rollback.
+ *
+ * Transcribed from `rollbackToPackageCommit`'s declared return. This is the
+ * TRUE contract for the live path the retired `PackageRollbackResponseSchema`
+ * falsely described as a version rollback (#12038 ruling 3A — retirement
+ * first, then this schema).
+ */
+export const RollbackToPackageCommitResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe('True exactly when nothing failed.'),
+ revertedCommits: z.array(z.string()).describe(
+ 'Ids of the commits that were rolled back, in the order they were reverted.',
+ ),
+ failed: z.array(z.object({
+ commitId: z.string().describe('The commit that could not be reverted.'),
+ error: z.string().describe('Why reverting it failed.'),
+ })).describe('Every commit the rollback could not revert.'),
+}));
+
+/**
+ * `GET /packages/:id/export` — the package's portable manifest (ADR-0070
+ * offline export), the same shape `marketplace-install-local` consumes.
+ *
+ * HONESTLY OPEN (#12038 ruling 4A). `assemblePackageManifest`
+ * (`runtime/src/domains/packages.ts`) builds the key set DYNAMICALLY from the
+ * metadata type registry — one plural key per type present
+ * (`manifest[plural] = items.map(clean)`), partitioning `views` per #5320 —
+ * plus the four fixed keys below. Only the fixed keys are pinnable;
+ * enumerating the registry here would freeze this contract against future
+ * metadata types, so everything else deliberately falls to the open
+ * catch-all. Freezes nothing.
+ */
+export const PackageExportManifestSchema = lazySchema(() => z.object({
+ id: z.string().describe('The exported package\'s id.'),
+ name: z.string().describe('The exported package\'s machine name.'),
+ version: z.string().describe('The exported package\'s version.'),
+ label: z.string().optional().describe('Display label, when the package declares one.'),
+}).catchall(z.unknown().describe(
+ 'One key per metadata type present in the package (plural spelling, e.g. '
+ + '`objects`, `views`), each an array of cleaned item bodies. The key set '
+ + 'is registry-derived at runtime and deliberately NOT enumerated here.',
+)));
+
+/**
+ * `POST /packages/:id/adopt-orphans` — bulk-rebind package-less (orphaned)
+ * metadata into this base (ADR-0070 D5); the client method is
+ * `packages.adoptOrphans`.
+ *
+ * Transcribed from `reassignOrphanedMetadata`'s declared return.
+ */
+export const ReassignOrphanedMetadataResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe('Whether the reassignment ran.'),
+ reassignedCount: z.number().describe('How many orphaned items were adopted.'),
+ reassigned: z.array(z.object({
+ type: z.string().describe('Metadata type of the adopted item.'),
+ name: z.string().describe('Name of the adopted item.'),
+ })).describe('Every item that was adopted.'),
+ targetPackageId: z.string().describe('The package the items were adopted into.'),
+}));
+
+/**
+ * `POST /packages/:id/duplicate` — clone this base into a NEW writable
+ * package, re-namespacing objects and rewriting references (ADR-0070 D4).
+ *
+ * Transcribed from `duplicatePackage`'s declared return. ⚠️ `success` is the
+ * OPERATION's verdict (`failed.length === 0 && copied.length > 0`) — on this
+ * enveloped route a consumer reading the top-level envelope `success` instead
+ * is told a partial or empty duplicate succeeded (the objectui#6593 defect
+ * this declaration exists to end).
+ */
+export const DuplicatePackageResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe(
+ 'The duplicate\'s own verdict: true exactly when nothing failed AND at '
+ + 'least one item was copied.',
+ ),
+ copiedCount: z.number().describe('How many items were copied.'),
+ failedCount: z.number().describe('How many items could not be copied.'),
+ targetPackageId: z.string().describe('The new package the base was cloned into.'),
+ copied: z.array(z.object({
+ type: z.string().describe('Metadata type of the copied item.'),
+ name: z.string().describe('Name of the copied item.'),
+ })).describe('Every item that was copied.'),
+ failed: z.array(z.object({
+ type: z.string().describe('Metadata type of the failing item.'),
+ name: z.string().describe('Name of the failing item.'),
+ error: z.string().describe('Why copying it failed.'),
+ })).describe('Every item the clone could not copy.'),
+}));
+
+export type DiscardPackageDraftsResponse = z.input;
+/** Post-parse shape of {@link DiscardPackageDraftsResponse} — defaults applied, transforms run (ADR-0122). */
+export type DiscardPackageDraftsResponseParsed = z.infer;
+export type ListPackageCommitsResponse = z.input;
+/** Post-parse shape of {@link ListPackageCommitsResponse} — defaults applied, transforms run (ADR-0122). */
+export type ListPackageCommitsResponseParsed = z.infer;
+export type RevertPackageCommitResponse = z.input;
+/** Post-parse shape of {@link RevertPackageCommitResponse} — defaults applied, transforms run (ADR-0122). */
+export type RevertPackageCommitResponseParsed = z.infer;
+export type RollbackToPackageCommitResponse = z.input;
+/** Post-parse shape of {@link RollbackToPackageCommitResponse} — defaults applied, transforms run (ADR-0122). */
+export type RollbackToPackageCommitResponseParsed = z.infer;
+export type PackageExportManifest = z.input;
+/** Post-parse shape of {@link PackageExportManifest} — defaults applied, transforms run (ADR-0122). */
+export type PackageExportManifestParsed = z.infer;
+export type ReassignOrphanedMetadataResponse = z.input;
+/** Post-parse shape of {@link ReassignOrphanedMetadataResponse} — defaults applied, transforms run (ADR-0122). */
+export type ReassignOrphanedMetadataResponseParsed = z.infer;
+export type DuplicatePackageResponse = z.input;
+/** Post-parse shape of {@link DuplicatePackageResponse} — defaults applied, transforms run (ADR-0122). */
+export type DuplicatePackageResponseParsed = z.infer;
diff --git a/packages/spec/src/api/protocol.test.ts b/packages/spec/src/api/protocol.test.ts
index 31efc47d73..1150f0d763 100644
--- a/packages/spec/src/api/protocol.test.ts
+++ b/packages/spec/src/api/protocol.test.ts
@@ -2044,3 +2044,174 @@ describe('MetadataProtocol.deleteMetaItem types against the caught-up request sc
expect(misspelt.name).toBe('account_list');
});
});
+
+// ==========================================
+// Meta history / diagnostics response conformance (#12038)
+// ==========================================
+//
+// The #3877 conformance half of the #12038 bindings — one handwritten,
+// verbatim-shaped capture per newly bound schema (the AuditMetaItemResponse
+// pattern above), asserting the parse PRESERVES every member and that the
+// honest-empty body is a declared, legal answer. The four invented client-test
+// mocks the survey flagged (§3b) are NOT reused here — none of them described
+// a shape these routes ever answered.
+
+import {
+ GetPublishedMetaItemResponseSchema,
+ ListDraftsResponseSchema,
+ GetMetaDiagnosticsResponseSchema,
+ FindReferencesToMetaResponseSchema,
+ RollbackMetaItemResponseSchema,
+ DiffMetaItemResponseSchema,
+} from './protocol.zod';
+
+describe('ListDraftsResponseSchema declares the pending-drafts body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `listDrafts` return (one org draft). */
+ const realResponse = {
+ drafts: [
+ {
+ type: 'view',
+ name: 'account_pipeline',
+ organizationId: 'org_01',
+ packageId: 'com.example.crm',
+ updatedAt: '2026-08-27T09:12:44.000Z',
+ updatedBy: 'admin@objectos.ai',
+ },
+ {
+ type: 'object',
+ name: 'lead_source',
+ organizationId: null,
+ packageId: null,
+ updatedAt: null,
+ updatedBy: null,
+ },
+ ],
+ };
+
+ it('parses the real draft rows and PRESERVES every member', () => {
+ const result = ListDraftsResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the honest-empty answer parses — {drafts: []} is a declared, legal body', () => {
+ expect(ListDraftsResponseSchema.safeParse({ drafts: [] }).success).toBe(true);
+ });
+});
+
+describe('GetMetaDiagnosticsResponseSchema declares the validation-sweep body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `getMetaDiagnostics` return (one failing view). */
+ const realResponse = {
+ entries: [
+ {
+ type: 'view',
+ name: 'broken_pipeline',
+ diagnostics: {
+ valid: false,
+ errors: [{ path: 'columns.0.field', message: 'Unknown field: statuss', code: 'invalid_field' }],
+ warnings: [{ path: 'filters', message: 'Empty filter group' }],
+ },
+ },
+ ],
+ total: 1,
+ scannedTypes: 12,
+ scannedItems: 184,
+ stats: {
+ view: { count: 42, locked: 3, packages: ['com.example.crm'] },
+ object: { count: 17, locked: 0, packages: ['com.example.crm', 'com.example.docs'] },
+ },
+ };
+
+ it('parses the real sweep shape and PRESERVES every member', () => {
+ const result = GetMetaDiagnosticsResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the honest-empty sweep parses — no failing entries is a declared, legal body', () => {
+ const empty = { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
+ expect(GetMetaDiagnosticsResponseSchema.safeParse(empty).success).toBe(true);
+ });
+});
+
+describe('FindReferencesToMetaResponseSchema declares the reverse-references body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `findReferencesToMeta` return. */
+ const realResponse = {
+ references: [
+ { type: 'view', name: 'account_list', label: 'Accounts', path: 'columns.2.field', kind: 'field' },
+ { type: 'flow', name: 'lead_convert', path: 'nodes.3.object', kind: 'object' },
+ ],
+ };
+
+ it('parses the real reference rows and PRESERVES every member', () => {
+ const result = FindReferencesToMetaResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('the honest-empty answer parses — {references: []} is the declared answer on kernels without reference tracking', () => {
+ expect(FindReferencesToMetaResponseSchema.safeParse({ references: [] }).success).toBe(true);
+ });
+});
+
+describe('RollbackMetaItemResponseSchema declares the item-rollback body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `rollbackMetaItem` return. */
+ const realResponse = {
+ success: true,
+ version: 'W/"7"',
+ seq: 7,
+ restoredFromVersion: 4,
+ message: 'Rolled back to version 4',
+ };
+
+ it('parses the real rollback receipt and PRESERVES every member', () => {
+ const result = RollbackMetaItemResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('`message` is genuinely optional — the messageless receipt parses', () => {
+ const { message: _omitted, ...rest } = realResponse;
+ expect(RollbackMetaItemResponseSchema.safeParse(rest).success).toBe(true);
+ });
+});
+
+describe('DiffMetaItemResponseSchema declares the structural-diff body (#12038)', () => {
+ /** A verbatim-shaped capture of a real `diffMetaItem` return. */
+ const realResponse = {
+ type: 'object',
+ name: 'customer',
+ fromVersion: 2,
+ toVersion: 5,
+ added: [{ path: 'fields.priority', value: { type: 'select', options: ['low', 'high'] } }],
+ removed: [{ path: 'fields.legacy_code', value: { type: 'text' } }],
+ changed: [{ path: 'label', from: 'Customer', to: 'Account' }],
+ };
+
+ it('parses the real diff shape and PRESERVES every member', () => {
+ const result = DiffMetaItemResponseSchema.safeParse(realResponse);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(realResponse);
+ });
+
+ it('null version sides are declared — a first-version diff parses', () => {
+ const firstVersion = { ...realResponse, fromVersion: null, toVersion: null };
+ expect(DiffMetaItemResponseSchema.safeParse(firstVersion).success).toBe(true);
+ });
+});
+
+describe('GetPublishedMetaItemResponseSchema stays opaque by ruling (#12038 1C)', () => {
+ it('accepts an arbitrary metadata item body and PRESERVES it — no shape is imposed', () => {
+ const itemBody = { name: 'all_leads', type: 'grid', object: 'lead', columns: [{ field: 'name' }] };
+ const result = GetPublishedMetaItemResponseSchema.safeParse(itemBody);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(itemBody);
+ });
+
+ it('imposes nothing on the body — opacity is the ruled contract, not an accident', () => {
+ // The ruling (1C) forbids a discriminated union frozen against today's
+ // type registry; `unknown` accepts every body, including non-objects.
+ expect(GetPublishedMetaItemResponseSchema.safeParse('raw-string-body').success).toBe(true);
+ expect(GetPublishedMetaItemResponseSchema.safeParse(null).success).toBe(true);
+ });
+});
diff --git a/packages/spec/src/api/protocol.zod.ts b/packages/spec/src/api/protocol.zod.ts
index 68f0a65983..7a1b784bc4 100644
--- a/packages/spec/src/api/protocol.zod.ts
+++ b/packages/spec/src/api/protocol.zod.ts
@@ -1300,6 +1300,182 @@ export const AuditMetaItemResponseSchema = lazySchema(() => z.object({
),
}));
+// ==========================================
+// Meta history / diagnostics family (#12038)
+// ==========================================
+//
+// The response contracts for the previously-unbound `meta.*` history /
+// diagnostics routes, ruled on 2026-08-27 (#12038, 1C · 2C · 3A · 4A · 5A).
+// Every schema below is a DESCRIBE-ONLY TRANSCRIPTION of the return type its
+// producer in `@objectstack/metadata-protocol` already declares inline —
+// authoring one changes no wire byte. The route-ledger rows that name these
+// schemas state which surface's envelope they describe (the dispatcher wraps
+// `{ success, data }`; the REST server answers the payload bare — #12038
+// survey §8.1); each schema here is the PAYLOAD, envelope-free, so one
+// declaration is true on both surfaces.
+//
+// `meta.migrateStored` (`POST /meta/_migrate-stored`) is DELIBERATELY ABSENT
+// (#12038 ruling 2C): its only named type, `StoredMigrationReport`, lives in
+// `@objectstack/metadata-protocol` (`stored-migration.ts`), which
+// `packages/client` cannot reach (its deps are core + spec only). A second
+// declaration here would drift against the CLI that renders the same report,
+// and moving the type is a cross-package architecture change with zero
+// consumer pull. The route stays unbound, documented at its two ledger rows
+// and in the SDK annotation.
+
+/**
+ * `GET /meta/:type/:name/published` — the published body of a metadata item
+ * (ADR-0033).
+ *
+ * DELIBERATELY OPAQUE (#12038 ruling 1C). The route answers an arbitrary
+ * metadata item body — the union over every registered metadata type — and
+ * the layered producer feeding it declares that very field `z.unknown()` on
+ * purpose (`GetMetaItemLayeredResponseSchema.overlay`, "LAYER 2 — the stored
+ * customization row ALONE"). A discriminated union here would freeze this
+ * contract against today's type registry; the body stays opaque by ruling,
+ * not by omission. Two producers serve one route (the `state:'active'`
+ * overlay row via `getMetaItemLayered`, else the legacy code/package
+ * registry's `getPublished`) — opacity is also what keeps that fallback an
+ * implementation detail rather than a declared union.
+ */
+export const GetPublishedMetaItemResponseSchema = lazySchema(() => z.unknown().describe(
+ 'The published metadata item body, opaque by ruling (#12038 1C). Shape is '
+ + 'the item\'s own metadata-type schema, resolved at read time — never '
+ + 'frozen into this contract.',
+));
+
+/**
+ * `GET /meta/_drafts` — pending drafts (ADR-0033): metadata authored but not
+ * yet published, which the active-only item lists hide.
+ *
+ * Transcribed from `listDrafts`'s declared return
+ * (`@objectstack/metadata-protocol` `protocol.ts`).
+ */
+export const ListDraftsResponseSchema = lazySchema(() => z.object({
+ drafts: z.array(z.object({
+ type: z.string().describe('Metadata type name (canonical singular).'),
+ name: z.string().describe('Item name.'),
+ organizationId: z.string().nullable().describe(
+ 'Owning organization of the draft row, `null` for an environment-wide draft.',
+ ),
+ packageId: z.string().nullable().describe(
+ 'Package the draft is bound to, `null` for a package-less draft.',
+ ),
+ updatedAt: z.string().nullable().describe(
+ 'Last-touch timestamp of the draft row (ISO-8601 string), `null` when '
+ + 'the row recorded none.',
+ ),
+ updatedBy: z.string().nullable().describe(
+ 'Who last touched the draft, `null` when the row recorded none.',
+ ),
+ })).describe('Every pending draft visible to the caller, one row per item.'),
+}));
+
+/**
+ * `GET /meta/diagnostics` — the cross-type spec-validation sweep: every
+ * metadata entry that fails its registered Zod schema. Powers governance
+ * dashboards and doctor-style checks.
+ *
+ * Transcribed from `getMetaDiagnostics`'s declared return
+ * (`@objectstack/metadata-protocol` `protocol.ts`). `diagnostics` is the
+ * canonical `MetadataValidationResultSchema` from `@objectstack/spec/kernel`
+ * — the same type the save path's 422 and the read decorators speak, and the
+ * shape objectui's own `MetadataDiagnosticsSummary` independently re-declared
+ * field-for-field (#12038 survey §3d).
+ */
+export const GetMetaDiagnosticsResponseSchema = lazySchema(() => z.object({
+ entries: z.array(z.object({
+ type: z.string().describe('Metadata type of the failing item.'),
+ name: z.string().describe('Name of the failing item.'),
+ diagnostics: MetadataValidationResultSchema.describe(
+ 'The spec-validation verdict for the item — the same '
+ + '`MetadataValidationResult` the write path answers.',
+ ),
+ })).describe('One entry per item that failed validation (after filters).'),
+ total: z.number().describe('Number of entries in this answer.'),
+ scannedTypes: z.number().describe('How many metadata types the sweep visited.'),
+ scannedItems: z.number().describe('How many items the sweep visited.'),
+ stats: z.record(z.string(), z.object({
+ count: z.number().describe('Items of this type present.'),
+ locked: z.number().describe('Items of this type currently lock-protected.'),
+ packages: z.array(z.string()).describe('Packages contributing items of this type.'),
+ })).describe(
+ 'Per-type aggregate stats, keyed by metadata type — computed in the same '
+ + 'sweep so a directory page renders tile counts and a package filter in '
+ + 'one round-trip.',
+ ),
+}));
+
+/**
+ * `GET /meta/:type/:name/references` — reverse references: metadata items
+ * that reference the addressed item. `{ references: [] }` on kernels without
+ * reference tracking.
+ *
+ * Transcribed from `findReferencesToMeta`'s declared return
+ * (`@objectstack/metadata-protocol` `protocol.ts`).
+ */
+export const FindReferencesToMetaResponseSchema = lazySchema(() => z.object({
+ references: z.array(z.object({
+ type: z.string().describe('Metadata type of the REFERRING item.'),
+ name: z.string().describe('Name of the referring item.'),
+ label: z.string().optional().describe('Display label of the referring item, when it has one.'),
+ path: z.string().describe('Where inside the referring item the reference sits (dot path).'),
+ kind: z.string().describe('What kind of reference this is (e.g. which key carries it).'),
+ })).describe('Every found reference to the addressed item.'),
+}));
+
+/**
+ * `POST /meta/:type/:name/rollback` — restore the body at a history version
+ * as the new live row.
+ *
+ * Transcribed from `rollbackMetaItem`'s declared return
+ * (`@objectstack/metadata-protocol` `protocol.ts`); objectui's
+ * `metadata-client.ts` docblock independently documents the same five fields
+ * (#12038 survey §3d).
+ */
+export const RollbackMetaItemResponseSchema = lazySchema(() => z.object({
+ success: z.boolean().describe('Whether the rollback landed.'),
+ version: z.string().describe(
+ 'The new live row\'s ADR-0008 optimistic-concurrency token — the same '
+ + 'carrier `saveItem` returns; pass it back as `options.ifMatch`.',
+ ),
+ seq: z.number().describe('The new live row\'s history sequence number.'),
+ restoredFromVersion: z.number().describe('Which history version was restored.'),
+ message: z.string().optional().describe('Rollback note, when one was recorded.'),
+}));
+
+/**
+ * `GET /meta/:type/:name/diff` — structural diff between two history
+ * versions (`from`/`to`; omit both for previous-vs-current).
+ *
+ * Transcribed from `diffMetaItem`'s declared return
+ * (`@objectstack/metadata-protocol` `protocol.ts`).
+ */
+export const DiffMetaItemResponseSchema = lazySchema(() => z.object({
+ type: z.string().describe('Metadata type of the diffed item.'),
+ name: z.string().describe('Name of the diffed item.'),
+ fromVersion: z.number().nullable().describe(
+ 'The older side\'s history version, `null` when that side is absent '
+ + '(e.g. the item had no earlier version).',
+ ),
+ toVersion: z.number().nullable().describe(
+ 'The newer side\'s history version, `null` when that side is absent.',
+ ),
+ added: z.array(z.object({
+ path: z.string().describe('Dot path of the added member.'),
+ value: z.unknown().describe('The added value.'),
+ })).describe('Members present in `to` and absent in `from`.'),
+ removed: z.array(z.object({
+ path: z.string().describe('Dot path of the removed member.'),
+ value: z.unknown().describe('The removed value.'),
+ })).describe('Members present in `from` and absent in `to`.'),
+ changed: z.array(z.object({
+ path: z.string().describe('Dot path of the changed member.'),
+ from: z.unknown().describe('The older side\'s value.'),
+ to: z.unknown().describe('The newer side\'s value.'),
+ })).describe('Members present on both sides with different values.'),
+}));
+
/**
* Get Metadata Item with Cache Request
* Get a specific metadata item with HTTP cache validation support
@@ -2562,6 +2738,25 @@ export type DeleteMetaItemRequest = z.input;
export type DeleteMetaItemResponse = z.input;
export type AuditMetaItemRequest = z.input;
export type AuditMetaItemResponse = z.input;
+/** Opaque by ruling (#12038 1C) — see {@link GetPublishedMetaItemResponseSchema}. */
+export type GetPublishedMetaItemResponse = z.input;
+/** Post-parse shape of {@link GetPublishedMetaItemResponse} — defaults applied, transforms run (ADR-0122). */
+export type GetPublishedMetaItemResponseParsed = z.infer;
+export type ListDraftsResponse = z.input;
+/** Post-parse shape of {@link ListDraftsResponse} — defaults applied, transforms run (ADR-0122). */
+export type ListDraftsResponseParsed = z.infer;
+export type GetMetaDiagnosticsResponse = z.input;
+/** Post-parse shape of {@link GetMetaDiagnosticsResponse} — defaults applied, transforms run (ADR-0122). */
+export type GetMetaDiagnosticsResponseParsed = z.infer;
+export type FindReferencesToMetaResponse = z.input;
+/** Post-parse shape of {@link FindReferencesToMetaResponse} — defaults applied, transforms run (ADR-0122). */
+export type FindReferencesToMetaResponseParsed = z.infer;
+export type RollbackMetaItemResponse = z.input;
+/** Post-parse shape of {@link RollbackMetaItemResponse} — defaults applied, transforms run (ADR-0122). */
+export type RollbackMetaItemResponseParsed = z.infer;
+export type DiffMetaItemResponse = z.input;
+/** Post-parse shape of {@link DiffMetaItemResponse} — defaults applied, transforms run (ADR-0122). */
+export type DiffMetaItemResponseParsed = z.infer;
export type GetMetaItemCachedRequest = z.input;
export type GetMetaItemCachedResponse = z.input;
/** Post-parse shape of {@link GetMetaItemCachedResponse} — defaults applied, transforms run (ADR-0122). */
diff --git a/packages/spec/src/migrations/entries/retired-defs/18.api__PackageRollbackResponse.ts b/packages/spec/src/migrations/entries/retired-defs/18.api__PackageRollbackResponse.ts
new file mode 100644
index 0000000000..30ad85f798
--- /dev/null
+++ b/packages/spec/src/migrations/entries/retired-defs/18.api__PackageRollbackResponse.ts
@@ -0,0 +1,24 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+// #12038 — `api/package-api.zod.ts` `PackageRollbackResponseSchema`, retired
+// whole together with the `PackageApiContracts.rollbackPackage` entry that
+// bound it (maintainer ruling 2026-08-27, sub-question 3A). The schema
+// declared a VERSION rollback — `{ success, restoredVersion?, message? }` —
+// while the live `POST /api/v1/packages/:packageId/rollback` route posts
+// `{ commitId }` and the dispatcher serves it with `rollbackToPackageCommit`,
+// the ADR-0067 COMMIT rollback: a wrong-operation declaration bound to the
+// exact live path, which a future sweep would have read as authoritative.
+// Zero consumers measured across objectstack/objectui/cloud (#12038 survey
+// §5.2): only its own unit test and the #11925 negative guard, both updated
+// in the retiring PR. No carrier key, no authored document, so no tombstone
+// and no D2 conversion — this table plus the D3 semantic entry
+// `package-rollback-response-retired` ARE the declaration (the #8715 route-3
+// shape). The live route's true contract is
+// `RollbackToPackageCommitResponseSchema` (`api/package-lifecycle.zod.ts`),
+// authored in the same PR AFTER this retirement per the ruling's sequencing.
+//
+// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
+// removal ships on the 17.x line (launch-window convention: accept-set
+// narrowings ride minor releases) and the prescription lives at the major
+// boundary where `migrate meta` users look (the #8586 / #8715 precedent).
+export const entry = 'api/PackageRollbackResponse';
diff --git a/packages/spec/src/migrations/entries/semantic/18.package-rollback-response-retired.ts b/packages/spec/src/migrations/entries/semantic/18.package-rollback-response-retired.ts
new file mode 100644
index 0000000000..a1db24c253
--- /dev/null
+++ b/packages/spec/src/migrations/entries/semantic/18.package-rollback-response-retired.ts
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
+
+import type { SemanticMigration } from '../../types.js';
+
+export const entry: SemanticMigration = {
+ id: 'package-rollback-response-retired',
+ surface:
+ 'api.packageRollbackResponse (`PackageRollbackResponseSchema` in '
+ + 'api/package-api.zod.ts — 1 def, 3 exported names: '
+ + '`PackageRollbackResponseSchema`, `PackageRollbackResponse`, '
+ + '`PackageRollbackResponseParsed` — plus the `PackageApiContracts.'
+ + 'rollbackPackage` contract-map entry that bound it to '
+ + '`POST /api/v1/packages/:packageId/rollback`)',
+ replacement:
+ '`RollbackToPackageCommitResponseSchema` (api/package-lifecycle.zod.ts) — '
+ + 'the transcription of what the live route actually answers: the '
+ + 'dispatcher routes `POST /packages/:id/rollback` (body `{ commitId }`) '
+ + 'to `rollbackToPackageCommit`, the ADR-0067 COMMIT rollback, whose '
+ + 'declared return is `{ success, revertedCommits: string[], '
+ + 'failed: Array<{ commitId, error }> }`. Consumers of the retired type '
+ + 'were reading a VERSION-rollback shape (`restoredVersion`) the route has '
+ + 'never answered; read `revertedCommits`/`failed` instead. '
+ + '`PackageRollbackRequestSchema` stays published (ruled out of the '
+ + 'retirement), bound to no route.',
+ reason:
+ 'Maintainer ruling 2026-08-27 on #12038, sub-question 3A (五问一批, '
+ + '「其他接受」). The schema declared a version rollback — '
+ + '`{ success, restoredVersion?, message? }`, matching its file header '
+ + '"Rollback a package" — while the live path it was contract-bound to '
+ + 'serves the ADR-0067 commit rollback: a different operation with a '
+ + 'different result. Binding it in the SDK would compile and be false '
+ + '(#11925 left a compile-time guard against exactly that substitution). '
+ + 'Zero consumers measured across objectstack, objectui and cloud '
+ + '(#12038 survey §5.2, re-verified at the retiring PR\'s base): only its '
+ + 'own unit test and the #11925 negative guard. A published declaration '
+ + 'that outran the implementation is the #3877 hazard realised in the '
+ + 'opposite direction — not "no declaration" but a WRONG one — and it is '
+ + 'retired BEFORE the true schema is authored so no window exists in '
+ + 'which both claims are published.',
+ acceptanceCriteria:
+ 'No code imports `PackageRollbackResponseSchema`, '
+ + '`PackageRollbackResponse` or `PackageRollbackResponseParsed` from '
+ + '`@objectstack/spec` or `@objectstack/spec/api` — every one is TS2305 '
+ + 'after upgrade (pinned by runtime namespace probes in '
+ + 'api/package-api.test.ts). `PackageApiContracts` carries no entry whose '
+ + 'path is `/api/v1/packages/:packageId/rollback` (same pin). No metadata '
+ + 'document needs editing: the schema was reachable from no metadata-type '
+ + 'binding, stack collection or /meta door. ⚠️ Runtime behaviour is '
+ + 'deliberately UNCHANGED: nothing ever registered routes or generated '
+ + 'SDKs from the contract entry, and the route\'s handler emits the same '
+ + 'bytes before and after — the retirement removes a false claim, not '
+ + 'behaviour.',
+};
diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts
index f37f4c7fb5..e87d628f10 100644
--- a/packages/spec/src/migrations/registry.ts
+++ b/packages/spec/src/migrations/registry.ts
@@ -6540,6 +6540,55 @@ const step18: MigrationStep = {
+ 'drift window carrying `indexes[].where` is rejected with the database-layer prescription '
+ 'rather than saved with the key silently dropped.',
},
+ {
+ id: 'package-rollback-response-retired',
+ surface:
+ 'api.packageRollbackResponse (`PackageRollbackResponseSchema` in '
+ + 'api/package-api.zod.ts — 1 def, 3 exported names: '
+ + '`PackageRollbackResponseSchema`, `PackageRollbackResponse`, '
+ + '`PackageRollbackResponseParsed` — plus the `PackageApiContracts.'
+ + 'rollbackPackage` contract-map entry that bound it to '
+ + '`POST /api/v1/packages/:packageId/rollback`)',
+ replacement:
+ '`RollbackToPackageCommitResponseSchema` (api/package-lifecycle.zod.ts) — '
+ + 'the transcription of what the live route actually answers: the '
+ + 'dispatcher routes `POST /packages/:id/rollback` (body `{ commitId }`) '
+ + 'to `rollbackToPackageCommit`, the ADR-0067 COMMIT rollback, whose '
+ + 'declared return is `{ success, revertedCommits: string[], '
+ + 'failed: Array<{ commitId, error }> }`. Consumers of the retired type '
+ + 'were reading a VERSION-rollback shape (`restoredVersion`) the route has '
+ + 'never answered; read `revertedCommits`/`failed` instead. '
+ + '`PackageRollbackRequestSchema` stays published (ruled out of the '
+ + 'retirement), bound to no route.',
+ reason:
+ 'Maintainer ruling 2026-08-27 on #12038, sub-question 3A (五问一批, '
+ + '「其他接受」). The schema declared a version rollback — '
+ + '`{ success, restoredVersion?, message? }`, matching its file header '
+ + '"Rollback a package" — while the live path it was contract-bound to '
+ + 'serves the ADR-0067 commit rollback: a different operation with a '
+ + 'different result. Binding it in the SDK would compile and be false '
+ + '(#11925 left a compile-time guard against exactly that substitution). '
+ + 'Zero consumers measured across objectstack, objectui and cloud '
+ + '(#12038 survey §5.2, re-verified at the retiring PR\'s base): only its '
+ + 'own unit test and the #11925 negative guard. A published declaration '
+ + 'that outran the implementation is the #3877 hazard realised in the '
+ + 'opposite direction — not "no declaration" but a WRONG one — and it is '
+ + 'retired BEFORE the true schema is authored so no window exists in '
+ + 'which both claims are published.',
+ acceptanceCriteria:
+ 'No code imports `PackageRollbackResponseSchema`, '
+ + '`PackageRollbackResponse` or `PackageRollbackResponseParsed` from '
+ + '`@objectstack/spec` or `@objectstack/spec/api` — every one is TS2305 '
+ + 'after upgrade (pinned by runtime namespace probes in '
+ + 'api/package-api.test.ts). `PackageApiContracts` carries no entry whose '
+ + 'path is `/api/v1/packages/:packageId/rollback` (same pin). No metadata '
+ + 'document needs editing: the schema was reachable from no metadata-type '
+ + 'binding, stack collection or /meta door. ⚠️ Runtime behaviour is '
+ + 'deliberately UNCHANGED: nothing ever registered routes or generated '
+ + 'SDKs from the contract entry, and the route\'s handler emits the same '
+ + 'bytes before and after — the retirement removes a false claim, not '
+ + 'behaviour.',
+ },
{
id: 'plugin-auto-restart-never-reinitialised',
surface:
@@ -8700,6 +8749,28 @@ export const RETIRED_DEFS_BY_MAJOR: Readonly>
// entry id by `gen:migration-registry` (#7297). Add an entry by adding a
// FILE — never by editing between the markers, which is generated.
//
+ // #12038 — `api/package-api.zod.ts` `PackageRollbackResponseSchema`, retired
+ // whole together with the `PackageApiContracts.rollbackPackage` entry that
+ // bound it (maintainer ruling 2026-08-27, sub-question 3A). The schema
+ // declared a VERSION rollback — `{ success, restoredVersion?, message? }` —
+ // while the live `POST /api/v1/packages/:packageId/rollback` route posts
+ // `{ commitId }` and the dispatcher serves it with `rollbackToPackageCommit`,
+ // the ADR-0067 COMMIT rollback: a wrong-operation declaration bound to the
+ // exact live path, which a future sweep would have read as authoritative.
+ // Zero consumers measured across objectstack/objectui/cloud (#12038 survey
+ // §5.2): only its own unit test and the #11925 negative guard, both updated
+ // in the retiring PR. No carrier key, no authored document, so no tombstone
+ // and no D2 conversion — this table plus the D3 semantic entry
+ // `package-rollback-response-retired` ARE the declaration (the #8715 route-3
+ // shape). The live route's true contract is
+ // `RollbackToPackageCommitResponseSchema` (`api/package-lifecycle.zod.ts`),
+ // authored in the same PR AFTER this retirement per the ruling's sequencing.
+ //
+ // Registered under 18, not 17: v17.0.0 was cut before this landed, so the
+ // removal ships on the 17.x line (launch-window convention: accept-set
+ // narrowings ride minor releases) and the prescription lives at the major
+ // boundary where `migrate meta` users look (the #8586 / #8715 precedent).
+ 'api/PackageRollbackResponse',
// #8715 — identity/identity.zod.ts `ApiKeySchema`, retired whole (ADR-0049
// enforce-or-remove; maintainer ruling 2026-08-15, disposition B: delete).
// The schema documented better-auth's `apiKey` PLUGIN shape — a plugin this
diff --git a/packages/spec/src/system/book.test.ts b/packages/spec/src/system/book.test.ts
index 0dec346ec9..f78d9227d0 100644
--- a/packages/spec/src/system/book.test.ts
+++ b/packages/spec/src/system/book.test.ts
@@ -1,6 +1,7 @@
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, expectTypeOf } from 'vitest';
+import type { z } from 'zod';
import {
BookSchema,
resolveBookTree,
@@ -10,8 +11,14 @@ import {
resolveBookClaimedDocs,
resolveDocAudiences,
docAudienceAllows,
+ ResolvedEntrySchema,
+ ResolvedGroupSchema,
+ ResolvedBookSchema,
type Book,
type ResolverDoc,
+ type ResolvedEntry,
+ type ResolvedGroup,
+ type ResolvedBook,
} from './book.zod';
import { DocSchema } from './doc.zod';
@@ -322,3 +329,47 @@ describe('retired book translation maps (#4667)', () => {
})).not.toThrow();
});
});
+
+// ==========================================
+// book-tree response contract (#12038)
+// ==========================================
+
+describe('ResolvedBookSchema is the book-tree response contract (#12038)', () => {
+ // The conformance suite for the `GET /meta/book/:name/tree` ledger rows
+ // (#3877's no-row-without-conformance rule). Stronger than the handwritten
+ // captures its meta.* siblings use: `resolveBookTree()` is pure and lives in
+ // this file's module, so the suite drives the REAL producer and parses what
+ // it actually returns.
+ const spine: Book = {
+ name: 'crm_guide',
+ label: 'CRM Guide',
+ groups: [
+ { key: 'basics', label: 'Basics', include: 'crm_*' },
+ { key: 'links', label: 'Links', pages: ['---', { href: 'https://example.com', label: 'Site', badge: 'new' }] },
+ ],
+ };
+ const resolverDocs: ResolverDoc[] = [
+ { name: 'crm_intro', label: 'Intro', description: 'Start here', order: 1 },
+ { name: 'crm_setup', label: 'Setup', order: 2 },
+ ];
+
+ it('parses the real resolver output and PRESERVES it', () => {
+ const tree = resolveBookTree(BookSchema.parse(spine) as Book, resolverDocs);
+ const result = ResolvedBookSchema.safeParse(tree);
+ expect(result.success).toBe(true);
+ if (result.success) expect(result.data).toEqual(JSON.parse(JSON.stringify(tree)));
+ });
+
+ it('the honest-empty tree parses — a book whose rules match nothing is a declared, legal body', () => {
+ const empty = { name: 'crm_guide', groups: [] };
+ expect(ResolvedBookSchema.safeParse(empty).success).toBe(true);
+ });
+
+ it('each schema stays type-identical to the interface the resolver is typed by', () => {
+ // The interfaces remain the compile-time source `resolveBookTree` is typed
+ // by; these pins are what keeps the Zod transcription from drifting.
+ expectTypeOf>().toEqualTypeOf();
+ expectTypeOf>().toEqualTypeOf();
+ expectTypeOf>().toEqualTypeOf();
+ });
+});
diff --git a/packages/spec/src/system/book.zod.ts b/packages/spec/src/system/book.zod.ts
index 869869eadd..e52eb8241a 100644
--- a/packages/spec/src/system/book.zod.ts
+++ b/packages/spec/src/system/book.zod.ts
@@ -229,6 +229,40 @@ export interface ResolvedBook {
groups: ResolvedGroup[];
}
+/**
+ * The rendered-tree shapes above, as Zod — the response contract of
+ * `GET /meta/book/:name/tree`, which answers `resolveBookTree()`'s
+ * `ResolvedBook` verbatim (#12038, a describe-only transcription: the
+ * interfaces above stay the compile-time source `resolveBookTree` is typed
+ * by, and the `book-tree response contract` block in `book.test.ts` pins each
+ * schema type-identical to its interface so the two spellings cannot drift).
+ * Re-exported into `@objectstack/spec/api` (ruling 5A) so the route-ledger
+ * resolver can name it.
+ */
+export const ResolvedEntrySchema = lazySchema(() => z.object({
+ doc: z.string().optional().describe('Doc name, or undefined for an external link / separator.'),
+ href: z.string().optional().describe('External link target, when the entry is a link.'),
+ label: z.string().optional().describe('Display label, when one resolved.'),
+ description: z.string().optional().describe('Doc description, when one resolved.'),
+ badge: z.string().optional().describe('Badge text (e.g. "beta"), when declared.'),
+ icon: z.string().optional().describe('Icon name, when declared.'),
+ separator: z.boolean().optional().describe('True for a `---` separator node.'),
+}));
+
+/** One resolved group (section) of the tree — see {@link ResolvedEntrySchema}. */
+export const ResolvedGroupSchema = lazySchema(() => z.object({
+ key: z.string().describe('The group\'s key (from the spine, or `uncategorized`).'),
+ label: z.string().describe('The group\'s display label.'),
+ entries: z.array(ResolvedEntrySchema).describe('The group\'s resolved entries, in render order.'),
+}));
+
+/** The whole resolved tree `GET /meta/book/:name/tree` answers. */
+export const ResolvedBookSchema = lazySchema(() => z.object({
+ name: z.string().describe('The book\'s machine name.'),
+ label: z.string().optional().describe('The book\'s display label, when it declares one.'),
+ groups: z.array(ResolvedGroupSchema).describe('The resolved groups, in render order.'),
+}));
+
const UNCATEGORIZED_KEY = 'uncategorized';
/** Compile a `*`-glob over doc names to a RegExp anchored on the whole name. */