diff --git a/.changeset/18576-api-assembled-entry-split.md b/.changeset/18576-api-assembled-entry-split.md
new file mode 100644
index 00000000000..78c5d6ed935
--- /dev/null
+++ b/.changeset/18576-api-assembled-entry-split.md
@@ -0,0 +1,40 @@
+---
+'@objectstack/spec': minor
+---
+
+feat(spec)!: split the assembled-stage package API declarations off `@objectstack/spec/api` into the new `@objectstack/spec/api-assembled` entry (#18576)
+
+**BREAKING** — five Package API declarations, with their types, are no longer exported from `@objectstack/spec/api`. They are exported, unchanged, from the new entry `@objectstack/spec/api-assembled`.
+
+A `major`-class change — an existing import path stops resolving for these names — recorded as `minor` under the launch-window convention. Maintainer ruling on #18576, batch #145 item 1, letter B, 「同意,其他也同意」.
+
+**Why.** These five declarations embed the ASSEMBLED package body, which reaches the whole metadata vocabulary and, behind it, the datasource declaration and the driver-config validators. While they were declared inside `@objectstack/spec/api`, that tree was part of every bundle of the entry, and the entry ships as one self-contained bundle that a consumer's tree-shaking can recover little of. A browser module that imports two string constants from `@objectstack/spec/api` paid for all of it. Measured on the splitting PR (esbuild 0.28.2, `platform: browser`, conditions `browser` + `import`, minified, gzip -9), for objectui's `@object-ui/core` `column-sortability.ts`, which imports only those two constants: **311,124 → 166,529 bytes gzipped (−46.5%)**. The `./api` entry bundle itself goes from 612,813 to 469,795 bytes gzipped, and its module graph no longer reaches `stack.zod`, the datasource declaration or any driver-config module. `./api` also no longer needs a `browser` export condition, since nothing in its graph links the server-only pg URL grammar any more; the condition moves to `./api-assembled`.
+
+### FROM → TO
+
+| removed from `@objectstack/spec/api` | import instead from |
+| --- | --- |
+| `AssembledInstalledPackageSchema`, `AssembledInstalledPackage`, `AssembledInstalledPackageParsed` | `@objectstack/spec/api-assembled` |
+| `InstalledPackageAtEitherStageSchema`, `InstalledPackageAtEitherStage`, `InstalledPackageAtEitherStageParsed` | `@objectstack/spec/api-assembled` |
+| `ListInstalledPackagesResponseSchema`, `ListInstalledPackagesResponse`, `ListInstalledPackagesResponseParsed` | `@objectstack/spec/api-assembled` |
+| `GetInstalledPackageResponseSchema`, `GetInstalledPackageResponse`, `GetInstalledPackageResponseParsed` | `@objectstack/spec/api-assembled` |
+| `PackageApiContracts` | `@objectstack/spec/api-assembled` |
+
+**The one-line fix: change the import path.**
+
+```ts
+// before
+import { ListInstalledPackagesResponseSchema } from '@objectstack/spec/api';
+// after
+import { ListInstalledPackagesResponseSchema } from '@objectstack/spec/api-assembled';
+```
+
+The compiler finds every site: `TS2305` ("Module '"@objectstack/spec/api"' has no exported member …"), or `TS2724` with a did-you-mean when a similarly named export exists — measured on the splitting PR, `ListInstalledPackagesResponseSchema` from `/api` answers `TS2724 … Did you mean 'InstallPackageResponseSchema'?`, which is NOT the name you want. Nothing else changes: every schema parses and refuses exactly what it did, `PackageApiContracts` keeps its four entries, and the JSON Schema ids are the same (`json-schema/api/AssembledInstalledPackage.json` and its three siblings are still published under `api/`, and still documented in the API reference). Every other Package API declaration — the two read doors' request schemas, the install / uninstall / upgrade / rollback shapes and `PackageApiErrorCode` — stays on `@objectstack/spec/api`. If you only use those, or any other `/api` contract, you need to do nothing.
+
+⚠️ **Out-of-repo consumers are NOT MEASURED beyond objectui.** Inside this repository the moved names had four importers (the client's type import, one runtime conformance test, the client's return-type pins and the spec's own unit test), all moved in the same PR. objectui at the pinned `.objectui-sha` imports none of the moved names from anywhere; its six browser-shipped files that import `@objectstack/spec/api` keep resolving every name they use, from `/api` itself. `@object-ui/types` re-exports `@objectstack/spec/api` as a type-only `API` namespace, which loses the moved names with this release; objectui itself references none of them through it. The `cloud` repository was not measured.
+
+The ADR-0087 D3 semantic entry `api-assembled-entry-split` carries the judgement: an import path is TypeScript source, not metadata, so there is no source a D2 conversion could rewrite.
+
+Clause-②: yes (narrowing)
+
+
diff --git a/.changeset/18576-client-api-assembled-type-import.md b/.changeset/18576-client-api-assembled-type-import.md
new file mode 100644
index 00000000000..77c5417dadd
--- /dev/null
+++ b/.changeset/18576-client-api-assembled-type-import.md
@@ -0,0 +1,7 @@
+---
+'@objectstack/client': patch
+---
+
+fix(client): take `InstalledPackageAtEitherStage` from `@objectstack/spec/api-assembled` (#18576)
+
+`@objectstack/spec` moved the declarations that embed the assembled package body — `InstalledPackageAtEitherStage` among them — off `@objectstack/spec/api` into the new `@objectstack/spec/api-assembled` entry. The client's `packages.list` / `packages.get` return types (and their scoped twins) name that type, so the published declarations now import it from the new entry. The return types are the same type as before; nothing a caller writes changes. It is a type-only import, erased from the client's bundle.
diff --git a/content/docs/deployment/troubleshooting.mdx b/content/docs/deployment/troubleshooting.mdx
index 2055cfb0753..7f846d3f3c6 100644
--- a/content/docs/deployment/troubleshooting.mdx
+++ b/content/docs/deployment/troubleshooting.mdx
@@ -344,7 +344,7 @@ import { FieldSchema } from '@objectstack/spec/data';
import { ErrorResponseSchema } from '@objectstack/spec/api';
```
-Available subpaths (the `./*` entries of the package's `exports` map, in its order): `data`, `system`, `kernel`, `ai`, `automation`, `api`, `ui`, `contracts`, `integration`, `security`, `studio`, `marketplace`, `qa`, `identity`, `shared`, `meta-spelling`.
+Available subpaths (the `./*` entries of the package's `exports` map, in its order): `data`, `system`, `kernel`, `ai`, `automation`, `api`, `api-assembled`, `ui`, `contracts`, `integration`, `security`, `studio`, `marketplace`, `qa`, `identity`, `shared`, `meta-spelling`.
---
diff --git a/content/docs/getting-started/quick-reference.mdx b/content/docs/getting-started/quick-reference.mdx
index 301a077eedb..45e7a4cc539 100644
--- a/content/docs/getting-started/quick-reference.mdx
+++ b/content/docs/getting-started/quick-reference.mdx
@@ -126,7 +126,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 31 schemas)
+## API Protocol (17 of 32 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 a5a30772f05..6b4699c8169 100644
--- a/content/docs/references/api/index.mdx
+++ b/content/docs/references/api/index.mdx
@@ -27,6 +27,7 @@ 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 69b8a4dd37b..3a31ec00c46 100644
--- a/content/docs/references/api/meta.json
+++ b/content/docs/references/api/meta.json
@@ -34,6 +34,7 @@
"---More---",
"error-code-ledger",
"misc",
+ "package-api-assembled",
"package-lifecycle",
"sortability"
]
diff --git a/content/docs/references/api/package-api-assembled.mdx b/content/docs/references/api/package-api-assembled.mdx
new file mode 100644
index 00000000000..e0e7f45edfb
--- /dev/null
+++ b/content/docs/references/api/package-api-assembled.mdx
@@ -0,0 +1,445 @@
+---
+title: Package Api Assembled
+description: Package Api Assembled protocol schemas
+---
+
+{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
+
+The Package API declarations that carry the ASSEMBLED package body.
+
+Published from `@objectstack/spec/api-assembled`, never from
+`@objectstack/spec/api`. Everything here is part of the Package API
+(`/api/v1/packages`, `./package-api.zod.ts`); what sets these five apart is
+that each one embeds the assembled package body, `RecordStagePackageBodySchema`
+from `../stack.zod` — or, for the route map, names a schema that does:
+
+- `AssembledInstalledPackageSchema` — the installed row at the assembled stage;
+- `InstalledPackageAtEitherStageSchema` — the union the read doors serve;
+- `ListInstalledPackagesResponseSchema` / `GetInstalledPackageResponseSchema`
+ — the two read responses, bound to that union;
+- `PackageApiContracts` — the route map, which names both read responses.
+
+## Why they have their own entry
+
+The assembled body is the WHOLE metadata vocabulary: `../stack.zod` reaches
+every collection schema, the datasource declaration and, behind it, the
+driver-config validators and the server-only pg URL grammar. Declared inside
+`@objectstack/spec/api` (#17517), that tree became part of every bundle of the
+entry — and a browser module that imported two string constants from
+`./sortability.zod` paid for all of it, roughly doubling its gzipped bundle,
+because the entry ships as one self-contained bundle and little of that tree
+can be dropped by a consumer's tree-shaking. The maintainer ruling on #18576
+(letter B) removed the cost rather than watching it: the browser-facing
+`./api` no longer carries these declarations, and this entry does.
+
+⛔ Their MEANING did not change with the move — same schemas, same refusals,
+same JSON Schema ids (`api/...`, still published under `json-schema/api/`,
+because they are API-protocol declarations; only the import path moved).
+
+⛔ Only a declaration that genuinely needs the assembled body belongs here.
+Everything else in the Package API stays in `./package-api.zod.ts`, which
+`@objectstack/spec/api` publishes; `./api-entry-graph.pin.test.ts` pins that
+`./api` reaches neither `../stack.zod` nor the datasource declaration.
+
+
+**Source:** `packages/spec/src/api/package-api-assembled.zod.ts`
+
+
+## TypeScript Usage
+
+```typescript
+import { AssembledInstalledPackageSchema, GetInstalledPackageResponseSchema, InstalledPackageAtEitherStageSchema, ListInstalledPackagesResponseSchema } from '@objectstack/spec/api-assembled';
+import type { AssembledInstalledPackage, GetInstalledPackageResponse, InstalledPackageAtEitherStage, ListInstalledPackagesResponse } from '@objectstack/spec/api-assembled';
+
+// Validate data
+const result = AssembledInstalledPackageSchema.parse(data);
+```
+
+---
+
+## AssembledInstalledPackage
+
+Installed package row whose manifest is the assembled package body
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
+| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
+| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
+| **installedAt** | `string` | optional | Installation timestamp |
+| **updatedAt** | `string` | optional | Last update timestamp |
+| **installedVersion** | `string` | optional | Currently installed version for quick access |
+| **previousVersion** | `string` | optional | Version before the last upgrade |
+| **statusChangedAt** | `string` | optional | Status change timestamp |
+| **errorMessage** | `string` | optional | Error message when status is error |
+| **settings** | `Record` | optional | User-provided configuration settings |
+| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
+| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
+
+### Nested Shape: `AssembledInstalledPackage.manifest`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
+| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
+| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
+| **version** | `string` | ✅ | Package version (semantic versioning) |
+| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
+| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
+| **name** | `string` | ✅ | Human-readable package name |
+| **description** | `string` | optional | Package description |
+| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |
+| **objects** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }[]` | optional | Business Objects definition (owned by this package) |
+| **datasources** | `{ name: string; label?: string; driver: string; config: Record; … }[]` | optional | External Data Connections |
+| **dependencies** | `Record` | optional | Package dependencies |
+| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
+| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
+| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
+| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
+| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
+| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
+| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
+| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
+| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
+| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
+| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
+| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
+| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
+| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
+| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
+| **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles |
+| **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
+| **apps** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Applications |
+| **views** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }[]` | optional | List Views |
+| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |
+| **pages** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Custom Pages |
+| **dashboards** | `{ name: string; label: string \| Record; description?: string \| Record; header?: object; … }[]` | optional | Dashboards |
+| **reports** | `{ name: string; label: string \| Record; description?: string \| Record; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
+| **datasets** | `{ name: string; label: string \| Record; description?: string \| Record; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
+| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
+| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
+| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
+| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
+| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
+| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
+| **positions** | `{ name: string; label: string; description?: string; delegatable?: boolean; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
+| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
+| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
+| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
+| **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) |
+| **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) |
+| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them |
+| **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings |
+| **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes |
+| **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). |
+| **requires** | `string[]` | optional | Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; an unknown token is a defineStack error, declared-but-missing ⇒ fail-fast at startup) |
+| **tiers** | `string[]` | optional | Plugin tier presets to enable; overrides --preset |
+
+### Nested Shape: `AssembledInstalledPackage.upgradeHistory[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **fromVersion** | `string` | ✅ | Version before upgrade |
+| **toVersion** | `string` | ✅ | Version after upgrade |
+| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
+| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
+| **migrationLog** | `string[]` | optional | Migration step logs |
+
+
+---
+
+## GetInstalledPackageResponse
+
+Get installed 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?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
+| **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more` | ✅ | Installed package details |
+
+### Nested Shape: `GetInstalledPackageResponse.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) |
+| **message** | `string` | ✅ | Readable error message |
+| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
+| **refusal** | `true` | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose `message` is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose `message` is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — `true` is the only value. |
+| **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: `GetInstalledPackageResponse.meta`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **timestamp** | `string` | ✅ | |
+| **duration** | `integer` | optional | Server-side processing duration in milliseconds |
+| **requestId** | `string` | optional | |
+| **traceId** | `string` | optional | |
+
+### Nested Shape: `GetInstalledPackageResponse.data[option 1]`
+
+Installed package with runtime lifecycle state
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
+| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
+| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
+| **installedAt** | `string` | optional | Installation timestamp |
+| **updatedAt** | `string` | optional | Last update timestamp |
+| **installedVersion** | `string` | optional | Currently installed version for quick access |
+| **previousVersion** | `string` | optional | Version before the last upgrade |
+| **statusChangedAt** | `string` | optional | Status change timestamp |
+| **errorMessage** | `string` | optional | Error message when status is error |
+| **settings** | `Record` | optional | User-provided configuration settings |
+| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
+| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
+
+### Nested Shape: `GetInstalledPackageResponse.data[option 2]`
+
+Installed package row whose manifest is the assembled package body
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
+| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
+| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
+| **installedAt** | `string` | optional | Installation timestamp |
+| **updatedAt** | `string` | optional | Last update timestamp |
+| **installedVersion** | `string` | optional | Currently installed version for quick access |
+| **previousVersion** | `string` | optional | Version before the last upgrade |
+| **statusChangedAt** | `string` | optional | Status change timestamp |
+| **errorMessage** | `string` | optional | Error message when status is error |
+| **settings** | `Record` | optional | User-provided configuration settings |
+| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
+| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
+
+
+---
+
+## InstalledPackageAtEitherStage
+
+Installed package row at whichever manifest stage it was installed at
+
+### Union Options
+
+This schema accepts one of the following structures:
+
+#### Option 1
+
+Installed package with runtime lifecycle state
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
+| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
+| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
+| **installedAt** | `string` | optional | Installation timestamp |
+| **updatedAt** | `string` | optional | Last update timestamp |
+| **installedVersion** | `string` | optional | Currently installed version for quick access |
+| **previousVersion** | `string` | optional | Version before the last upgrade |
+| **statusChangedAt** | `string` | optional | Status change timestamp |
+| **errorMessage** | `string` | optional | Error message when status is error |
+| **settings** | `Record` | optional | User-provided configuration settings |
+| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
+| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
+
+### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
+| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
+| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
+| **version** | `string` | ✅ | Package version (semantic versioning) |
+| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
+| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
+| **name** | `string` | ✅ | Human-readable package name |
+| **description** | `string` | optional | Package description |
+| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: legacy string[] or structured plugin block (ADR-0025 §3.2) — at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
+| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files |
+| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
+| **dependencies** | `Record` | optional | Package dependencies |
+| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
+| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
+| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
+| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
+| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
+| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
+| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
+| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
+| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
+| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
+| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
+| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
+| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
+
+### Nested Shape: `InstalledPackageAtEitherStage[option 1].upgradeHistory[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **fromVersion** | `string` | ✅ | Version before upgrade |
+| **toVersion** | `string` | ✅ | Version after upgrade |
+| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
+| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
+| **migrationLog** | `string[]` | optional | Migration step logs |
+
+---
+
+#### Option 2
+
+Installed package row whose manifest is the assembled package body
+
+### Properties
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
+| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
+| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
+| **installedAt** | `string` | optional | Installation timestamp |
+| **updatedAt** | `string` | optional | Last update timestamp |
+| **installedVersion** | `string` | optional | Currently installed version for quick access |
+| **previousVersion** | `string` | optional | Version before the last upgrade |
+| **statusChangedAt** | `string` | optional | Status change timestamp |
+| **errorMessage** | `string` | optional | Error message when status is error |
+| **settings** | `Record` | optional | User-provided configuration settings |
+| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
+| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
+
+### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
+| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
+| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
+| **version** | `string` | ✅ | Package version (semantic versioning) |
+| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
+| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
+| **name** | `string` | ✅ | Human-readable package name |
+| **description** | `string` | optional | Package description |
+| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |
+| **objects** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }[]` | optional | Business Objects definition (owned by this package) |
+| **datasources** | `{ name: string; label?: string; driver: string; config: Record; … }[]` | optional | External Data Connections |
+| **dependencies** | `Record` | optional | Package dependencies |
+| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
+| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
+| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
+| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
+| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
+| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
+| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
+| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
+| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
+| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
+| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
+| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
+| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
+| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
+| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
+| **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles |
+| **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
+| **apps** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Applications |
+| **views** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }[]` | optional | List Views |
+| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |
+| **pages** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Custom Pages |
+| **dashboards** | `{ name: string; label: string \| Record; description?: string \| Record; header?: object; … }[]` | optional | Dashboards |
+| **reports** | `{ name: string; label: string \| Record; description?: string \| Record; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
+| **datasets** | `{ name: string; label: string \| Record; description?: string \| Record; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
+| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
+| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
+| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
+| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
+| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
+| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
+| **positions** | `{ name: string; label: string; description?: string; delegatable?: boolean; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
+| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
+| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
+| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
+| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
+| **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) |
+| **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) |
+| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them |
+| **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings |
+| **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes |
+| **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). |
+| **requires** | `string[]` | optional | Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; an unknown token is a defineStack error, declared-but-missing ⇒ fail-fast at startup) |
+| **tiers** | `string[]` | optional | Plugin tier presets to enable; overrides --preset |
+
+### Nested Shape: `InstalledPackageAtEitherStage[option 2].upgradeHistory[number]`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **fromVersion** | `string` | ✅ | Version before upgrade |
+| **toVersion** | `string` | ✅ | Version after upgrade |
+| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
+| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
+| **migrationLog** | `string[]` | optional | Migration step logs |
+
+---
+
+
+---
+
+## ListInstalledPackagesResponse
+
+List installed packages 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?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
+| **data** | `{ packages: (object \| object)[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
+
+### Nested Shape: `ListInstalledPackagesResponse.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) |
+| **message** | `string` | ✅ | Readable error message |
+| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
+| **refusal** | `true` | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose `message` is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose `message` is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — `true` is the only value. |
+| **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: `ListInstalledPackagesResponse.meta`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **timestamp** | `string` | ✅ | |
+| **duration** | `integer` | optional | Server-side processing duration in milliseconds |
+| **requestId** | `string` | optional | |
+| **traceId** | `string` | optional | |
+
+### Nested Shape: `ListInstalledPackagesResponse.data`
+
+| Property | Type | Required | Description |
+| :--- | :--- | :--- | :--- |
+| **packages** | `({ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more)[]` | ✅ | Installed packages |
+| **total** | `integer` | optional | Total matching packages |
+| **nextCursor** | `string` | optional | Cursor for the next page |
+| **hasMore** | `boolean` | ✅ | Whether more packages are available — this door serves one page, so always `false` |
+
+
+---
+
diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx
index 8b0b36df91a..704849141a0 100644
--- a/content/docs/references/api/package-api.mdx
+++ b/content/docs/references/api/package-api.mdx
@@ -20,6 +20,30 @@ POST /api/v1/packages/:packageId/rollback — Rollback a package
DELETE /api/v1/packages/:packageId — Uninstall a package
```
+### Five declarations of this API live one file over, on purpose
+
+The two READ responses (`ListInstalledPackagesResponseSchema`,
+`GetInstalledPackageResponseSchema`), the installed-row stages they are bound
+to (`AssembledInstalledPackageSchema`, `InstalledPackageAtEitherStageSchema`)
+and the `PackageApiContracts` map that names both responses are declared in
+`./package-api-assembled.zod.ts` and published from
+`@objectstack/spec/api-assembled`, not from `@objectstack/spec/api`.
+
+The reason is weight, not meaning. Four of them carry the ASSEMBLED package
+body (the fifth, the route map, names two of those four), which is the whole
+metadata vocabulary (`../stack.zod`) plus the datasource and driver-config
+validators behind it. While they sat in this
+file, every `@objectstack/spec/api` bundle linked that tree, and a browser
+consumer that imported two string constants from `./sortability.zod` paid
+for all of it: measured at about twice the gzipped bundle of the same import
+before the stage declarations arrived. The maintainer ruling on #18576
+(letter B) split the entry so the browser-facing half does not carry them.
+
+⛔ Nothing in this file may import `../stack.zod` or anything that reaches
+`../data/datasource.zod`: that edge is exactly what the split removed from
+`@objectstack/spec/api`, and `./api-entry-graph.pin.test.ts` refuses it.
+A declaration that needs the assembled body goes in the sibling file.
+
**Source:** `packages/spec/src/api/package-api.zod.ts`
@@ -27,107 +51,13 @@ DELETE /api/v1/packages/:packageId — Uninstall a package
## TypeScript Usage
```typescript
-import { AssembledInstalledPackageSchema, GetInstalledPackageRequestSchema, GetInstalledPackageResponseSchema, InstalledPackageAtEitherStageSchema, ListInstalledPackagesRequestSchema, ListInstalledPackagesResponseSchema, PackageApiErrorCode, PackageInstallBodySchema, PackageInstallRequestSchema, PackageInstallResponseSchema, PackagePathParamsSchema, PackageRollbackRequestSchema, PackageUpgradeRequestSchema, PackageUpgradeResponseSchema, ResolveDependenciesRequestSchema, ResolveDependenciesResponseSchema, UninstallPackageApiRequestSchema, UninstallPackageApiResponseSchema, UploadArtifactRequestSchema, UploadArtifactResponseSchema } from '@objectstack/spec/api';
-import type { AssembledInstalledPackage, GetInstalledPackageRequest, GetInstalledPackageResponse, InstalledPackageAtEitherStage, ListInstalledPackagesRequest, ListInstalledPackagesResponse, PackageApiErrorCode, PackageInstallBody, PackageInstallRequest, PackageInstallResponse, PackagePathParams, PackageRollbackRequest, PackageUpgradeRequest, PackageUpgradeResponse, ResolveDependenciesRequest, ResolveDependenciesResponse, UninstallPackageApiRequest, UninstallPackageApiResponse, UploadArtifactRequest, UploadArtifactResponse } from '@objectstack/spec/api';
+import { GetInstalledPackageRequestSchema, ListInstalledPackagesRequestSchema, PackageApiErrorCode, PackageInstallBodySchema, PackageInstallRequestSchema, PackageInstallResponseSchema, PackagePathParamsSchema, PackageRollbackRequestSchema, PackageUpgradeRequestSchema, PackageUpgradeResponseSchema, ResolveDependenciesRequestSchema, ResolveDependenciesResponseSchema, UninstallPackageApiRequestSchema, UninstallPackageApiResponseSchema, UploadArtifactRequestSchema, UploadArtifactResponseSchema } from '@objectstack/spec/api';
+import type { GetInstalledPackageRequest, ListInstalledPackagesRequest, PackageApiErrorCode, PackageInstallBody, PackageInstallRequest, PackageInstallResponse, PackagePathParams, PackageRollbackRequest, PackageUpgradeRequest, PackageUpgradeResponse, ResolveDependenciesRequest, ResolveDependenciesResponse, UninstallPackageApiRequest, UninstallPackageApiResponse, UploadArtifactRequest, UploadArtifactResponse } from '@objectstack/spec/api';
// Validate data
-const result = AssembledInstalledPackageSchema.parse(data);
+const result = GetInstalledPackageRequestSchema.parse(data);
```
----
-
-## AssembledInstalledPackage
-
-Installed package row whose manifest is the assembled package body
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
-| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
-| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
-| **installedAt** | `string` | optional | Installation timestamp |
-| **updatedAt** | `string` | optional | Last update timestamp |
-| **installedVersion** | `string` | optional | Currently installed version for quick access |
-| **previousVersion** | `string` | optional | Version before the last upgrade |
-| **statusChangedAt** | `string` | optional | Status change timestamp |
-| **errorMessage** | `string` | optional | Error message when status is error |
-| **settings** | `Record` | optional | User-provided configuration settings |
-| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
-| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
-
-### Nested Shape: `AssembledInstalledPackage.manifest`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
-| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
-| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
-| **version** | `string` | ✅ | Package version (semantic versioning) |
-| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
-| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
-| **name** | `string` | ✅ | Human-readable package name |
-| **description** | `string` | optional | Package description |
-| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |
-| **objects** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }[]` | optional | Business Objects definition (owned by this package) |
-| **datasources** | `{ name: string; label?: string; driver: string; config: Record; … }[]` | optional | External Data Connections |
-| **dependencies** | `Record` | optional | Package dependencies |
-| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
-| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
-| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
-| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
-| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
-| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
-| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
-| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
-| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
-| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
-| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
-| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
-| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
-| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
-| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
-| **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles |
-| **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
-| **apps** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Applications |
-| **views** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }[]` | optional | List Views |
-| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |
-| **pages** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Custom Pages |
-| **dashboards** | `{ name: string; label: string \| Record; description?: string \| Record; header?: object; … }[]` | optional | Dashboards |
-| **reports** | `{ name: string; label: string \| Record; description?: string \| Record; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
-| **datasets** | `{ name: string; label: string \| Record; description?: string \| Record; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
-| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
-| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
-| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
-| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
-| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
-| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
-| **positions** | `{ name: string; label: string; description?: string; delegatable?: boolean; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
-| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
-| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
-| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
-| **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) |
-| **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) |
-| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them |
-| **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings |
-| **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes |
-| **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). |
-| **requires** | `string[]` | optional | Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; an unknown token is a defineStack error, declared-but-missing ⇒ fail-fast at startup) |
-| **tiers** | `string[]` | optional | Plugin tier presets to enable; overrides --preset |
-
-### Nested Shape: `AssembledInstalledPackage.upgradeHistory[number]`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **fromVersion** | `string` | ✅ | Version before upgrade |
-| **toVersion** | `string` | ✅ | Version after upgrade |
-| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
-| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
-| **migrationLog** | `string[]` | optional | Migration step logs |
-
-
---
## GetInstalledPackageRequest
@@ -142,250 +72,6 @@ Get installed package request
| **version** | `string` | optional | Scope the read to this exact installed version; `latest` or omitted reads the installed row |
----
-
-## GetInstalledPackageResponse
-
-Get installed 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?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
-| **data** | `{ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more` | ✅ | Installed package details |
-
-### Nested Shape: `GetInstalledPackageResponse.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) |
-| **message** | `string` | ✅ | Readable error message |
-| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
-| **refusal** | `true` | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose `message` is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose `message` is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — `true` is the only value. |
-| **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: `GetInstalledPackageResponse.meta`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `string` | ✅ | |
-| **duration** | `integer` | optional | Server-side processing duration in milliseconds |
-| **requestId** | `string` | optional | |
-| **traceId** | `string` | optional | |
-
-### Nested Shape: `GetInstalledPackageResponse.data[option 1]`
-
-Installed package with runtime lifecycle state
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
-| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
-| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
-| **installedAt** | `string` | optional | Installation timestamp |
-| **updatedAt** | `string` | optional | Last update timestamp |
-| **installedVersion** | `string` | optional | Currently installed version for quick access |
-| **previousVersion** | `string` | optional | Version before the last upgrade |
-| **statusChangedAt** | `string` | optional | Status change timestamp |
-| **errorMessage** | `string` | optional | Error message when status is error |
-| **settings** | `Record` | optional | User-provided configuration settings |
-| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
-| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
-
-### Nested Shape: `GetInstalledPackageResponse.data[option 2]`
-
-Installed package row whose manifest is the assembled package body
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
-| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
-| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
-| **installedAt** | `string` | optional | Installation timestamp |
-| **updatedAt** | `string` | optional | Last update timestamp |
-| **installedVersion** | `string` | optional | Currently installed version for quick access |
-| **previousVersion** | `string` | optional | Version before the last upgrade |
-| **statusChangedAt** | `string` | optional | Status change timestamp |
-| **errorMessage** | `string` | optional | Error message when status is error |
-| **settings** | `Record` | optional | User-provided configuration settings |
-| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
-| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
-
-
----
-
-## InstalledPackageAtEitherStage
-
-Installed package row at whichever manifest stage it was installed at
-
-### Union Options
-
-This schema accepts one of the following structures:
-
-#### Option 1
-
-Installed package with runtime lifecycle state
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | Package manifest at the AUTHORING stage; a row installed by a `defineStack()` host carries the assembled body instead — see `AssembledInstalledPackageSchema` / `InstalledPackageAtEitherStageSchema` |
-| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
-| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
-| **installedAt** | `string` | optional | Installation timestamp |
-| **updatedAt** | `string` | optional | Last update timestamp |
-| **installedVersion** | `string` | optional | Currently installed version for quick access |
-| **previousVersion** | `string` | optional | Version before the last upgrade |
-| **statusChangedAt** | `string` | optional | Status change timestamp |
-| **errorMessage** | `string` | optional | Error message when status is error |
-| **settings** | `Record` | optional | User-provided configuration settings |
-| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
-| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
-
-### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
-| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
-| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
-| **version** | `string` | ✅ | Package version (semantic versioning) |
-| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
-| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
-| **name** | `string` | ✅ | Human-readable package name |
-| **description** | `string` | optional | Package description |
-| **permissions** | `string[] \| { services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: legacy string[] or structured plugin block (ADR-0025 §3.2) — at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
-| **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files |
-| **datasources** | `string[]` | optional | Glob patterns for Datasource definitions |
-| **dependencies** | `Record` | optional | Package dependencies |
-| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
-| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
-| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
-| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
-| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
-| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
-| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
-| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
-| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
-| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
-| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
-| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
-| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
-
-### Nested Shape: `InstalledPackageAtEitherStage[option 1].upgradeHistory[number]`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **fromVersion** | `string` | ✅ | Version before upgrade |
-| **toVersion** | `string` | ✅ | Version after upgrade |
-| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
-| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
-| **migrationLog** | `string[]` | optional | Migration step logs |
-
----
-
-#### Option 2
-
-Installed package row whose manifest is the assembled package body
-
-### Properties
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **manifest** | `{ id: string; namespace?: string; defaultDatasource?: string; version: string; … }` | ✅ | The ASSEMBLED package body this row carries, at the stage the registry records it |
-| **status** | `Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>` | optional (default: `"installed"`) | Package state: installed, disabled, installing, upgrading, uninstalling, or error |
-| **enabled** | `boolean` | optional (default: `true`) | Whether the package is currently enabled |
-| **installedAt** | `string` | optional | Installation timestamp |
-| **updatedAt** | `string` | optional | Last update timestamp |
-| **installedVersion** | `string` | optional | Currently installed version for quick access |
-| **previousVersion** | `string` | optional | Version before the last upgrade |
-| **statusChangedAt** | `string` | optional | Status change timestamp |
-| **errorMessage** | `string` | optional | Error message when status is error |
-| **settings** | `Record` | optional | User-provided configuration settings |
-| **upgradeHistory** | `{ fromVersion: string; toVersion: string; upgradedAt: string; status: Enum<'success' \| 'failed' \| 'rolled_back'>; … }[]` | optional | Version upgrade history |
-| **registeredNamespaces** | `string[]` | optional | Namespace prefixes registered by this package |
-
-### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **id** | `string` | ✅ | Unique package identifier — must match reverse-domain notation (e.g. com.acme.crm) |
-| **namespace** | `string` | optional | Short namespace identifier; also the mandatory prefix of every object name (e.g. "todo" → object names "todo_task", "todo_project") |
-| **defaultDatasource** | `string` | optional (default: `"default"`) | Default datasource for all objects in this package |
-| **version** | `string` | ✅ | Package version (semantic versioning) |
-| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
-| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
-| **name** | `string` | ✅ | Human-readable package name |
-| **description** | `string` | optional | Package description |
-| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |
-| **objects** | `{ name: string; label?: string; pluralLabel?: string; description?: string; … }[]` | optional | Business Objects definition (owned by this package) |
-| **datasources** | `{ name: string; label?: string; driver: string; config: Record; … }[]` | optional | External Data Connections |
-| **dependencies** | `Record` | optional | Package dependencies |
-| **configuration** | `never` | optional | [REMOVED] `manifest.configuration` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — nothing ever read the block: no settings UI rendered it and no loader resolved a setting from it, so authoring it configured nothing. Worse, `properties.*.secret` promised "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even parsed the flag — a false assurance about credential handling. Delete the key. A plugin is configured by the host that composes it: pass options to its constructor in `defineStack({ plugins: [new MyPlugin({ … })] })`, which is the enforced channel. A declarative settings surface must be designed with an enforcing reader first, not revived here. |
-| **contributes** | `{ kinds?: object[] }` | optional | Platform contributions |
-| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Seed Data / Fixtures for bootstrapping |
-| **capabilities** | `{ name: string; label?: string; description?: string; scope?: Enum<'platform' \| 'org'>; … }[]` | optional | [ADR-0066 D1] Authorization capabilities this package defines (seeded with package provenance) |
-| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
-| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +9 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
-| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
-| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
-| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
-| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |
-| **packaging** | `Enum<'bundled' \| 'manifest-deps'>` | optional | Dependency packaging strategy (ADR-0025 §3.3) |
-| **main** | `string` | optional | Entry module of a code-bearing plugin, relative to the plugin root; `os plugin build` bundles it and writes `dist/index.mjs` here in the compiled manifest (ADR-0025 §3.4) |
-| **integrity** | `Record` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
-| **functions** | `Record }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
-| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
-| **translations** | `Record; apps?: Record; messages?: Record; globalActions?: Record; … }>[]` | optional | I18n Translation Bundles |
-| **objectExtensions** | `{ extend: string; fields?: Record; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
-| **apps** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Applications |
-| **views** | `{ name?: string; label?: string \| Record; object?: string; list?: object; … }[]` | optional | List Views |
-| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |
-| **pages** | `{ name: string; label: string \| Record; description?: string \| Record; icon?: string; … }[]` | optional | Custom Pages |
-| **dashboards** | `{ name: string; label: string \| Record; description?: string \| Record; header?: object; … }[]` | optional | Dashboards |
-| **reports** | `{ name: string; label: string \| Record; description?: string \| Record; type?: Enum<'tabular' \| 'summary' \| 'matrix' \| 'joined'>; … }[]` | optional | Analytics Reports |
-| **datasets** | `{ name: string; label: string \| Record; description?: string \| Record; object: string; … }[]` | optional | Analytics semantic-layer datasets (ADR-0021) |
-| **actions** | `{ name: string; label: string \| Record; description?: string \| Record; objectName?: string; … }[]` | optional | Global and Object Actions. Unique per scope, not per stack: the runtime keys every action by its owning object's name (or 'global' when object-less), a colon, then the action name, and defineStack refuses two declarations that resolve to one key — both here, both on one object's actions, or one in each position, identical twins included (an embedded action is keyed by the object it is written on, not by its own objectName). One global and one object-bound action may share a name; on that object's route the object's own actions take precedence for by-name readers. composeStacks runs the same key rule across its input stacks (counting distinct stacks, not sites) and names both source stacks on a collision. |
-| **flows** | `{ name: string; label: string; description?: string; successMessage?: string; … }[]` | optional | Screen Flows |
-| **jobs** | `{ name: string; label?: string; description?: string; schedule: object \| object \| object; … }[]` | optional | Background / Scheduled Jobs (run by IJobService on cron/interval/once schedules) |
-| **emailTemplates** | `{ name: string; label: string; category?: Enum<'auth' \| 'notification' \| 'workflow' \| 'marketing' \| 'custom'>; locale?: string; … }[]` | optional | Email Templates resolved by IEmailService.sendTemplate(`{ template, locale }`) |
-| **docs** | `{ name: string; label?: string; description?: string; content: string; … }[]` | optional | Package documentation — flat Markdown items compiled from src/docs/*.md (ADR-0046) |
-| **books** | `{ name: string; label?: string; description?: string; slug?: string; … }[]` | optional | Documentation navigation spines — ordered groups with derived membership (ADR-0046 §6) |
-| **positions** | `{ name: string; label: string; description?: string; delegatable?: boolean; … }[]` | optional | Positions — flat capability-distribution groups (ADR-0090 D3) |
-| **sharingRules** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Record Sharing Rules |
-| **apis** | `{ name: string; path: string; method: Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>; summary?: string; … }[]` | optional | API Endpoints — declared endpoints are live from protocol 17; each is gated at publish (ADR-0121) |
-| **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Outbound Webhooks |
-| **agents** | `{ name: string; label: string; avatar?: string; role: string; … }[]` | optional | AI Agents — platform-internal (ADR-0063 §2): the kernel ships exactly two (ask/build); third parties extend via skills, not agents |
-| **tools** | `{ name: string; label: string; description: string; parameters: Record; … }[]` | optional | AI Tool metadata records — optional refinement layer, never required: the default path is skills referencing platform tools or materialised action_`` tools (ADR-0109) |
-| **skills** | `{ name: string; label: string; description?: string; surface?: Enum<'ask' \| 'build' \| 'both'>; … }[]` | optional | AI Skills (reusable capability bundles — the third-party AI extension primitive, ADR-0063) |
-| **hooks** | `{ name: string; label?: string; object: string \| string[]; events: Enum<'beforeFind' \| 'afterFind' \| 'beforeInsert' \| 'afterInsert' \| 'beforeUpdate' \| …>[]; … }[]` | optional | Object Lifecycle Hooks, as a JSON document carries them |
-| **mappings** | `{ name: string; label?: string; sourceFormat?: Enum<'csv' \| 'json' \| 'xml' \| 'sql'>; targetObject: string; … }[]` | optional | Data Import/Export Mappings |
-| **analyticsCubes** | `{ name: string; title?: string; description?: string; sql: string; … }[]` | optional | Analytics Semantic Layer Cubes |
-| **connectors** | `{ name: string; label: string; type: Enum<'saas' \| 'database' \| 'file_storage' \| 'message_queue' \| 'api' \| 'custom'>; description?: string; … }[]` | optional | External System Connectors. A provider-bound entry (has `provider`: openapi/mcp/rest) is materialized into a live, dispatchable connector at boot and referenced by flows via `connector_action`; credentials are `auth.credentialRef` references, never inline secrets. An entry with no `provider` is a catalog descriptor only (NOT dispatchable) — set `enabled: false` on deliberate descriptors. Unknown provider / unresolvable credentialRef / name conflict ⇒ hard boot error (ADR-0097). |
-| **requires** | `string[]` | optional | Capability names this stack requires from the platform (canonical kebab-case tokens from PLATFORM_CAPABILITY_TOKENS; an unknown token is a defineStack error, declared-but-missing ⇒ fail-fast at startup) |
-| **tiers** | `string[]` | optional | Plugin tier presets to enable; overrides --preset |
-
-### Nested Shape: `InstalledPackageAtEitherStage[option 2].upgradeHistory[number]`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **fromVersion** | `string` | ✅ | Version before upgrade |
-| **toVersion** | `string` | ✅ | Version after upgrade |
-| **upgradedAt** | `string` | ✅ | Upgrade timestamp |
-| **status** | `Enum<'success' \| 'failed' \| 'rolled_back'>` | ✅ | Upgrade outcome |
-| **migrationLog** | `string[]` | optional | Migration step logs |
-
----
-
-
---
## ListInstalledPackagesRequest
@@ -403,54 +89,6 @@ List installed packages request
| **cursor** | `never` | optional | [REMOVED] `limit` / `cursor` were removed from GET /api/v1/packages in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — both were declared here and read by nothing: the serving door filters on `status` / `type` / `enabled` and then returns every remaining row, so no page was ever withheld and no continuation token was ever minted. `limit` also declared `.default(50)`, so a reader of the published schema was entitled to believe an unparameterised list is capped at 50 rows; it has never been capped at all, and nothing parses a query string through this schema, so that default has never been stamped onto anything. Delete the key. This route is NOT paginated — it answers the whole installed set, which is a bounded table of tens of rows, and `hasMore` on the response is a constant `false` that is now true by construction. Filter with `status`, `type` and `enabled` instead of asking for a window. A first-class package cursor, if one is ever designed, will be a response-minted opaque token, not this key. |
----
-
-## ListInstalledPackagesResponse
-
-List installed packages 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?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
-| **data** | `{ packages: (object \| object)[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
-
-### Nested Shape: `ListInstalledPackagesResponse.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) |
-| **message** | `string` | ✅ | Readable error message |
-| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
-| **refusal** | `true` | optional | Producer-declared: the 5xx this envelope carries is a deliberate refusal whose `message` is authored for the caller, so a boundary that reads the declaration keeps it verbatim (until the withhold arms read it, a declared refusal is still withheld). Absent (the default) on a declared fault, whose `message` is withheld from the body and logged for the operator; redundant on a 4xx. Presence is the declaration — `true` is the only value. |
-| **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: `ListInstalledPackagesResponse.meta`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **timestamp** | `string` | ✅ | |
-| **duration** | `integer` | optional | Server-side processing duration in milliseconds |
-| **requestId** | `string` | optional | |
-| **traceId** | `string` | optional | |
-
-### Nested Shape: `ListInstalledPackagesResponse.data`
-
-| Property | Type | Required | Description |
-| :--- | :--- | :--- | :--- |
-| **packages** | `({ manifest: object; status?: Enum<'installed' \| 'disabled' \| 'installing' \| 'upgrading' \| 'uninstalling' \| 'error'>; enabled?: boolean; installedAt?: string; … } \| … +1 more)[]` | ✅ | Installed packages |
-| **total** | `integer` | optional | Total matching packages |
-| **nextCursor** | `string` | optional | Cursor for the next page |
-| **hasMore** | `boolean` | ✅ | Whether more packages are available — this door serves one page, so always `false` |
-
-
---
## PackageApiErrorCode
diff --git a/content/docs/references/api/package-lifecycle.mdx b/content/docs/references/api/package-lifecycle.mdx
index 1564ca24c78..cc8ed88b4be 100644
--- a/content/docs/references/api/package-lifecycle.mdx
+++ b/content/docs/references/api/package-lifecycle.mdx
@@ -25,11 +25,12 @@ schema declares the `data` payload, envelope-free — the same convention as
(`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`.
+copy) because the route-ledger resolver looks names up only in the API
+protocol's entries — `@objectstack/spec/api`, and `/api-assembled` for the
+declarations that embed the assembled package body (#18576).
The retired `PackageRollbackResponseSchema` and its
-`PackageApiContracts.rollbackPackage` binding (see `./package-api.zod.ts`)
+`PackageApiContracts.rollbackPackage` binding (see `./package-api-assembled.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).
diff --git a/content/docs/references/index.mdx b/content/docs/references/index.mdx
index 035c94d90d7..d49933c191c 100644
--- a/content/docs/references/index.mdx
+++ b/content/docs/references/index.mdx
@@ -20,7 +20,7 @@ counts are sums of the rows they head. Regenerate with
| Module | Pages | Schemas | Description |
| :--- | ---: | ---: | :--- |
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
-| [API Protocol](/docs/references/api) | 31 | 444 | REST contracts, endpoints, routing, realtime, batch, discovery. |
+| [API Protocol](/docs/references/api) | 32 | 444 | REST contracts, endpoints, routing, realtime, batch, discovery. |
| [Automation Protocol](/docs/references/automation) | 14 | 75 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
| [Data Protocol](/docs/references/data) | 29 | 175 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -33,7 +33,7 @@ counts are sums of the rows they head. Regenerate with
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 34 | 275 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 159 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
-| **Total** | **195** | **1538** | 14 protocol modules |
+| **Total** | **196** | **1538** | 14 protocol modules |
---
@@ -62,7 +62,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
## API Protocol
-**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **31 pages, 444 schemas**
+**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **32 pages, 444 schemas**
REST contracts, endpoints, routing, realtime, batch, discovery.
@@ -86,7 +86,8 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
| [`metadata.zod.ts`](/docs/references/api/metadata) | `AppDefinitionResponse`, `ConceptListResponse`, `MetadataBulkRegisterRequest`, `MetadataBulkResponse`, `MetadataBulkUnregisterRequest`, `MetadataDeleteResponse`, `MetadataDependenciesResponse`, `MetadataDependentsResponse`, `MetadataExistsResponse`, `MetadataExportRequest`, `MetadataExportResponse`, `MetadataImportRequest`, `MetadataImportResponse`, `MetadataItemResponse`, `MetadataListResponse`, `MetadataNamesResponse`, `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) | `AssembledInstalledPackage`, `GetInstalledPackageRequest`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesRequest`, `ListInstalledPackagesResponse`, `PackageApiErrorCode`, `PackageInstallBody`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` |
+| [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `ListInstalledPackagesRequest`, `PackageApiErrorCode`, `PackageInstallBody`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` |
+| [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` |
| [`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`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `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`, `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`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `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`, `SearchAllPageHit`, `SearchAllResponse`, `SetPresenceRequest`, `SetPresenceResponse`, `UninstallPackageRequest`, `UninstallPackageResponse`, `UnregisterDeviceRequest`, `UnregisterDeviceResponse`, `UpdateAiConversationRequest`, `UpdateDataRequest`, `UpdateDataResponse`, `UpdateManyDataRequest`, `UpdateManyDataResponse`, `UpdateNotificationPreferencesRequest`, `UpdateNotificationPreferencesResponse`, `ValidateDataIssue`, `ValidateDataRequest`, `ValidateDataResponse` |
diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts
index fc23fd963d4..27e31168c33 100644
--- a/packages/client/src/index.ts
+++ b/packages/client/src/index.ts
@@ -123,21 +123,26 @@ import {
PackageExportManifest,
ReassignOrphanedMetadataResponse,
DuplicatePackageResponse,
- // [#17536] The element the two `/packages` READ doors are declared to serve.
- // `ListInstalledPackagesResponseSchema.packages` is
- // `z.array(InstalledPackageAtEitherStageSchema)` and
- // `GetInstalledPackageResponseSchema.data` is that same schema
- // (`spec/src/api/package-api.zod.ts`) — a union over the two manifest stages,
- // authoring (`InstalledPackageSchema`) and assembled
- // (`AssembledInstalledPackageSchema`), each a closed RUNTIME declaration. The
- // client is a CONSUMER of that contract, so the widest value those doors are
- // declared to answer is what they are declared to return here. ⚠️ What the
- // published TYPE admits is wider than what the runtime parse accepts — the
- // measurement, and what a caller does about it, are on `packages.list` below
- // (#19324). The WRITE methods on the same object keep `InstalledPackage`:
- // PR #17517 moved the read doors alone.
- InstalledPackageAtEitherStage,
} from '@objectstack/spec/api';
+// [#17536] The element the two `/packages` READ doors are declared to serve.
+// `ListInstalledPackagesResponseSchema.packages` is
+// `z.array(InstalledPackageAtEitherStageSchema)` and
+// `GetInstalledPackageResponseSchema.data` is that same schema
+// (`spec/src/api/package-api-assembled.zod.ts`) — a union over the two manifest
+// stages, authoring (`InstalledPackageSchema`) and assembled
+// (`AssembledInstalledPackageSchema`), each a closed RUNTIME declaration. The
+// client is a CONSUMER of that contract, so the widest value those doors are
+// declared to answer is what they are declared to return here. ⚠️ What the
+// published TYPE admits is wider than what the runtime parse accepts — the
+// measurement, and what a caller does about it, are on `packages.list` below
+// (#19324). The WRITE methods on the same object keep `InstalledPackage`:
+// PR #17517 moved the read doors alone.
+//
+// Imported from `@objectstack/spec/api-assembled`, not `/api`: the declarations
+// that embed the assembled package body left the browser-facing `/api` entry
+// (#18576 ruling, letter B). A TYPE import — erased from this package's
+// bundle, so the client links none of that tree.
+import type { InstalledPackageAtEitherStage } from '@objectstack/spec/api-assembled';
import type {
ApprovalRequestRow,
ApprovalActionRow,
@@ -2494,7 +2499,7 @@ export class ObjectStackClient {
* the row with a `packages/spec` schema and reading the parse's output:
*
* ```ts
- * const parsed = AssembledInstalledPackageSchema.safeParse(pkg); // spec/api
+ * const parsed = AssembledInstalledPackageSchema.safeParse(pkg); // spec/api-assembled
* if (parsed.success) {
* // parsed.data.manifest — the ASSEMBLED stage, object definitions
* } else {
diff --git a/packages/client/src/return-type-precision.test.ts b/packages/client/src/return-type-precision.test.ts
index 78185c990b5..8439bd817db 100644
--- a/packages/client/src/return-type-precision.test.ts
+++ b/packages/client/src/return-type-precision.test.ts
@@ -69,10 +69,12 @@ import type { SearchAllResponse } from '@objectstack/spec/api';
// [#17536] The two READ doors' declared element, and the branch of it that made
// the client's authoring-stage declaration wrong. Both are imported as TYPES:
// the pins below are the only thing that can observe a return-type move.
+// Published from `@objectstack/spec/api-assembled` since the #18576 split: the
+// declarations that embed the assembled package body left `/api`.
import type {
AssembledInstalledPackage,
InstalledPackageAtEitherStage,
-} from '@objectstack/spec/api';
+} from '@objectstack/spec/api-assembled';
import type {
AnalyticsMetadataResponse,
AnalyticsSqlResponse,
diff --git a/packages/client/src/route-ledger-response-schema.test.ts b/packages/client/src/route-ledger-response-schema.test.ts
index a417620c1f0..3c440839bf7 100644
--- a/packages/client/src/route-ledger-response-schema.test.ts
+++ b/packages/client/src/route-ledger-response-schema.test.ts
@@ -4,7 +4,8 @@
* `responseSchema` resolution guard (#5791, first step of #3877).
*
* WHAT THE FIELD IS. Every route ledger's entry type now carries an optional
- * `responseSchema` — the NAME of the `@objectstack/spec/api` export declaring
+ * `responseSchema` — the NAME of the `@objectstack/spec/api` (or, since
+ * #18576, `@objectstack/spec/api-assembled`) export declaring
* that route's response payload. #3877 measured the hole it opens onto: of 237
* ledgered routes, 215 are `sdk` surface and **zero** carried any schema
* reference, so for ~90% of the mounted surface the problem was never
@@ -17,7 +18,8 @@
* rots the same way with no edit at all. That is the "declared but nobody
* verifies" surface #3877 exists to remove, so the field could not land without
* the resolver that refuses it. Each name is looked up in the LIVE
- * `@objectstack/spec/api` export namespace and required to be a real zod
+ * `@objectstack/spec/api` + `@objectstack/spec/api-assembled` export namespaces
+ * and required to be a real zod
* schema, exercised rather than duck-typed.
*
* WHY HERE, of all packages. The five ledgers are five independent declarations
@@ -41,6 +43,7 @@
import { describe, it, expect } from 'vitest';
import * as specApi from '@objectstack/spec/api';
+import * as specApiAssembled from '@objectstack/spec/api-assembled';
import { ROUTE_LEDGER } from '../../runtime/src/route-ledger';
import { REST_ROUTE_LEDGER } from '../../rest/src/rest-route-ledger';
import { STORAGE_ROUTE_LEDGER } from '../../services/service-storage/src/storage-route-ledger';
@@ -68,7 +71,18 @@ function declaredRows(): Array<{ ledger: string; route: string; responseSchema:
);
}
-const exportsOfSpecApi = specApi as unknown as Record;
+/**
+ * The API protocol's exports, across BOTH entries that publish it. Since the
+ * #18576 ruling (letter B) the declarations whose payload embeds the assembled
+ * package body — the two package READ responses among them — ship from
+ * `@objectstack/spec/api-assembled` instead of `@objectstack/spec/api`, so a
+ * ledger row naming one of them resolves there. The two entries share no name
+ * (pinned below), so the union cannot hide which one answered.
+ */
+const exportsOfSpecApi = {
+ ...(specApi as unknown as Record),
+ ...(specApiAssembled as unknown as Record),
+} as Record;
/**
* The resolver under test, extracted so the negative control below can drive
@@ -84,7 +98,7 @@ const exportsOfSpecApi = specApi as unknown as Record;
function resolutionFailure(name: string): string | undefined {
if (name.trim() === '') return 'is empty';
if (!Object.prototype.hasOwnProperty.call(exportsOfSpecApi, name)) {
- return 'is not an export of `@objectstack/spec/api`';
+ return 'is not an export of `@objectstack/spec/api` or `@objectstack/spec/api-assembled`';
}
const candidate = exportsOfSpecApi[name] as { safeParse?: (v: unknown) => unknown };
if (typeof candidate?.safeParse !== 'function') return 'is exported but is not a zod schema';
@@ -94,7 +108,7 @@ function resolutionFailure(name: string): string | undefined {
}
describe('[#5791] every ledgered `responseSchema` names a real spec schema', () => {
- it('resolves each declared name against the live `@objectstack/spec/api` exports', () => {
+ it('resolves each declared name against the live `@objectstack/spec/api` / `/api-assembled` exports', () => {
const broken = declaredRows()
.map(({ ledger, route, responseSchema }) => {
const why = resolutionFailure(responseSchema);
@@ -105,7 +119,7 @@ describe('[#5791] every ledgered `responseSchema` names a real spec schema', ()
expect(
broken,
'Ledger rows whose `responseSchema` does not resolve to a zod schema exported from '
- + '`@objectstack/spec/api`. Fix the name, or drop the field — an unresolvable '
+ + '`@objectstack/spec/api` or `@objectstack/spec/api-assembled`. Fix the name, or drop the field — an unresolvable '
+ 'declaration is worse than none (#3877).',
).toEqual([]);
});
@@ -128,7 +142,7 @@ describe('[#5791] every ledgered `responseSchema` names a real spec schema', ()
// The failure path, driven. Each case is a way a hand-written name goes
// wrong in review, and none of them is caught by `string`.
expect(resolutionFailure('GetDiscoverResponseSchema')).toBe(
- 'is not an export of `@objectstack/spec/api`',
+ 'is not an export of `@objectstack/spec/api` or `@objectstack/spec/api-assembled`',
); // one letter short of a real export
expect(resolutionFailure('')).toBe('is empty');
expect(resolutionFailure('WELL_KNOWN_CAPABILITY_KEYS')).toBe(
@@ -137,6 +151,14 @@ describe('[#5791] every ledgered `responseSchema` names a real spec schema', ()
// …and the positive control, so the rejections above are not a resolver
// that rejects everything.
expect(resolutionFailure('DiscoverySchema')).toBeUndefined();
+ // …and one from the sibling entry, so the union really reaches it.
+ expect(resolutionFailure('ListInstalledPackagesResponseSchema')).toBeUndefined();
+ });
+
+ it('the two API entries share no export name, so the union is unambiguous', () => {
+ const shared = Object.keys(specApiAssembled).filter((name) =>
+ Object.prototype.hasOwnProperty.call(specApi, name));
+ expect(shared, 'a name exported by both API entries').toEqual([]);
});
it('the two #5791 landing rows are the discovery pair, in two different ledgers', () => {
diff --git a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts
index c81095de20c..18cd35a0fa4 100644
--- a/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts
+++ b/packages/runtime/src/domains/packages-read-delete-response-conformance.test.ts
@@ -2,8 +2,11 @@
/**
* #16781 deliverable 2 — the payloads `GET /packages` and
- * `DELETE /packages/:id` actually serve, parsed against the contracts
- * `@objectstack/spec/api` declares for them.
+ * `DELETE /packages/:id` actually serve, parsed against the contracts the
+ * API protocol declares for them — the delete response from
+ * `@objectstack/spec/api`, the two read responses from
+ * `@objectstack/spec/api-assembled` (they embed the assembled package body, and
+ * the #18576 ruling moved every such declaration off the browser-facing entry).
*
* ## What was measured, and why this file exists
*
@@ -45,8 +48,9 @@
* That is the mismatch #14242 identified one layer down, whose maintainer
* ruling (2026-09-02, quoted in `stack.zod.ts` at `ArtifactPackageSchema`) was
* to «declare the assembled stage rather than widen the authoring one». #17431
- * followed that ruling one layer up: `@objectstack/spec/api` now declares
- * `AssembledInstalledPackageSchema`, and both read responses are bound to
+ * followed that ruling one layer up: the API protocol now declares
+ * `AssembledInstalledPackageSchema` (published from
+ * `@objectstack/spec/api-assembled` since #18576), and both read responses are bound to
* `InstalledPackageAtEitherStageSchema` — a union over the two whole, closed
* stage declarations. ⛔ Neither stage was widened; a row belonging to NEITHER
* is still refused, and that is asserted below rather than assumed.
@@ -75,11 +79,13 @@
import { describe, it, expect, vi } from 'vitest';
import { SchemaRegistry } from '@objectstack/objectql';
+import { UninstallPackageApiResponseSchema } from '@objectstack/spec/api';
+// The two READ responses embed the assembled package body, so they ship from
+// `@objectstack/spec/api-assembled` rather than `/api` (#18576 ruling, letter B).
import {
ListInstalledPackagesResponseSchema,
GetInstalledPackageResponseSchema,
- UninstallPackageApiResponseSchema,
-} from '@objectstack/spec/api';
+} from '@objectstack/spec/api-assembled';
import { InstalledPackageSchema } from '@objectstack/spec/kernel';
import { HttpDispatcher, type HttpDispatcherResult } from '../http-dispatcher.js';
diff --git a/packages/runtime/src/route-ledger.ts b/packages/runtime/src/route-ledger.ts
index ddf3e858969..609cdeee096 100644
--- a/packages/runtime/src/route-ledger.ts
+++ b/packages/runtime/src/route-ledger.ts
@@ -119,7 +119,11 @@ export interface RouteLedgerEntry {
/** Dotted method path on `ObjectStackClient` — required when disposition is `sdk`. */
client?: string;
/**
- * Name of the `@objectstack/spec/api` export declaring this route's response
+ * Name of the API-protocol export declaring this route's response PAYLOAD —
+ * an export of `@objectstack/spec/api`, or of its sibling entry
+ * `@objectstack/spec/api-assembled`, which carries the API declarations whose
+ * payload embeds the assembled package body (the two package READ responses;
+ * the #18576 ruling moved them off the browser-facing `/api`). The
* PAYLOAD — the `data` of the shared `{ success, data }` envelope where the
* route emits one, the whole body where it does not. The envelope itself is
* not this field's business; `pnpm check:route-envelope` guards it
@@ -140,8 +144,8 @@ export interface RouteLedgerEntry {
* can demand coverage for it; a name written ahead of the test it points at
* would BE the "declared but unverified" surface the programme exists to
* remove. `packages/client/src/route-ledger-response-schema.test.ts` resolves
- * every name written here against the live `@objectstack/spec/api` exports,
- * so a typo or a retired schema fails loudly rather than rotting.
+ * every name written here against the live exports of those two entries, so a
+ * typo or a retired schema fails loudly rather than rotting.
*
* A NAME rather than a live schema object, deliberately: this module stays
* import-free — the client-side guards compile it as a relative SOURCE file,
@@ -382,7 +386,7 @@ export const ROUTE_LEDGER: readonly RouteLedgerEntry[] = [
// note records what its declaration does NOT carry.
{ route: 'GET /packages', domain: '/packages', disposition: 'sdk', client: 'packages.list',
responseSchema: 'ListInstalledPackagesResponseSchema',
- note: 'The schema names the WHOLE BODY here, envelope included (`BaseResponseSchema.extend({ data })`), not the `data` alone its lifecycle siblings above declare. This row was blank until now as a MEASURED verdict: an earlier contract review had added `hasMore`, but every row was still typed `InstalledPackageSchema`, whose `manifest` is the AUTHORING-stage `ManifestSchema` (`objects` = glob patterns), while a `defineStack()` host installs the ASSEMBLED body (`objects` = object definitions) — the stage mismatch the comment above names. It is filled by following that ruling one layer up: `@objectstack/spec/api` declares `AssembledInstalledPackageSchema` and binds both read responses to `InstalledPackageAtEitherStageSchema`, a union over the two whole CLOSED stage declarations — neither stage widened, and a row belonging to neither still refused. Fillable because `domains/packages-read-delete-response-conformance.test.ts` drives THIS handler and parses the payload it answers on BOTH authoring paths. ⚠️ The declaration is a strict SUBSET of the wire: each row also carries `writable`, this door\'s own computed verdict and not a declared record field, which a declared parse therefore strips — asserted by name in the same file rather than fixed' },
+ note: 'The schema names the WHOLE BODY here, envelope included (`BaseResponseSchema.extend({ data })`), not the `data` alone its lifecycle siblings above declare. This row was blank until now as a MEASURED verdict: an earlier contract review had added `hasMore`, but every row was still typed `InstalledPackageSchema`, whose `manifest` is the AUTHORING-stage `ManifestSchema` (`objects` = glob patterns), while a `defineStack()` host installs the ASSEMBLED body (`objects` = object definitions) — the stage mismatch the comment above names. It is filled by following that ruling one layer up: the API protocol declares `AssembledInstalledPackageSchema` and binds both read responses to `InstalledPackageAtEitherStageSchema`, a union over the two whole CLOSED stage declarations — neither stage widened, and a row belonging to neither still refused. Fillable because `domains/packages-read-delete-response-conformance.test.ts` drives THIS handler and parses the payload it answers on BOTH authoring paths. ⚠️ The declaration is a strict SUBSET of the wire: each row also carries `writable`, this door\'s own computed verdict and not a declared record field, which a declared parse therefore strips — asserted by name in the same file rather than fixed' },
{ route: 'POST /packages', domain: '/packages', disposition: 'sdk', client: 'packages.install' },
{ route: 'GET /packages/:id', domain: '/packages', disposition: 'sdk', client: 'packages.get',
responseSchema: 'GetInstalledPackageResponseSchema',
diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md
index 494d5b646ed..f668ca8f77f 100644
--- a/packages/spec/PROTOCOL_MAP.md
+++ b/packages/spec/PROTOCOL_MAP.md
@@ -166,6 +166,7 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l
| [`contract.zod.ts`](src/api/contract.zod.ts) | | **API Contracts**. Versioned API signatures. |
| [`storage.zod.ts`](src/api/storage.zod.ts) | | **Storage API**. File upload/download endpoints. |
| [`package-api.zod.ts`](src/api/package-api.zod.ts) | | **Package API**. Package lifecycle endpoints (`/api/v1/packages`). |
+| [`package-api-assembled.zod.ts`](src/api/package-api-assembled.zod.ts) | | **Package API — assembled stage**. The installed-package rows at the assembled stage, the two package read responses and the route map; published from `@objectstack/spec/api-assembled` so `/api` does not link the assembled package body. |
---
diff --git a/packages/spec/api-surface/api-assembled.json b/packages/spec/api-surface/api-assembled.json
new file mode 100644
index 00000000000..2311ff88751
--- /dev/null
+++ b/packages/spec/api-surface/api-assembled.json
@@ -0,0 +1,19 @@
+{
+ "description": "Every exported `name (kind)` of one published entry point of @objectstack/spec — the breadth half of the ADR-0059 backward-compatibility gate. Sharded by entry point (#5837) so two PRs touching different entry points never share a file. Reads the BUILT dist/*.d.ts: regenerate with `pnpm --filter @objectstack/spec gen:api-surface` after a real build.",
+ "entry": "./api-assembled",
+ "exports": [
+ "AssembledInstalledPackage (type)",
+ "AssembledInstalledPackageParsed (type)",
+ "AssembledInstalledPackageSchema (const)",
+ "GetInstalledPackageResponse (type)",
+ "GetInstalledPackageResponseParsed (type)",
+ "GetInstalledPackageResponseSchema (const)",
+ "InstalledPackageAtEitherStage (type)",
+ "InstalledPackageAtEitherStageParsed (type)",
+ "InstalledPackageAtEitherStageSchema (const)",
+ "ListInstalledPackagesResponse (type)",
+ "ListInstalledPackagesResponseParsed (type)",
+ "ListInstalledPackagesResponseSchema (const)",
+ "PackageApiContracts (const)"
+ ]
+}
diff --git a/packages/spec/api-surface/api.json b/packages/spec/api-surface/api.json
index 789b68f032f..f2fe07b5ee2 100644
--- a/packages/spec/api-surface/api.json
+++ b/packages/spec/api-surface/api.json
@@ -79,9 +79,6 @@
"AppDefinitionResponseSchema (const)",
"ApproveAiPendingActionResponse (type)",
"ApproveAiPendingActionResponseSchema (const)",
- "AssembledInstalledPackage (type)",
- "AssembledInstalledPackageParsed (type)",
- "AssembledInstalledPackageSchema (const)",
"AuditMetaItemRequest (type)",
"AuditMetaItemRequestSchema (const)",
"AuditMetaItemResponse (type)",
@@ -457,9 +454,6 @@
"GetFlowResponseSchema (const)",
"GetInstalledPackageRequest (type)",
"GetInstalledPackageRequestSchema (const)",
- "GetInstalledPackageResponse (type)",
- "GetInstalledPackageResponseParsed (type)",
- "GetInstalledPackageResponseSchema (const)",
"GetLocalesRequest (type)",
"GetLocalesRequestSchema (const)",
"GetLocalesResponse (type)",
@@ -579,9 +573,6 @@
"InstallPackageResponse (type)",
"InstallPackageResponseSchema (const)",
"InstalledPackage (type)",
- "InstalledPackageAtEitherStage (type)",
- "InstalledPackageAtEitherStageParsed (type)",
- "InstalledPackageAtEitherStageSchema (const)",
"ListAiConversationsRequest (type)",
"ListAiConversationsRequestSchema (const)",
"ListAiConversationsResponse (type)",
@@ -613,9 +604,6 @@
"ListInstalledPackagesRequest (type)",
"ListInstalledPackagesRequestParsed (type)",
"ListInstalledPackagesRequestSchema (const)",
- "ListInstalledPackagesResponse (type)",
- "ListInstalledPackagesResponseParsed (type)",
- "ListInstalledPackagesResponseSchema (const)",
"ListNotificationsRequest (type)",
"ListNotificationsRequestParsed (type)",
"ListNotificationsRequestSchema (const)",
@@ -767,7 +755,6 @@
"OperatorMapping (type)",
"OperatorMappingSchema (const)",
"PROVENANCE_WAIVERS (const)",
- "PackageApiContracts (const)",
"PackageApiErrorCode (const)",
"PackageApiErrorCode (type)",
"PackageExportManifest (type)",
diff --git a/packages/spec/browser-reachable-entries.json b/packages/spec/browser-reachable-entries.json
index c7d8f17394a..85619ddb0f1 100644
--- a/packages/spec/browser-reachable-entries.json
+++ b/packages/spec/browser-reachable-entries.json
@@ -15,6 +15,7 @@
".",
"./ai",
"./api",
+ "./api-assembled",
"./automation",
"./data",
"./identity",
@@ -32,19 +33,19 @@
"_comment": "Entries measured for WEIGHT and left in `unjudged` on purpose, each with the reading that decided it. `unjudged` is a flat list of subpaths, so a member has nowhere to carry a reason — without this map a deliberate non-promotion and a never-examined entry are the same silence, which is the state #17535 was filed against. ⛔ An entry here is NOT a lesser `browserReachable`: this gate still asserts nothing about it. ⛔ An entry NOT here has not been measured, and its absence says nothing about its weight. ⛔ A reading here is a count plus the tree it was taken against — re-measuring REPLACES a row, it never appends to one.",
"./api": {
"verdict": "measured, and stays unjudged",
- "why": "`browserReachable` is a SCHEMA-FREE PROMISE, not a byte budget. `./api`'s vocabulary IS the zod graph — the symbols a consumer reaches there are request/response schemas — so the promotion #17535 offers is not a threshold anyone can set: declared browser-reachable, this entry reds rule 1 on the same commit. Measured by ablation on f962be9d08: 4 problems, `dist/api/index.mjs` and `dist/api/index.js` each linking 'zod' and the undeclared external 'pg-connection-string'. Promotion would first require the entry to stop linking zod — a redesign of a published export surface, not a ledger edit — and `_unjudgedComment` above already reserves promotion to a maintainer decision plus a passing gate. So the honest record is this one.",
+ "why": "`browserReachable` is a SCHEMA-FREE PROMISE, not a byte budget. `./api`'s vocabulary IS the zod graph — the symbols a consumer reaches there are request/response schemas — so promotion is not a threshold anyone can set: declared browser-reachable, this entry reds rule 1 on the same commit. Re-measured by ablation after the #18576 split, at 5f845af5c4: 2 problems, `dist/api/index.mjs` and `dist/api/index.js` each linking 'zod'. The 'pg-connection-string' link the pre-split ablation (f962be9d08, 4 problems) also found is gone with the split, and `./api` no longer carries a `browser` condition. Promotion would still first require the entry to stop linking zod — a redesign of a published export surface, not a ledger edit — and `_unjudgedComment` above reserves promotion to a maintainer decision plus a passing gate. So the honest record is still this one.",
"measured": {
- "what": "the growth #17535 reports, re-derived in isolation: parent 0aa88eb6b0 vs the #17517 merge 9165d5cd4c, both built from source; `./contracts` and `./meta-spelling` measured byte-identical across the pair as controls",
- "entryBundleRaw": "1433893 -> 1867678 bytes (+433785)",
- "entryBundleGzip": "418675 -> 546266 bytes (+127591, +30.5%)",
- "browserBundleOfWholeNamespaceGzip": "273904 -> 330104 bytes (+56200, +20.5%) — this is the axis #17535's +19.4% reproduces on",
- "sourceGraphInputs": "110 -> 159 (+49) — #17535 reports 188 -> 237, a different absolute base and the same delta",
- "narrowestRealConsumerImportGzip": "132121 -> 261221 bytes (+129100, +97.7%) — a browser bundle whose ONLY use of this entry is the two string constants `@object-ui/core` re-exports. The narrower the import, the WORSE the ratio: tree-shaking recovers proportionally less of the new graph than of the old one, so the headline percentage understates what the real consumers pay by roughly 5x.",
- "onMainToday": "f962be9d08: 1914896 raw / 560968 gzip (node condition), 1912968 / 560390 (browser condition), 160 source graph inputs"
+ "what": "the #18576 split (maintainer ruling, letter B: the assembled-package declarations leave `./api` for `./api-assembled`), measured across its own branch: base fc6ddb87a4 vs head 5f845af5c4, both built from source in one worktree; bundles by esbuild 0.28.2, platform browser, conditions browser+import, minified, gzip -9",
+ "entryBundleRaw": "2082098 -> 1597172 bytes (esm)",
+ "entryBundleGzip": "612813 -> 469795 bytes (esm, -23.3%) — one bundle now serves both conditions",
+ "sourceGraphInputs": "171 -> 120 modules (esbuild metafile over src/api/index.ts); stack.zod, the datasource declaration and all nine driver modules are gone from the graph",
+ "narrowestRealConsumerImportGzip": "311124 -> 166529 bytes (-46.5%) — objectui `@object-ui/core` utils/column-sortability.ts bundled AS-IS (its only imports are two string constants and two types from this entry). The same probe on metadata-client.ts's one value import reads 311182 -> 166616 (-46.5%). The dynamic `import()` in app-shell clientValidation.ts pulls the whole namespace and reads 391171 -> 327732 (-16.2%).",
+ "wholeNamespaceGzip": "387421 -> 324465 bytes (-16.3%)",
+ "history": "the growth #17535 reported — parent 0aa88eb6b0 vs the #17517 merge 9165d5cd4c, narrowest consumer 132121 -> 261221 gzip — is the regression this split removes; the gap between 132121 and today's 166529 is the rest of `./api` growing since, not the assembled tree"
},
- "objectuiLeg": "MEASURED, and it was the half #17535 could not reach. objectui at dda8f3815d — ⚠️ this session's attached checkout, NOT the pinned `.objectui-sha` 53ded82bf7, which is absent from that shallow clone and was ⛔ not fetched or bumped to take a reading — value-imports `@objectstack/spec/api` from 6 browser-shipped non-test source files: `@object-ui/core` utils/column-sortability.ts, `@object-ui/data-objectstack` metadata-client.ts, `@object-ui/app-shell` views/metadata-admin/clientValidation.ts, `@object-ui/plugin-chatbot` usePendingActions.ts, `@object-ui/react` utils/error-message.ts, `@object-ui/types` data.ts. So the bytes are paid by a real downstream browser consumer, not hypothetically. Control for the scan: `./contracts` reports 9 such files, matching the sites this file's own `browserReachable` entry already names.",
+ "objectuiLeg": "MEASURED at objectui 62597c5880, which IS the pinned `.objectui-sha` (the pre-split reading took dda8f3815d, not the pin). Six browser-shipped non-test files import `@objectstack/spec/api`: three VALUE imports — `@object-ui/core` utils/column-sortability.ts, `@object-ui/data-objectstack` metadata-client.ts, `@object-ui/app-shell` views/metadata-admin/clientValidation.ts (dynamic) — and three type-only ones that cost a bundle nothing — `@object-ui/plugin-chatbot` usePendingActions.ts, `@object-ui/react` utils/error-message.ts, `@object-ui/types` data.ts. None imports a name the split moved, so all six keep resolving from `./api` unchanged; `@object-ui/types` also re-exports this entry as a type-only `API` namespace, and nothing in objectui reads a moved name through it.",
"cloudLeg": "NOT MEASURED. The `cloud` repo was not attached to the session that took these readings. ⛔ Its absence is not a zero — a reading that did not cover a population vouches only for the part it did cover.",
- "notAnOutlier": "⚠️ `./api` is one of fifteen unjudged entries and the measurement does NOT single it out. By the same scan, objectui browser-shipped source value-imports twelve of the fifteen, several of them harder than this one: `./ui` 48 files, `./data` 30, `./kernel` 12, against `./api`'s 6; `./kernel` is the heavier bundle (1544209 raw / 457761 gzip on f962be9d08). Only `./marketplace`, `./qa` and `./studio` are at zero. ⛔ This row is therefore a record about `./api` alone and says NOTHING about the other fourteen — it is not a clean bill for them, and their silence is still the untested kind.",
+ "notAnOutlier": "⚠️ `./api` is one of sixteen unjudged entries and the measurement does NOT single it out. The weight scan below was taken when `unjudged` held fifteen — at f962be9d08 / objectui dda8f3815d, before #18576 split `./api-assembled` off `./api` — and was not re-taken for the split: objectui browser-shipped source value-imported twelve of those fifteen, several of them harder than this one: `./ui` 48 files, `./data` 30, `./kernel` 12, against `./api`'s 6; `./kernel` is the heavier bundle (1544209 raw / 457761 gzip on f962be9d08). Only `./marketplace`, `./qa` and `./studio` were at zero. The sixteenth, `./api-assembled`, was not in that scan; at the pinned objectui 62597c5880 no source file names it. ⛔ This row is therefore a record about `./api` alone and says NOTHING about the other fifteen — it is not a clean bill for them, and their silence is still the untested kind.",
"whatWouldChangeThis": "Nothing in this gate has a weight axis for a schema-bearing entry — there is no byte threshold in it, for any entry, judged or not. Giving `./api` one means a NEW rule, which is a different decision from the one this row records and belongs to whoever takes that decision."
}
},
diff --git a/packages/spec/export-origins/api-assembled.json b/packages/spec/export-origins/api-assembled.json
new file mode 100644
index 00000000000..584038cb43d
--- /dev/null
+++ b/packages/spec/export-origins/api-assembled.json
@@ -0,0 +1,19 @@
+{
+ "description": "Which SOURCE DECLARATION each name exported by one public entry point of @objectstack/spec resolves to, after its alias chain is unwound: `# ()`. Two exports share an origin string iff they are the same declaration — so equal origins across two entries are a harmless re-export, and different origins under one name are the #4411 dual-source trap. Generated from src/ (no build needed) and read by the export-surface pin tests, which compare against it instead of each building their own ts.createProgram — that was ~55s of compilation per CI lap and a non-deterministic timeout that ejected unrelated PRs from the merge queue (#4796). Sharded by entry point (#5837) so two retirement PRs never share a file. Carries NO line numbers: the pins asserted the line as `\\d+`, and recording it would rewrite this artifact on every edit that shifts a line in any .zod.ts. Regenerate with `pnpm --filter @objectstack/spec gen:export-origins` and read the diff.",
+ "entry": "./api-assembled",
+ "exports": {
+ "AssembledInstalledPackage": "src/api/package-api-assembled.zod.ts#AssembledInstalledPackage (type)",
+ "AssembledInstalledPackageParsed": "src/api/package-api-assembled.zod.ts#AssembledInstalledPackageParsed (type)",
+ "AssembledInstalledPackageSchema": "src/api/package-api-assembled.zod.ts#AssembledInstalledPackageSchema (const)",
+ "GetInstalledPackageResponse": "src/api/package-api-assembled.zod.ts#GetInstalledPackageResponse (type)",
+ "GetInstalledPackageResponseParsed": "src/api/package-api-assembled.zod.ts#GetInstalledPackageResponseParsed (type)",
+ "GetInstalledPackageResponseSchema": "src/api/package-api-assembled.zod.ts#GetInstalledPackageResponseSchema (const)",
+ "InstalledPackageAtEitherStage": "src/api/package-api-assembled.zod.ts#InstalledPackageAtEitherStage (type)",
+ "InstalledPackageAtEitherStageParsed": "src/api/package-api-assembled.zod.ts#InstalledPackageAtEitherStageParsed (type)",
+ "InstalledPackageAtEitherStageSchema": "src/api/package-api-assembled.zod.ts#InstalledPackageAtEitherStageSchema (const)",
+ "ListInstalledPackagesResponse": "src/api/package-api-assembled.zod.ts#ListInstalledPackagesResponse (type)",
+ "ListInstalledPackagesResponseParsed": "src/api/package-api-assembled.zod.ts#ListInstalledPackagesResponseParsed (type)",
+ "ListInstalledPackagesResponseSchema": "src/api/package-api-assembled.zod.ts#ListInstalledPackagesResponseSchema (const)",
+ "PackageApiContracts": "src/api/package-api-assembled.zod.ts#PackageApiContracts (const)"
+ }
+}
diff --git a/packages/spec/export-origins/api.json b/packages/spec/export-origins/api.json
index 05b68eced5d..dc2f25bd619 100644
--- a/packages/spec/export-origins/api.json
+++ b/packages/spec/export-origins/api.json
@@ -74,9 +74,6 @@
"AppDefinitionResponseSchema": "src/api/metadata.zod.ts#AppDefinitionResponseSchema (const)",
"ApproveAiPendingActionResponse": "src/api/protocol.zod.ts#ApproveAiPendingActionResponse (type)",
"ApproveAiPendingActionResponseSchema": "src/api/protocol.zod.ts#ApproveAiPendingActionResponseSchema (const)",
- "AssembledInstalledPackage": "src/api/package-api.zod.ts#AssembledInstalledPackage (type)",
- "AssembledInstalledPackageParsed": "src/api/package-api.zod.ts#AssembledInstalledPackageParsed (type)",
- "AssembledInstalledPackageSchema": "src/api/package-api.zod.ts#AssembledInstalledPackageSchema (const)",
"AuditMetaItemRequest": "src/api/protocol.zod.ts#AuditMetaItemRequest (type)",
"AuditMetaItemRequestSchema": "src/api/protocol.zod.ts#AuditMetaItemRequestSchema (const)",
"AuditMetaItemResponse": "src/api/protocol.zod.ts#AuditMetaItemResponse (type)",
@@ -435,9 +432,6 @@
"GetFlowResponseSchema": "src/api/automation-api.zod.ts#GetFlowResponseSchema (const)",
"GetInstalledPackageRequest": "src/api/package-api.zod.ts#GetInstalledPackageRequest (type)",
"GetInstalledPackageRequestSchema": "src/api/package-api.zod.ts#GetInstalledPackageRequestSchema (const)",
- "GetInstalledPackageResponse": "src/api/package-api.zod.ts#GetInstalledPackageResponse (type)",
- "GetInstalledPackageResponseParsed": "src/api/package-api.zod.ts#GetInstalledPackageResponseParsed (type)",
- "GetInstalledPackageResponseSchema": "src/api/package-api.zod.ts#GetInstalledPackageResponseSchema (const)",
"GetLocalesRequest": "src/api/protocol.zod.ts#GetLocalesRequest (type)",
"GetLocalesRequestSchema": "src/api/protocol.zod.ts#GetLocalesRequestSchema (const)",
"GetLocalesResponse": "src/api/protocol.zod.ts#GetLocalesResponse (type)",
@@ -553,9 +547,6 @@
"InstallPackageResponse": "src/kernel/package-registry.zod.ts#InstallPackageResponse (type)",
"InstallPackageResponseSchema": "src/kernel/package-registry.zod.ts#InstallPackageResponseSchema (const)",
"InstalledPackage": "src/kernel/package-registry.zod.ts#InstalledPackage (type)",
- "InstalledPackageAtEitherStage": "src/api/package-api.zod.ts#InstalledPackageAtEitherStage (type)",
- "InstalledPackageAtEitherStageParsed": "src/api/package-api.zod.ts#InstalledPackageAtEitherStageParsed (type)",
- "InstalledPackageAtEitherStageSchema": "src/api/package-api.zod.ts#InstalledPackageAtEitherStageSchema (const)",
"ListAiConversationsRequest": "src/api/protocol.zod.ts#ListAiConversationsRequest (type)",
"ListAiConversationsRequestSchema": "src/api/protocol.zod.ts#ListAiConversationsRequestSchema (const)",
"ListAiConversationsResponse": "src/api/protocol.zod.ts#ListAiConversationsResponse (type)",
@@ -587,9 +578,6 @@
"ListInstalledPackagesRequest": "src/api/package-api.zod.ts#ListInstalledPackagesRequest (type)",
"ListInstalledPackagesRequestParsed": "src/api/package-api.zod.ts#ListInstalledPackagesRequestParsed (type)",
"ListInstalledPackagesRequestSchema": "src/api/package-api.zod.ts#ListInstalledPackagesRequestSchema (const)",
- "ListInstalledPackagesResponse": "src/api/package-api.zod.ts#ListInstalledPackagesResponse (type)",
- "ListInstalledPackagesResponseParsed": "src/api/package-api.zod.ts#ListInstalledPackagesResponseParsed (type)",
- "ListInstalledPackagesResponseSchema": "src/api/package-api.zod.ts#ListInstalledPackagesResponseSchema (const)",
"ListNotificationsRequest": "src/api/protocol.zod.ts#ListNotificationsRequest (type)",
"ListNotificationsRequestParsed": "src/api/protocol.zod.ts#ListNotificationsRequestParsed (type)",
"ListNotificationsRequestSchema": "src/api/protocol.zod.ts#ListNotificationsRequestSchema (const)",
@@ -738,7 +726,6 @@
"OperatorMapping": "src/api/query-adapter.zod.ts#OperatorMapping (type)",
"OperatorMappingSchema": "src/api/query-adapter.zod.ts#OperatorMappingSchema (const)",
"PROVENANCE_WAIVERS": "src/api/error-code-ledger.zod.ts#PROVENANCE_WAIVERS (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)",
diff --git a/packages/spec/llms.txt b/packages/spec/llms.txt
index 361af0f460a..f97267a17e6 100644
--- a/packages/spec/llms.txt
+++ b/packages/spec/llms.txt
@@ -77,7 +77,7 @@ const query = {
---
-## 3. Schema Inventory by Domain (202 schemas)
+## 3. Schema Inventory by Domain (203 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 | 34 | Auth, Cache, Compliance, Dev Login, Encryption, HTTP Server, License, Logging, Metrics |
| kernel | 31 | Plugin, Manifest, Events (6 sub-modules), Feature, Context, Package Registry |
| data | 30 | Object, Field, Query, Filter, Driver (SQL/NoSQL/Memory/Mongo/Postgres), Cube |
-| api | 30 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope, Package Lifecycle |
+| api | 31 | Endpoint, REST Server, Discovery, OData, Batch, WebSocket, Response Envelope, Package Lifecycle, Package API (assembled stage) |
| ui | 18 | View, App, Action, Dashboard, Page, Chart, Component, Animation |
| automation | 14 | Flow, Approval, BPMN Interop, Control Flow, State Machine, Webhook, Schedule Organization |
| shared | 15 | Enums, HTTP, Identifiers, Mapping, Metadata Types, Connector Auth, Retry Policy, Value Domain, Epoch Instant (EpochMs), Duration (DurationMs / DurationSeconds) |
@@ -130,6 +130,11 @@ here is verifiable from the installed package.
- `ApiEndpointSchema`: REST endpoints.
- `ResponseEnvelopeConfigSchema`, `ApiErrorSchema`: Request/Response envelopes.
- `DiscoverySchema`: Service discovery.
+- NOT here: the installed-package read responses (`ListInstalledPackagesResponseSchema`,
+ `GetInstalledPackageResponseSchema`), the rows they carry (`AssembledInstalledPackageSchema`,
+ `InstalledPackageAtEitherStageSchema`) and `PackageApiContracts` embed the assembled package
+ body, so they import from `@objectstack/spec/api-assembled` — which keeps `/api` light enough
+ for browser code.
---
diff --git a/packages/spec/package.json b/packages/spec/package.json
index 96c8bc10351..d08edbd1135 100644
--- a/packages/spec/package.json
+++ b/packages/spec/package.json
@@ -108,23 +108,33 @@
}
},
"./api": {
+ "import": {
+ "types": "./dist/api/index.d.mts",
+ "default": "./dist/api/index.mjs"
+ },
+ "require": {
+ "types": "./dist/api/index.d.ts",
+ "default": "./dist/api/index.js"
+ }
+ },
+ "./api-assembled": {
"browser": {
"import": {
- "types": "./dist/api/index.d.mts",
- "default": "./dist/browser/api/index.mjs"
+ "types": "./dist/api-assembled/index.d.mts",
+ "default": "./dist/browser/api-assembled/index.mjs"
},
"require": {
- "types": "./dist/api/index.d.ts",
- "default": "./dist/browser/api/index.js"
+ "types": "./dist/api-assembled/index.d.ts",
+ "default": "./dist/browser/api-assembled/index.js"
}
},
"import": {
- "types": "./dist/api/index.d.mts",
- "default": "./dist/api/index.mjs"
+ "types": "./dist/api-assembled/index.d.mts",
+ "default": "./dist/api-assembled/index.mjs"
},
"require": {
- "types": "./dist/api/index.d.ts",
- "default": "./dist/api/index.js"
+ "types": "./dist/api-assembled/index.d.ts",
+ "default": "./dist/api-assembled/index.js"
}
},
"./ui": {
diff --git a/packages/spec/scripts/build-declaration-map.ts b/packages/spec/scripts/build-declaration-map.ts
index 332f5a64d13..9bce15cf8d6 100644
--- a/packages/spec/scripts/build-declaration-map.ts
+++ b/packages/spec/scripts/build-declaration-map.ts
@@ -83,6 +83,7 @@
*/
import ts from 'typescript';
+import { SPLIT_ENTRIES } from './lib/split-entries';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -367,6 +368,15 @@ export function buildDeclarationMap(pkgDir: string): ComposedMap {
const originsByCategory = new Map>(
originShards.map((s) => [s.name, s.doc.exports]),
);
+ // [#18576] A SPLIT entry publishes part of its HOME category's protocol
+ // (`lib/split-entries.ts`): the schema manifest keys its defs
+ // `/`, so they resolve against the home's origins joined with the
+ // split entry's. The two entries share no name (the route-ledger resolver in
+ // packages/client pins that), so the join cannot shadow either side.
+ for (const [split, { home }] of Object.entries(SPLIT_ENTRIES)) {
+ const extra = originsByCategory.get(split);
+ if (extra) originsByCategory.set(home, { ...(originsByCategory.get(home) ?? {}), ...extra });
+ }
const defKeys = manifestShards.flatMap((s) => s.doc.schemas);
// Anti-vacuity floor, same instinct as build-export-origins' entry-point
diff --git a/packages/spec/scripts/build-docs.ts b/packages/spec/scripts/build-docs.ts
index 0da1d529d89..4f9dbd55259 100644
--- a/packages/spec/scripts/build-docs.ts
+++ b/packages/spec/scripts/build-docs.ts
@@ -60,6 +60,7 @@ import {
type ZodFileInput,
} from './lib/schema-index';
import { schemaNameFromExportKey } from './lib/schema-name';
+import { formatSplitEntryCoverage, splitEntryCoverage } from './lib/split-entries';
import { renderSchemaSection } from './lib/schema-section';
import { API_SURFACE_DIR_NAME, readApiSurfaceFrom } from './lib/sharded-artifacts';
@@ -367,6 +368,14 @@ function groupSchemasByPage(): Map`.
+import * as ApiAssembled from '../src/api-assembled';
import * as Automation from '../src/automation';
import * as Contracts from '../src/contracts';
import * as Data from '../src/data';
@@ -150,7 +155,7 @@ import * as UI from '../src/ui';
// packages/spec/src/index.ts). Build subpath-by-subpath instead so every
// category folder under json-schema/ gets populated.
const Protocol: Record> = {
- AI, API, Automation, Contracts, Data, Identity, Integration,
+ AI, API: { ...API, ...ApiAssembled }, Automation, Contracts, Data, Identity, Integration,
Kernel, Marketplace, QA, Security, Shared, Studio, System, UI,
};
diff --git a/packages/spec/scripts/export-origins.test.ts b/packages/spec/scripts/export-origins.test.ts
index 03104d24525..353990f7305 100644
--- a/packages/spec/scripts/export-origins.test.ts
+++ b/packages/spec/scripts/export-origins.test.ts
@@ -49,6 +49,7 @@ const ENTRY_NAMESPACES: ReadonlyArray<[string, () => Promise