Skip to content

Commit 0ecc2c4

Browse files
feat(metadata-protocol): ensure door for a read-only base's paired customization package (#28512)
<!-- claude-projects-attribution: {"github_login":"os-zhuang"} --> _Requested by **Sam** · [project thread](https://claude.ai/code/project/chan_01LjZwNRFiG2fkp8SNf6rHNx?thread=cmsg_01LjZwNRFiG2fkp8SNf6rHNxSrmC56pjkzCzxHmbpYUu4i)_ Fixes #28407 Clause-②: yes (narrowing) The new route `POST /api/v1/packages/:id/ensure-customization` and its two schemas (`EnsureCustomizationPackageRequestSchema`, `EnsureCustomizationPackageResponseSchema`) widen the surface, and `POST /api/v1/packages` and the duplicate door's target narrow theirs: a pair-shaped id (`BASE.custom`) is refused unless its manifest declares the pair namespace and a dependency on the base. Metadata-protocol: added Skills-docs: none — no skill under `skills/**` teaches the `/packages` doors or `client.packages`; the docs half is in this diff (the generated API reference for the two schemas, and the packages row of `content/docs/api/client-sdk.mdx`). ## 维护者速读 - 改了什么:新增 `POST /api/v1/packages/:id/ensure-customization` 与 SDK `client.packages.ensureCustomization`,为已安装的只读基础包答出它的配对定制包 `BASE.custom`,缺则经安装原语创建(命名空间 `BASE_NS_custom`,依赖基础包 `^MAJOR`);安装门、安装原语和复制门拒收缺这两项的配对形 id。 - 为什么改:ADR-0146 D3 的配对定制包此前没有创建入口,手工安装会从 id 派生出 `custom` 命名空间,也不声明对基础包的依赖。 - 风险与回滚:新路由是纯新增;收窄只拦 id 形如 `BASE.custom` 且缺显式命名空间或缺依赖的安装与复制,导出的完整配对照常安装。回滚即 revert 本 PR,无数据迁移。 - 先看:`packages/metadata-protocol/src/protocol-customization-package.ts`(`ensurePairOnce` 与同一配对的单飞等待)、`packages/runtime/src/domains/packages.ts`(新门的鉴权顺序与 `POST /packages` 的配对拒收)、`packages/metadata-protocol/src/protocol-package-install.ts`(安装原语里的同一谓词)。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/packages-ensure-customization.test.ts`,预期 `Tests 35 passed (35)`。 - 需要你做的:无,席位落地。 ## What changes - **`@objectstack/spec`** (`api/package-lifecycle.zod.ts`): `EnsureCustomizationPackageRequestSchema` (strict, one key `basePackageId`) and `EnsureCustomizationPackageResponseSchema` (six required fields: `basePackageId`, `packageId`, `namespace`, `dependencyRange`, `created`, `writable`), with their `z.input` / `z.infer` aliases. No existing shape moves. The authorable surface gains exactly these 7 keys (`packages/spec/authorable-surface/api.json`): `EnsureCustomizationPackageRequest:basePackageId`, and `EnsureCustomizationPackageResponse:` `basePackageId`, `created`, `dependencyRange`, `namespace`, `packageId`, `writable`; the JSON-schema manifest gains the two schema names. That move is why the kind is `added` and spec is `minor` (see Acceptance notes). - **`@objectstack/metadata-protocol`**: a new module `protocol-customization-package.ts` holds the body of the new class method `ensureCustomizationPackage` (the protocol.ts split pattern) and the predicate `customizationPairManifestRefusal`, exported from the package index. - Create: the pair id, namespace and range come from the N-08 helpers in `@objectstack/spec/kernel`, never a local copy. The pair is installed through `this.installPackage` with manifest id `BASE.custom`, name `BASE_NAME (custom)`, version `0.1.0`, type `module`, explicit `namespace`, and `dependencies` naming the base at `^MAJOR`. - Idempotent: an existing pair that declares the namespace and a dependency on the base is answered unchanged (`created: false`, its own declared range). One that lacks either is refused `409` and never adopted or rewritten. - Concurrency: calls for the same pair on the same registry are single-flighted. A call that arrives while another is creating the pair waits for it to settle and then judges the registry afresh, so it never answers for a pair whose persist then fails and is undone. - Refusals, each leaving no package: `400` malformed request or base that is itself a pair; `404` base not installed; `409` writable base, base with no namespace, base namespace too long for the `_custom` suffix (nothing truncated or hashed), non-SemVer base version, or an installed pair lacking namespace or dependency; `422 NAMESPACE_CONFLICT` when another package owns the pair namespace (the registry's code), including an owner the registry would read as a co-owner. - The predicate: an install whose id is pair-shaped must declare the explicit pair namespace (the exact one when the base is installed) and a dependency on the base, or it is refused `400` naming the ensure door. `installPackage` asks it (every in-process caller, and the duplicate's copy record), and `duplicatePackage` asks it before reading a row. - **`@objectstack/runtime`**: the route in `domains/packages.ts`, gated as the install door is: `manage_metadata`, then the walled refusal (only the platform operator under `isolated` / `group`), then the closed query set (empty), then a body check (no body or `{}`; a body with keys is `400`, never ignored). `201` when created, `200` when present; `DEPENDENCY_MISSING` keeps its facts. `POST /packages` asks the predicate before either install limb. The route is mounted in `dispatcher-plugin.ts` and has a ledger row in `route-ledger.ts` (`disposition: 'sdk'`, client `packages.ensureCustomization`, response schema named); the census sentence reads 114. - **`@objectstack/client`**: `packages.ensureCustomization(baseId)` returns `EnsureCustomizationPackageResponse`; `content/docs/api/client-sdk.mdx` counts 18 packages methods. ## Tests - `packages/runtime/src/domains/packages-ensure-customization.test.ts` (new, 35 tests) over a real `ObjectStackProtocolImplementation` and `SchemaRegistry`, with a recording `sys_packages` publish double. It covers: create `com.acme.crm.custom` with namespace `crm_custom` (not `custom`) and `^3`, `201`, parsed through the response schema, persisted once, read back by `GET`; a code-provided base; re-ensure `200 created:false` with no write; an exported valid pair answered unchanged; every refusal with its ADR-0112 envelope and no package or write left; walled `isolated` and `group`, where the tenant admin gets `403` and the operator `201`; `single` gives `201`; the capability gate; two real concurrent calls give one `201`, one `200`, one write and one pair; the persist-failure race; `POST /packages` on both arms (three refused shapes, an exported pair that declares both installs, a non-pair id untouched); an exported pair whose base is absent gets `422 DEPENDENCY_MISSING`; the install primitive refuses an in-process pair manifest; the duplicate into a pair id is `400` before any row is read. - Door-family sweeps gain the new door: walled refusal (fourteen doors), capability gate, closed query, and the `501` probe in handle-typing. - Spec conformance in `api/package-lifecycle.test.ts`: the strict request refuses `namespace` and `dependencyRange`, and the response capture parses and requires every member. Client: URL, method, no body, URL-encoding and the answer; return-type precision pins `EnsureCustomizationPackageResponse`. - The card's two named tests stay green: `packages/spec/src/kernel/namespace-prefix.test.ts` (10/10) and `packages/metadata-protocol/src/protocol.duplicate-package-target-namespace.test.ts` (13/13). <details><summary>Ablations: 19 mutations, each red, each restored with blob equal to HEAD</summary> Run through `scripts/ablation-replace.mjs` in WRAP mode. Every leg printed `ok mutation landed` and `ok restored: blob == HEAD … git diff HEAD is empty`. The metadata-protocol legs rebuilt its dist first. | id | mutation | red tests | |---|---|---| | A1 | ensure route: walled refusal ignored | walled 403/201 (isolated, group), walled sweep (2) | | A2 | ensure route: body check off | body carrying keys | | A3 | ensure route: query check removed | query refused; closed-query sweep (3) | | A4 | ensure route: no 201 on create | 10, every create path | | A5 | `POST /packages`: pair gate off | fallback arm, three refused shapes (the composed arm stays refused by the install primitive) | | A6 | ensure route: capability gate removed | own capability test; capability sweep (2) | | B1 | created pair namespace `custom` | 10 (the install primitive refuses it, 400) | | B2 | created pair range `>=0.0.0` | create, re-ensure | | B3 | existing pair not recognised | re-ensure, exported pair, stray pair, concurrency | | B4 | writable base admitted | writable refusal | | B5 | namespace owner check off | squatter, co-owner | | B6 | single-flight wait removed | persist-failure race only | | B7 | install primitive predicate off | in-process install refusal | | B8 | duplicate predicate off | duplicate before any row | | B9 | pair-of-pair sentence | base itself a pair | | B10 | base pair-namespace check off | no namespace, too long | | B11 | existing pair lack check off | stray pair | | B12 | SemVer check off | non-SemVer base | | B13 | not-installed check off | base not installed | </details> ## Acceptance notes - **Declaration measured.** `check-changeset-no-major`, driven with a `pull_request` payload declaring `Metadata-protocol: none`, exits 1 because the two new schemas move `packages/spec/authorable-surface/api.json` (the 7 keys above). With `added` and `@objectstack/spec` at `minor` (metadata-protocol, runtime and client `patch`) it exits 0, and `check-adr-0087-registration` exits 0, verifying `not-required (runtime-interface-only packages/metadata-protocol/src/protocol.ts#ObjectStackProtocolImplementation)`. - **OpenAPI: nothing to regenerate.** `packages/spec/json-schema/openapi.json` is gitignored and its generator (`build-openapi.ts`) emits only nine fixed contract schemas and no `paths`. The served route section is built at serve time by `packages/rest` from its route manager, so no committed artifact names a route. - **N-08 consequence.** `customizationBasePackageId` reads any id ending in `.custom` as a pair, so a package named `com.acme.custom` is now the pair of `com.acme` at the install doors and needs both declarations. This follows from the ruled id rule, and is not new policy here. - **Prerelease base, read and not tested.** A base at a prerelease version gets `^MAJOR`, and ADR-0141 D2's check does not admit a prerelease, so the pair's own install would be `422 DEPENDENCY_MISSING` at the ensure door (the route keeps the facts). - **Existing pair range.** The ensure door checks that an existing pair declares a dependency on the base, not that its range admits the installed base; ADR-0141 D2 judged that at the pair's install. - **Concurrency scope.** The single-flight is per process and per registry. Two nodes ensuring the same pair at once fall to the `sys_packages` key; that was not measured. The plain two-call test is green with or without the single-flight (B6 reddens only the persist-failure race), which is why both tests exist. - **Ablation evidence for the B legs.** `ablation-dist-preflight.mjs` read exit 1 on the marker for every B leg, because esbuild folds the planted `void "MARKER"` to `void 0`. The mutations did reach dist: every B leg went red with the runtime source untouched, and the emitted guard read `if (!base && (void 0, false))` for B13. - **Outside the literal file surface.** `packages/runtime/src/dispatcher-plugin.ts` (the mount), `packages/runtime/src/route-ledger.ts` (row and census), `packages/client/src/index.ts` and two client tests (the SDK the card asks for), four door-family sweep tests, and `content/docs/api/client-sdk.mdx`. - **Serial with #28414.** `origin/main` had not moved from the base when the gates ran, so there was nothing to rebase. ## Verification <details><summary>Builds, lint and type checks</summary> - `pnpm build` (full workspace, under the verify lock): `VERDICT command-exit 0`, 116 tasks. - `pnpm lint`: exit 0. - Test-layer type checks (`scripts/check-test-typecheck.mts`): client 0, runtime 0, spec 0. The runtime check first found one error in `domains/packages.ts` (the predicate's lookup typed narrower than `registry.getPackage`); the predicate now takes `(id) => unknown`. </details> <details><summary>Derived gates: 108 run, 108 exit 0</summary> `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 108 commands; each ran once with stdin closed and was recorded as `COMMAND :: exit CODE`. `--ran` reports `108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED`. One was red on first run, `check:route-ledger-census` (113 vs 114 rows), and is fixed by its `--fix`. After the type fix, these were re-run on the final source and exit 0: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`, `check:type-check-coverage`, `check:nul-bytes`, `check:dispatcher-error-vocabulary`, `check:undeclared-dep-imports`, `check:route-ledger-census`. </details> <details><summary>Dogfood battery</summary> Once, on the final head `33590a408e75cead7e14e9d4ddcd7cdf38fcea97`, after rebuilding the runtime dist: `bash scripts/pm/os-verify-lock.sh -c "pnpm --filter @objectstack/dogfood test"` (no shard, no path). - `os-verify-lock: VERDICT command-exit 0 · held the lock 1229s (20m29s) · waited 0s` - `Test Files 363 passed | 3 skipped (366)` - `Tests 2926 passed | 28 skipped (2954)` - No failures. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QUs3BtJ6E3jM8dgVzLbpGi --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a52191f commit 0ecc2c4

29 files changed

Lines changed: 1285 additions & 20 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': patch
4+
'@objectstack/runtime': patch
5+
'@objectstack/client': patch
6+
---
7+
8+
feat(metadata-protocol): an ensure door for a base's paired customization package (ADR-0146 D3)
9+
10+
Clause-②: yes (narrowing)
11+
Metadata-protocol: added
12+
13+
The new route `POST /api/v1/packages/:id/ensure-customization`, with its request and response schemas (`EnsureCustomizationPackageRequestSchema`, `EnsureCustomizationPackageResponseSchema`), widens the surface, and `POST /api/v1/packages` and the duplicate door's target narrow theirs: a package id shaped like a pair (`<base>.custom`) is refused unless its manifest carries what the ensure door writes.
14+
15+
<!-- adr-0087: not-required (runtime-interface-only packages/metadata-protocol/src/protocol.ts#ObjectStackProtocolImplementation) The narrowing is a refusal added to the install primitive and the duplicate verb of this runtime class, and to the HTTP install door that fronts them: an install whose id is a paired customization package id must declare the explicit pair namespace and a dependency on the base. No Zod schema, spec contract or stored metadata shape changes, and no stored row is reread differently: a pair already in `sys_packages` replays at boot as stored. So `objectstack migrate meta` has nothing to rewrite. -->
16+
17+
**What it does.** The paired customization package of a read-only base holds the changes a customer makes to that base. `POST /api/v1/packages/:id/ensure-customization` answers that pair, `<base>.custom`, and creates it when it is absent. It is created through the same install primitive `POST /api/v1/packages` uses, with an explicit namespace (the base's namespace followed by `_custom`, so `crm` gives `crm_custom`), a dependency on the base at the major of the base's version (`^3` for `3.4.1`), version `0.1.0` and type `module`. The answer names the base, the pair, its namespace, its dependency range, whether this call created it (`201` when created, `200` when it was already there) and whether it is writable. Asking again changes nothing. Two calls at once leave one pair, and a call that arrives while another is creating the pair waits for it and answers from what it left.
18+
19+
**What it refuses, leaving nothing behind.** A base that is not installed (`404`). A base that is writable, because it is customized directly, a base that declares no namespace, a base whose namespace is too long to take the `_custom` suffix (nothing is truncated or hashed), and a base whose version is not SemVer (`409`). A base that is itself a pair (`400`). A pair namespace another package already owns (`422 NAMESPACE_CONFLICT`, the code the registry stamps for the same condition). An installed `<base>.custom` that lacks the namespace or the dependency (`409`): it is never adopted or rewritten. The caller is judged as at the install door: `manage_metadata` under `single`, and under `isolated` or `group` only the platform operator, so a tenant administrator gets `403`.
20+
21+
**The narrowing.** `POST /api/v1/packages`, the install primitive and the duplicate door now refuse, with `400`, a package whose id is a pair id but whose manifest lacks the explicit pair namespace or a dependency on the base. The refusal names the ensure door. An exported pair that declares both still installs, and the dependency check then judges its range against the base installed there.
22+
23+
**SDK.** `client.packages.ensureCustomization(baseId)` returns `EnsureCustomizationPackageResponse`.
24+
25+
What to do: nothing for an existing stack. A pair is created through the ensure door, or installed from an export that declares both its namespace and its dependency on the base.

‎content/docs/api/client-sdk.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ The `@objectstack/client` SDK aims to implement the ObjectStack API protocol spe
164164
| **meta** | ✅ | 18 | Metadata read/write, published versions & drafts (ADR-0033), per-item publish/rollback/diff, FSM introspection (ADR-0020), diagnostics, references, audit trail, book trees (ADR-0046) |
165165
| **data** | ✅ | 12 | CRUD & query operations, record clone, streaming export (M10.9) |
166166
| **auth** | ✅ | 5 | Authentication & user management |
167-
| **packages** | ✅ | 17 | Package lifecycle: install/enable, drafts (ADR-0033), commits & rollback (ADR-0067), export/duplicate (ADR-0070) |
167+
| **packages** | ✅ | 18 | Package lifecycle: install/enable, drafts (ADR-0033), commits & rollback (ADR-0067), export/duplicate (ADR-0070), a read-only base's paired customization package (ADR-0146) |
168168
| **analytics** | ✅ | 4 | Analytics queries + semantic-layer dataset query (ADR-0021) |
169169
| **automation** | ✅ | 18 | Flow CRUD, trigger/execute, runs, screen-flow resume, descriptor/status registries |
170170
| **actions** | ✅ | 2 | Server-registered action handlers (`engine.registerAction`) |

‎content/docs/references/api/package-lifecycle.mdx‎

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@ through the `{ success, data }` envelope (`http-dispatcher.ts`), so each
2222
schema declares the `data` payload, envelope-free — the same convention as
2323
`PublishPackageDraftsResponseSchema` and its ledger row.
2424

25-
One pair is the exception: `ImportUnmanagedPackageRequestSchema` /
25+
Two pairs are the exception, each with a request half its producer PARSES
26+
rather than a transcription: `ImportUnmanagedPackageRequestSchema` /
2627
`ImportUnmanagedPackageResponseSchema` (ADR-0131 D6) declare a protocol verb
27-
no route serves yet, and the request half is parsed by its producer rather
28-
than transcribed — see their own docblocks.
28+
no route serves yet, and `EnsureCustomizationPackageRequestSchema` /
29+
`EnsureCustomizationPackageResponseSchema` (ADR-0146 D3) declare the ensure
30+
door of a base's paired `.custom` package — see their own docblocks.
2931

3032
`packages.publish`'s contract is NOT here: its producer
3133
(`MetadataManager.publishPackage`) already has an exact published schema,
@@ -48,8 +50,8 @@ authored after that retirement per the ruling's sequencing (3A).
4850
## TypeScript Usage
4951

5052
```typescript
51-
import { DiscardPackageDraftsResponseSchema, DuplicatePackageResponseSchema, ImportUnmanagedPackageRequestSchema, ImportUnmanagedPackageResponseSchema, ListPackageCommitsResponseSchema, PackageExportManifestSchema, PackagePublishResultSchema, ReassignOrphanedMetadataResponseSchema, RevertPackageCommitResponseSchema, RollbackToPackageCommitResponseSchema } from '@objectstack/spec/api';
52-
import type { DiscardPackageDraftsResponse, DuplicatePackageResponse, ImportUnmanagedPackageRequest, ImportUnmanagedPackageResponse, ListPackageCommitsResponse, PackageExportManifest, PackagePublishResult, ReassignOrphanedMetadataResponse, RevertPackageCommitResponse, RollbackToPackageCommitResponse } from '@objectstack/spec/api';
53+
import { DiscardPackageDraftsResponseSchema, DuplicatePackageResponseSchema, EnsureCustomizationPackageRequestSchema, EnsureCustomizationPackageResponseSchema, ImportUnmanagedPackageRequestSchema, ImportUnmanagedPackageResponseSchema, ListPackageCommitsResponseSchema, PackageExportManifestSchema, PackagePublishResultSchema, ReassignOrphanedMetadataResponseSchema, RevertPackageCommitResponseSchema, RollbackToPackageCommitResponseSchema } from '@objectstack/spec/api';
54+
import type { DiscardPackageDraftsResponse, DuplicatePackageResponse, EnsureCustomizationPackageRequest, EnsureCustomizationPackageResponse, ImportUnmanagedPackageRequest, ImportUnmanagedPackageResponse, ListPackageCommitsResponse, PackageExportManifest, PackagePublishResult, ReassignOrphanedMetadataResponse, RevertPackageCommitResponse, RollbackToPackageCommitResponse } from '@objectstack/spec/api';
5355

5456
// Validate data
5557
const result = DiscardPackageDraftsResponseSchema.parse(data);
@@ -117,6 +119,33 @@ const result = DiscardPackageDraftsResponseSchema.parse(data);
117119
| **error** | `string` | ✅ | Why copying it failed. |
118120

119121

122+
---
123+
124+
## EnsureCustomizationPackageRequest
125+
126+
### Properties
127+
128+
| Property | Type | Required | Description |
129+
| :--- | :--- | :--- | :--- |
130+
| **basePackageId** | `string` | ✅ | Id of the installed base whose paired customization package is ensured (`com.acme.crm` → `com.acme.crm.custom`). The base must be installed and read-only — a writable base is customized directly — and must not itself be a customization package. |
131+
132+
133+
---
134+
135+
## EnsureCustomizationPackageResponse
136+
137+
### Properties
138+
139+
| Property | Type | Required | Description |
140+
| :--- | :--- | :--- | :--- |
141+
| **basePackageId** | `string` | ✅ | The base the pair customizes. |
142+
| **packageId** | `string` | ✅ | The paired customization package: `<basePackageId>.custom`. |
143+
| **namespace** | `string` | ✅ | The pair's namespace: the base's namespace followed by `_custom` (`crm` → `crm_custom`), set explicitly and never derived from the pair's id. A new object authored into the pair is named `<namespace>_<short>`. |
144+
| **dependencyRange** | `string` | ✅ | The range the pair declares on its base in `manifest.dependencies`. A created pair declares `^<major>` of the base's version at creation; an existing pair answers the range it declares, unchanged. |
145+
| **created** | `boolean` | ✅ | True when this call created the pair; false when the pair was already installed and was returned unchanged. |
146+
| **writable** | `boolean` | ✅ | Whether the pair is a writable package — the verdict the package read doors serve as `writable`. A created pair is writable; an existing pair delivered by code or an installed artifact is not, and its items are authored where it is built. |
147+
148+
120149
---
121150

122151
## ImportUnmanagedPackageRequest

‎content/docs/references/index.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Protocol reference — every schema by module
33
navTitle: Protocol Reference
4-
description: Every schema published by @objectstack/spec — 1578 schemas across 14 protocol modules
4+
description: Every schema published by @objectstack/spec — 1580 schemas across 14 protocol modules
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -21,7 +21,7 @@ counts are sums of the rows they head. Regenerate with
2121
| Module | Pages | Schemas | Description |
2222
| :--- | ---: | ---: | :--- |
2323
| [AI Protocol](/docs/references/ai) | 12 | 68 | Agents, tools, skills, RAG and knowledge sources, model registry, conversations. |
24-
| [API Protocol](/docs/references/api) | 34 | 454 | REST contracts, endpoints, routing, realtime, batch, discovery. |
24+
| [API Protocol](/docs/references/api) | 34 | 456 | REST contracts, endpoints, routing, realtime, batch, discovery. |
2525
| [Automation Protocol](/docs/references/automation) | 15 | 77 | Flows and their nodes, approvals, ETL pipelines, webhooks, state machines, execution records. |
2626
| [Data Protocol](/docs/references/data) | 31 | 174 | Objects, fields, queries, filters, datasources and drivers — the ObjectQL layer. |
2727
| [Identity Protocol](/docs/references/identity) | 5 | 27 | Users and accounts, organizations, positions, SCIM provisioning. |
@@ -34,7 +34,7 @@ counts are sums of the rows they head. Regenerate with
3434
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
3535
| [System Protocol](/docs/references/system) | 37 | 287 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
3636
| [UI Protocol](/docs/references/ui) | 16 | 175 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
37-
| **Total** | **205** | **1578** | 14 protocol modules |
37+
| **Total** | **205** | **1580** | 14 protocol modules |
3838

3939
---
4040

@@ -63,7 +63,7 @@ Agents, tools, skills, RAG and knowledge sources, model registry, conversations.
6363

6464
## API Protocol
6565

66-
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 454 schemas**
66+
**Source:** `packages/spec/src/api/` · **Import:** `@objectstack/spec/api` · **34 pages, 456 schemas**
6767

6868
REST contracts, endpoints, routing, realtime, batch, discovery.
6969

@@ -90,7 +90,7 @@ REST contracts, endpoints, routing, realtime, batch, discovery.
9090
| [`odata.zod.ts`](/docs/references/api/odata) | `ODataConfig`, `ODataError`, `ODataFilterFunction`, `ODataMetadata`, `ODataQuery`, `ODataResponse` |
9191
| [`package-api.zod.ts`](/docs/references/api/package-api) | `GetInstalledPackageRequest`, `ListInstalledPackagesRequest`, `PackageApiErrorCode`, `PackageInstallBody`, `PackageInstallManifest`, `PackageInstallRequest`, `PackageInstallResponse`, `PackagePathParams`, `PackageRollbackRequest`, `PackageUpgradeRequest`, `PackageUpgradeResponse`, `ResolveDependenciesRequest`, `ResolveDependenciesResponse`, `UninstallPackageApiRequest`, `UninstallPackageApiResponse`, `UploadArtifactRequest`, `UploadArtifactResponse` |
9292
| [`package-api-assembled.zod.ts`](/docs/references/api/package-api-assembled) | `AssembledInstalledPackage`, `GetInstalledPackageResponse`, `InstalledPackageAtEitherStage`, `ListInstalledPackagesResponse` |
93-
| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
93+
| [`package-lifecycle.zod.ts`](/docs/references/api/package-lifecycle) | `DiscardPackageDraftsResponse`, `DuplicatePackageResponse`, `EnsureCustomizationPackageRequest`, `EnsureCustomizationPackageResponse`, `ImportUnmanagedPackageRequest`, `ImportUnmanagedPackageResponse`, `ListPackageCommitsResponse`, `PackageExportManifest`, `PackagePublishResult`, `ReassignOrphanedMetadataResponse`, `RevertPackageCommitResponse`, `RollbackToPackageCommitResponse` |
9494
| [`plugin-rest-api.zod.ts`](/docs/references/api/plugin-rest-api) | `ErrorHandlingConfig`, `OpenApiGenerationConfig`, `RequestValidationConfig`, `ResponseEnvelopeConfig`, `RestApiEndpoint`, `RestApiPluginConfig`, `RestApiRouteCategory`, `RestApiRouteRegistration`, `ValidationMode` |
9595
| [`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`, `FlowRegistration`, `FlowRegistrationRefusal`, `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`, `GetRecordFieldAccessResponse`, `GetTranslationsRequest`, `GetTranslationsResponse`, `GetUiViewRequest`, `GetUiViewResponse`, `HistoryMetaItemRequest`, `HistoryMetaItemResponse`, `HttpFindQueryParams`, `InstallPackageRequest`, `InstallPackageResponse`, `ListAiConversationsRequest`, `ListAiConversationsResponse`, `ListAiPendingActionsRequest`, `ListAiPendingActionsResponse`, `ListDraftsResponse`, `ListNotificationsRequest`, `ListNotificationsResponse`, `ListPackagesRequest`, `ListPackagesResponse`, `MarkAllNotificationsReadRequest`, `MarkAllNotificationsReadResponse`, `MarkNotificationsReadRequest`, `MarkNotificationsReadResponse`, `MetadataAuthoringClient`, `MetadataAuthoringSurface`, `MetadataConflictError`, `Notification`, `NotificationPreferences`, `PublicFormSubmitResponse`, `PublishMetaItemRequest`, `PublishMetaItemResponse`, `PublishPackageDraftsRequest`, `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` |
9696
| [`query-adapter.zod.ts`](/docs/references/api/query-adapter) | `ODataQueryAdapter`, `OperatorMapping`, `QueryAdapterConfig`, `QueryAdapterTarget`, `RestQueryAdapter` |

‎docs/audits/2026-07-unknown-key-strictness-ledger.counts/api.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ hand-patch a number here** — fix the code or the verdict and regenerate.
1919

2020
| Dir | Sites |
2121
|---|---|
22-
| `api/` | 464 |
22+
| `api/` | 466 |

‎packages/client/src/index.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ import {
128128
PackageExportManifest,
129129
ReassignOrphanedMetadataResponse,
130130
DuplicatePackageResponse,
131+
EnsureCustomizationPackageResponse,
131132
ValidateDataIssue,
132133
} from '@objectstack/spec/api';
133134
// [#17536] The element the two `/packages` READ doors are declared to serve.
@@ -3144,6 +3145,30 @@ export class ObjectStackClient {
31443145
});
31453146
return this.unwrapResponse<DuplicatePackageResponse>(res);
31463147
},
3148+
3149+
/**
3150+
* ADR-0146 D3: ensure the paired customization package `<id>.custom` of
3151+
* the installed, read-only base `id` — the one creation path of the pair.
3152+
* The server answers the pair, creating it when absent (`created: true`,
3153+
* HTTP 201) or returning it unchanged (`created: false`, HTTP 200); its
3154+
* namespace (`<base namespace>_custom`) and its dependency range on the
3155+
* base are derived by the server, never sent. Customer changes to the base
3156+
* are then authored into `packageId`.
3157+
*
3158+
* Refused, with nothing written: a base that is not installed (404), a
3159+
* writable base — customize it directly — or one with no pair namespace,
3160+
* and an installed `<id>.custom` that lacks the namespace or the
3161+
* dependency (409); another package owning the namespace (422
3162+
* `NAMESPACE_CONFLICT`). Under a walled tenancy posture only the platform
3163+
* operator may call it.
3164+
*/
3165+
ensureCustomization: async (id: string): Promise<EnsureCustomizationPackageResponse> => {
3166+
const route = this.getRoute('packages');
3167+
const res = await this.fetch(`${this.baseUrl}${route}/${encodeURIComponent(id)}/ensure-customization`, {
3168+
method: 'POST',
3169+
});
3170+
return this.unwrapResponse<EnsureCustomizationPackageResponse>(res);
3171+
},
31473172
};
31483173

31493174
/**

0 commit comments

Comments
 (0)