Skip to content

Commit b389e43

Browse files
feat(metadata-protocol,runtime,service-automation,spec)!: the protocol refuses every organization-scoped write; an uninstall is environment-wide (ADR-0131 D6/D12) (#22515)
Refs #15206 (S4) Refs #22350 Clause-②: yes (narrowing) Stage S4 of #15206 (ADR-0131 C5): the metadata protocol refuses every organization-scoped write, and the per-organization write path behind it is deleted. It carries #22350 (ruling A): the `organizationId` / `allTenants` keys and both `TENANT_SCOPE_REQUIRED` refusals of the package uninstall retire. #15206 remains open for S5. #22350 is closed by the seat with this stage's landing record, not by this PR. ## What changes **Protocol (`@objectstack/metadata-protocol`)** - **One refusal for every write verb.** `organizationScopedWriteRefusal` is asked FIRST, before any read: `saveMetaItem` (draft and publish), `publishMetaItem`, `deleteMetaItem`, `rollbackMetaItem`, `revertCommit`, `rollbackToPackageCommit`, `publishPackageDrafts`, `discardPackageDrafts`, `revertStoredPackage`, `duplicatePackage` and `reassignOrphanedMetadata`. - Answer: `403 NOT_OVERRIDABLE` for every type. The first sentence names the tenancy posture in force. - The five-type `allowOrgOverride` exemption is gone, and the `OS_METADATA_WRITABLE` hatch no longer opens an organization scope. - `organizationId` is removed from each verb's request type. - **Deleted as unreachable or per-organization:** - `orgScopedWriteRefusal` (the old exemption) - `anonymousFormIntakeOrgScopeRefusal` / `anonymousFormIntakeReopenRefusal` / `envWideRawViewRows` - `resolveMetaItemOrgScope` / `resolveDraftOrgScopeForPublish` - the per-draft and per-item org-scope threading in publish, the package publish, discard, revert, rollback, duplicate and adopt-orphans. - **The audit and commit ledgers write `organization_id` NULL.** `MetadataAuditEntry.organizationId` is typed `null`. - **`revertCommit` refuses** a commit row recorded in a legacy organization layer. - **`migrateStoredMetadata` reports** an organization-scoped row as `skipped`, naming the promotion ceremony (ADR-0131 C7), and never re-saves it. - **`applyRemoteMetadataMutation`** converges the registry on the environment row. - **`deletePackage` (#22350 A).** - A request carrying `organizationId` or `allTenants` (any value) → `400 INVALID_REQUEST`, nothing removed. - With neither key, the uninstall is environment-wide: every row bound to the package. - Legacy organization-scoped rows of the package are removed with it through the repository, so the history tombstone is kept. - `UninstallCleanup` args drop `organizationId`. - **`findPlatformScheduleOrgGaps`** loses its `organizationId` input, because every write is platform-level now. Its hint no longer prescribes "publish into an organization". - **Seeds applied on publish** get no caller organization: a seed dataset names its own organization (ADR-0131 D9, §12). **Packages door (`@objectstack/runtime`, `domains/packages.ts`)** - No organization is threaded into any `/packages` verb, nor into the commit list or `assemblePackageManifest` (S3's carried finding). - `requireUninstallOrganizationScope` and the door's `400 TENANT_SCOPE_REQUIRED` are deleted. Who may uninstall stays with `requireManageMetadata` plus the read-only-package gate. **Callers** - **`@objectstack/service-automation`** `flow-credential-migration.ts`: a legacy organization-scoped flow row is not re-saved. It is reported as `failed` with `NOT_OVERRIDABLE` and logged at `error`, stating that the row still carries the credential in cleartext. - **`@objectstack/cloud-connection`**: the `runUninstallCleanups` runner type drops the retired `organizationId`. Type-only; the emitted JS is unchanged. **Spec (`@objectstack/spec`)** - `organizationId` leaves the `SaveMetaItem`, `PublishMetaItem` and `DeleteMetaItem` request schemas. The authorable-surface lines go with them: the defs are not reachable from a metadata root, and `check:authorable-surface` proves it. - Both `TENANT_SCOPE_REQUIRED` ledger rows are removed. - ADR-0087 semantic entries `metadata-write-organization-scope-refused` and `package-uninstall-environment-wide`; `registry.ts` and the reference docs are regenerated. **Docs** - `environment-variables.mdx` (`OS_METADATA_WRITABLE` no longer opens organization scope). - `metadata-service.mdx` (uninstall is environment-wide). - `public-data-collection.mdx` §4: an organization copy of a public form is a legacy row that no write can edit. The organization-scoped save check is deleted (patch round 1). ## Not in this PR (by ruling) - **Read narrowing.** The protocol's reads still accept an organization and still serve legacy rows; that is S5. - **Anonymous form doors.** Their Default-Organization read is unchanged (Q3 A). - **Stored rows.** No stored row is migrated or dropped (C7). The column and index work belongs to #15211. ## Cross-lane paths - `domain:cli`: `packages/runtime/src/domains/packages.ts` (declared) and runtime tests. - `domain:services`: `packages/services/service-automation/src/flow-credential-migration.ts` (declared) and its test. - `domain:spec`: - `packages/spec/src/api/protocol.zod.ts`, `error-code-ledger.zod.ts` and `stack.zod.ts` (comment); - `migrations/entries/semantic/18.*` (2 new); - `migrations/registry.ts`, `authorable-surface/api.json` and `src/api/protocol.test.ts`; - `content/docs/references/api/*` (generated). All of these are declared. - **Declared by the seat after the build** (re-declarations on the lane posts): - `domain:cli` (#6024) 6085525732: `packages/cloud-connection/src/marketplace-install-local-plugin.ts`, the runtime tests and the `packages/rest` tests. - `domain:cli` (#6024) 6088463629: `packages/runtime/src/audit-meta-item-org-scope.integration.test.ts`. - `domain:devx` (#6023) 6085543707: `environment-variables.mdx`, `metadata-service.mdx` and the two dogfood tests. - `domain:devx` (#6023) 6088455367: `content/docs/ui/public-data-collection.mdx`. - `domain:spec` (#6017) 6085530833: `stack.zod.ts`, `authorable-surface/api.json`, `src/api/protocol.test.ts`, and the two entries with their regenerated pages. - `domain:spec` (#6017) 6088472708: the regenerated `spec-changes.json` and `docs/protocol-upgrade-guide.md`. - `domain:services` (#6021) 6085537186: the `flow-credential-migration` test. - The `packages/objectql` test files are in this seat's own lane. ## Size About 6,500 changed lines (+1,639 / −4,857, 93 files at `2318d0ba1`), over the human-merge line (3,000 on `main`). Stage 0's named split seam does not work, for two measured reasons: - **It is coupled.** The packages door must stop sending an organization in the same landing as the protocol refusal, or every `/packages` write by an org-active caller answers 403. - **It does not get under the line.** The protocol package alone is about 3,800 changed lines: source about 1,580, tests about 2,260. So this lands on the human-merge route as one PR. Most of the volume is deleted tests of deleted behaviour. ## Verification Three rounds measured this PR. The later two are summarised first; the first round's detail follows. - **Merge-main round** (head `2318d0ba1`, report 6089617413). It merged `origin/main` `5910b5e3e` through `os-regen-merge.sh`, then regenerated `spec-changes.json` and `protocol-upgrade-guide.md`. Every step-18 id is present in `registry.ts`, `spec-changes.json` and the upgrade guide. The `packages/spec` full suite passes 19,751. All 125 derived gates exit 0. `packages/runtime` is NOT MEASURED in this round: the merge touched no runtime file. - **Patch round 1** (head `7054e63de`, report 6087716647). This round followed contract review 6085867875, which FAILed. The `packages/spec` full suite passes 19,716. The `packages/runtime` full suite passes 345 files, plus the re-premised `audit-meta-item-org-scope` at 7/7. All 125 derived gates exit 0. ### First round (head `537fa89c8`, merge base `2e10c9ab`) **Identity pin.** `protocol.org-scoped-write-refused.test.ts` passes 302 tests. It is enumerated over every `DEFAULT_METADATA_TYPE_REGISTRY` type plus the plugin type `acme_widget`, and covers: - save (draft and publish), publish and package publish → `403 NOT_OVERRIDABLE`, with nothing persisted; - the first sentence naming each posture (`single`, `group`, `isolated`); - the hatch closed to organization scope; - environment-wide controls accepted; - a legacy organization draft left as stored. **Reverse verification.** The fix was committed first. Using `scripts/ablation-replace.mjs` with a trap, the five-type exemption was re-inserted at the save door (`if (!registryAllowsOverlay(request.type))` around the refusal): - Predicted: the tier-A cases red, everything else green. - Measured: 17 red / 285 green. The red cases are the 10 tier-A save cases (5 types × draft/publish), plus 7 view-based cases (posture sentence ×3, hatch, topology, and the legacy-overlay re-save ×2). - Every other type's case and every control stayed green. The restore was verified as blob equal to HEAD with an empty `git diff HEAD`. **Suites (local, under the verification lock):** - `@objectstack/metadata-protocol` full suite: 223 files, 28,304 passed, exit 0. - `@objectstack/runtime` packages domain plus the touched integration files: 30 files, 498 passed. - The repaired `objectql`, `rest`, `service-automation` and `metadata-core` files each pass. - `objectql` typecheck plus `check:test-typecheck`: exit 0. - `dogfood`: 11 files that exercise metadata writes and package install / uninstall / publish, 167 tests passed. **Typecheck:** spec, metadata-protocol, objectql, rest, service-automation, runtime, cloud-connection, cli and dogfood all pass at the merged head. **Gates:** `dispatch-gates --commands` derived 125 families. All 125 were run, and `--ran` reconciles them with exit codes: 125 run, 0 NOT-MEASURED, all exit 0. - Two were red on the first pass and are fixed in this PR: `check:error-code-provenance` (the flow-credential move's `NOT_OVERRIDABLE` row is now registered) and `check:objectql-double-limit`. - Four first returned prerequisite exits — a shallow clone, or a `dist` mid-rebuild — and were re-run green after deepening and rebuilding. - `check:generated` reports all 15 artifacts up to date. **objectui** at the pin `f0268ad784`: it sends no `organizationId` or `allTenants` on any metadata or package write, and imports none of the changed request types or `TENANT_SCOPE_REQUIRED` (`git grep`: 0 hits). **NOT MEASURED locally in the first round** (declared to CI): - the `spec` suite, which was measured in patch round 1; - the full `runtime` suite, which was measured in patch round 1; - the `rest`, `objectql`, `cli` and dogfood suites beyond the files named above; - `pnpm lint`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NcYr731pAx356wDedHe9Ca --- _Generated by [Claude Code](https://claude.ai/code/session_01NcYr731pAx356wDedHe9Ca)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b53b949 commit b389e43

93 files changed

Lines changed: 1639 additions & 4857 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/runtime': minor
4+
'@objectstack/service-automation': minor
5+
'@objectstack/spec': minor
6+
---
7+
8+
feat(metadata-protocol,runtime,service-automation,spec)!: the metadata protocol refuses every organization-scoped write, and an uninstall is environment-wide (ADR-0131 D6/D12)
9+
10+
Clause-②: yes (narrowing)
11+
12+
<!-- adr-0087: registered metadata-write-organization-scope-refused, package-uninstall-environment-wide -->
13+
14+
**BREAKING, in both directions.** It narrows: an organization-scoped metadata write is refused, and request keys retire. It widens one refusal: a package uninstall naming neither `organizationId` nor `allTenants`, refused before with `400 TENANT_SCOPE_REQUIRED`, is now accepted and runs environment-wide. Graded `minor` on the v18 prerelease line: Changesets is in pre mode with the tag `next`, and the fixed group is already majored by the line's opening marker, so this ships in an `18.0.0-next.N`.
15+
16+
ADR-0131 D6 retires the per-organization overlay axis. The `/meta` doors already carry no organization; now the metadata protocol itself refuses an organization-scoped write from every door, and the per-organization write path behind it is deleted.
17+
18+
**What changes.**
19+
20+
- Every protocol write that names an organization is refused with `403 NOT_OVERRIDABLE`, before anything is read or written, for every metadata type and every tenancy posture. This covers `saveMetaItem` (draft and publish), `publishMetaItem`, `deleteMetaItem`, `rollbackMetaItem`, `revertCommit`, `rollbackToPackageCommit`, `publishPackageDrafts`, `discardPackageDrafts`, `revertStoredPackage`, `duplicatePackage` and `reassignOrphanedMetadata`. The message's first sentence names the tenancy posture in force. The five types that declared `allowOrgOverride` (`view`, `dashboard`, `report`, `translation`, `email_template`) and the `OS_METADATA_WRITABLE` hatch no longer open an organization scope.
21+
- The audit ledger (`sys_metadata_audit`) and the commit ledger (`sys_metadata_commit`) record `organization_id` NULL.
22+
- The `/packages` doors of the runtime dispatcher thread no organization into any verb: publish-drafts, discard-drafts, the commit list, commit revert, rollback, revert, adopt-orphans, duplicate, delete, and the package manifest read.
23+
- `deletePackage` retires its `organizationId` and `allTenants` request keys and both `400 TENANT_SCOPE_REQUIRED` refusals. The dispatcher's `DELETE /api/v1/packages/:id` no longer refuses an operator with no active organization. An uninstall removes every row bound to the package in this environment. Who may uninstall is the door's operator gate, as before.
24+
- A seed published with a package no longer takes the publisher's active organization: a seed dataset names the organization it populates (ADR-0131 D9).
25+
26+
**What moves for consumers.**
27+
28+
| From | To |
29+
|:--|:--|
30+
| `organizationId` on a `SaveMetaItem` / `PublishMetaItem` / `DeleteMetaItem` request (`@objectstack/spec`) | drop it: the write lands environment-wide. The key is stripped at a spec parse (the schemas are not strict) and refused at the protocol: a request still naming one answers `403 NOT_OVERRIDABLE` |
31+
| `organizationId` on any other protocol write verb's request | drop it, same refusal |
32+
| `deletePackage({ packageId, organizationId })` or `deletePackage({ packageId, allTenants: true })` | `deletePackage({ packageId })`; either retired key answers `400 INVALID_REQUEST` and removes nothing |
33+
| `DeletePackageRequest.organizationId` / `.allTenants` (`@objectstack/metadata-protocol`) | gone from the type |
34+
| `UninstallCleanup`'s `organizationId` argument | gone; a cleanup receives `{ packageId, actor? }` |
35+
| `TENANT_SCOPE_REQUIRED` in the error-code ledger | retired; no producer emits it |
36+
| `findPlatformScheduleOrgGaps`' `organizationId` input | gone: every write is platform-level |
37+
| a `seed` draft whose records carry no `organization_id`, relying on the publisher's active organization under `group` | set `organization_id` on each record (ADR-0131 D12, item 12); under `group` a seed record that names no organization is refused at load, and under `single` the loader still derives the Default Organization |
38+
39+
**What a deployment observes.**
40+
41+
- Rows stored organization-scoped before this release are not touched by any write. `POST /meta/_migrate-stored` reports each one as `skipped` and names the promotion ceremony (ADR-0131 C7); the stored-flow credential move reports such a flow as not moved (`NOT_OVERRIDABLE`) and logs that its credential is still in cleartext in that row — rotate it. A legacy organization-scoped draft is no longer promoted, discarded or reverted by a package verb, a commit recorded in a legacy organization layer is refused by `revertCommit`, and `duplicatePackage` and adopt-orphans copy or adopt the environment's rows only.
42+
- An uninstall removes the package's legacy organization-scoped rows with it, as the declared cross-tenant uninstall did.
43+
44+
**What does not change.** The protocol's reads still accept an organization and still serve legacy organization rows; that narrowing is a later stage. The anonymous form doors' read of the Default Organization's layer is unchanged.

‎content/docs/deployment/environment-variables.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ that bypassed write hooks, `rebuildSearchCompanion` (from
306306
| `OS_MARKETPLACE_CACHE` | enum | `on` | `off` disables the in-memory marketplace listing cache. |
307307
| `OS_MARKETPLACE_PUBLIC_BASE_URL` | url | — | Public base URL of the marketplace registry (proxied from this runtime when set). |
308308
| `OS_ALLOW_UNMASKED_OBJECT_METADATA` | boolean | `false` | Escape hatch for the metadata-plane field-level security mask ([ADR-0106](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0106-metadata-plane-fls-object-schema-masking.md) D8). By default every object schema served by `/meta` and `/metadata` is projected onto the fields the **calling user** may read, so a field they cannot read does not appear at all — not its name, label, type, picklist options, formula, `visibleWhen` predicate, `defaultValue`, or the `requiredPermissions` capability guarding it. Set to `1` to serve the full schema to every authenticated caller, as releases before this one did. This changes **disclosure only**: the data plane still masks values and refuses forbidden writes either way, and the console reads field affordances from `/auth/me/permissions`, so toggling it never changes UI correctness. The REST layer also honours a per-server `metadata.maskObjectFields: false`; this variable is the deployment-wide knob and covers the runtime `/metadata` dispatcher, which has no REST config to read. |
309-
| `OS_METADATA_WRITABLE` | csv | — (none) | Comma-separated metadata type names (e.g. `hook,job`) granted a runtime escape hatch that treats them as `allowOrgOverride: true` for items **no managed package ships**: it opens runtime creation of a type whose registry entry allows none, and an organization-scoped write of a type with no per-organization channel. It **never opens an item a managed package ships**: managed content is sealed ([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6), so an overlay or a removal of a shipped flow, object, field, permission set, position or any other type without an environment overlay is refused with `403 NOT_OVERRIDABLE` whether the hatch is set or not. Customize managed content through its type's own route instead: an environment overlay for `view`, `dashboard`, `report`, `translation` and `email_template`; disable, or clone under a new name, for a flow; clone for a permission set. Overlay rows this hatch wrote before are still served, and a row of a type that merges overlays at read (`permission`, `position`, `page`, `app`, `dataset`, `book`, `tool`, `skill`) can still be removed to restore the package's definition. See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). |
309+
| `OS_METADATA_WRITABLE` | csv | — (none) | Comma-separated metadata type names (e.g. `hook,job`) granted a runtime escape hatch that treats them as `allowOrgOverride: true` for items **no managed package ships**: it opens runtime creation of a type whose registry entry allows none. It never opens an organization-scoped write: since [ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6 the metadata protocol refuses every organization-scoped write with `403 NOT_OVERRIDABLE`. It **never opens an item a managed package ships**: managed content is sealed ([ADR-0131](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0131-total-organization-ownership-no-null-organization-id.md) D6), so an overlay or a removal of a shipped flow, object, field, permission set, position or any other type without an environment overlay is refused with `403 NOT_OVERRIDABLE` whether the hatch is set or not. Customize managed content through its type's own route instead: an environment overlay for `view`, `dashboard`, `report`, `translation` and `email_template`; disable, or clone under a new name, for a flow; clone for a permission set. Overlay rows this hatch wrote before are still served, and a row of a type that merges overlays at read (`permission`, `position`, `page`, `app`, `dataset`, `book`, `tool`, `skill`) can still be removed to restore the package's definition. See [ADR-0005](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0005-metadata-customization-overlay.md). |
310310

311311
---
312312

‎content/docs/kernel/contracts/metadata-service.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,6 @@ pending draft is done via the `/meta/:type/:name/publish` route.
497497
| `POST` | `/api/v1/packages/publish` | Publish a package to the marketplace registry (body: `{ manifest, metadata }`) — the REST registrar's one route |
498498
| `GET` | `/api/v1/packages` | List the installed packages (the in-memory registry; published-but-not-installed artifacts are not listed) |
499499
| `GET` | `/api/v1/packages/:id` | Get an installed package — the bare row under `data`; a missing id answers `404 RESOURCE_NOT_FOUND`, message `Package 'ID' not found` |
500-
| `DELETE` | `/api/v1/packages/:id` | Uninstall a package for the caller's organization (`?keepData=true` keeps the object tables) |
500+
| `DELETE` | `/api/v1/packages/:id` | Uninstall a package environment-wide: every row bound to it in this environment (`?keepData=true` keeps the object tables) |
501501
| `POST` | `/api/v1/meta/:type/:name/publish` | Promote a metadata item's pending draft to live |
502502
| `POST` | `/api/v1/meta/:type/:name/rollback` | Restore a historical version as the live overlay |

‎content/docs/references/api/contract.mdx‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +322 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +321 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **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) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **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`. |
@@ -347,7 +347,6 @@ const result = ApiErrorSchema.parse(data);
347347
* `SUGGESTION_NOT_FOUND`
348348
* `SUGGESTION_STATE`
349349
* `SUMMARY_RECOMPUTE_FAILED`
350-
* `TENANT_SCOPE_REQUIRED`
351350
* `THROTTLED`
352351
* `UNAUTHORIZED`
353352
* `UNIQUE_SCOPE_CONFIRMATION_REQUIRED`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,6 @@ const result = ErrorCode.parse(data);
514514
* `SUGGESTION_NOT_FOUND`
515515
* `SUGGESTION_STATE`
516516
* `SUMMARY_RECOMPUTE_FAILED`
517-
* `TENANT_SCOPE_REQUIRED`
518517
* `THROTTLED`
519518
* `UNAUTHORIZED`
520519
* `UNIQUE_SCOPE_CONFIRMATION_REQUIRED`

‎content/docs/references/api/protocol.mdx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -703,7 +703,6 @@ A write-path strip event: caller-supplied fields legally dropped from the payloa
703703
| :--- | :--- | :--- | :--- |
704704
| **type** | `string` | ✅ | Metadata type name |
705705
| **name** | `string` | ✅ | Item name |
706-
| **organizationId** | `string` | optional | Organization (tenant) scope for the reset. Load-bearing, not advisory: it selects the ADR-0005 overlay partition, so it decides WHICH row the reset destroys — an org-scoped delete removes that tenant's own overlay, while an org-less delete reaches the environment-wide row and would blank the item for every tenant. Absent = environment-wide. |
707706
| **parentVersion** | `string` | optional | ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the `If-Match` request header). Present, a concurrent edit is reported as a 409 conflict instead of silently reset; absent = last-write-wins against the current row (Studio's "Reset" button is unpinned). |
708707
| **actor** | `string` | optional | Identity recorded on the delete's history tombstone row. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system". |
709708
| **state** | `Enum<'active' \| 'draft'>` | optional | Which lifecycle row to discard: `draft` discards the pending draft overlay only (the still-active overlay, if any, keeps serving); `active` or absent resets the live row. Absent defaults to `active`. |
@@ -2299,7 +2298,6 @@ Installed package with runtime lifecycle state
22992298
| :--- | :--- | :--- | :--- |
23002299
| **type** | `string` | ✅ | Metadata type name |
23012300
| **name** | `string` | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). The promotion door enforces the same grammar as `saveMetaItem`. |
2302-
| **organizationId** | `string` | optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `NO_DRAFT`. Absent = environment-wide. |
23032301
| **actor** | `string` | optional | Identity recorded on the `op='publish'` history event. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. |
23042302
| **message** | `string` | optional | Optional human-readable note recorded with the publish history event. |
23052303
| **packageId** | `string \| null` | optional | ADR-0048 — the software package the draft being promoted was listed under, when the caller has one to state (`?package=<id>` on the REST door). ⚠️ `null` is NOT the same as absent, and the difference is load-bearing: the implementation branches on the KEY BEING PRESENT, so an ABSENT key keeps the historical "match any package" resolution while `null` pins the lookup to the package-UNBOUND row. Spread it in conditionally; a present-and-`undefined` key coerces to `null` downstream and makes a package-bound draft unfindable — a silent `no_draft` on the untouched path. |
@@ -2597,7 +2595,6 @@ Installed package with runtime lifecycle state
25972595
| **type** | `string` | ✅ | Metadata type name |
25982596
| **name** | `string` | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). Slash-compound names are refused at the publish door. |
25992597
| **item** | `any` | ✅ | Metadata item definition |
2600-
| **organizationId** | `string` | optional | Organization (tenant) scope for the write. Load-bearing, not advisory: it selects the overlay partition (ADR-0005) the row lands in — an org-scoped save writes that tenant's own overlay, while an org-less save writes the environment-wide row every tenant reads — and it is the scope stamped on the write's audit row. An org-scoped write of a type whose registry entry declares `allowOrgOverride: false` is refused (403). Absent = environment-wide. |
26012598
| **parentVersion** | `string \| null` | optional | ADR-0008 optimistic-concurrency pin: the version token the caller believes is current (on the REST door, the `If-Match` request header; the item read's `version` and a save receipt's `version` serve it). Present as a string, a concurrent edit is reported as a 409 conflict instead of silently overwritten. ⚠️ `null` is NOT the same as absent: a present `null` asserts "no current row of this lifecycle" — the first-write pin, refused 409 when a row already exists (on the REST door, `If-None-Match: *`; that header takes `*` alone and never beside `If-Match`, and any other spelling is refused 400) — while an ABSENT key is unpinned: the implementation adopts the current row's hash as the parent (last-write-wins). Nullable because that is the implementation's parameter type, and unlike the reset twin (which folds a present `null` back to the current hash) this verb passes `null` through to the repository's conflict check unchanged. |
26022599
| **actor** | `string` | optional | Identity recorded on the write's history event (`recorded_by`, a lookup into `sys_user`) and audit row. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system". |
26032600
| **force** | `boolean` | optional | Destructive-change acknowledgement (`?force=true` on the REST door): skips the safety diff that refuses an `object` save whose body drops fields or narrows types the stored item still carries (409 with the findings otherwise). Only `object` saves reach that diff, so the flag is inert for every other type. Absent = the guard runs. |

0 commit comments

Comments
 (0)