Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/28318-served-disableable-verdict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@objectstack/spec': minor
'@objectstack/runtime': minor
---

feat(spec,runtime): every served package row carries `disableable`, the disable door's own verdict, beside `writable`

Clause-②: yes (widening)
Metadata-protocol: added

- **New key.** `SERVED_ROW_VERDICTS` in `@objectstack/spec/api-assembled` declares `disableable: boolean` on both stage branches of `InstalledPackageAtEitherStageSchema`, so `GET /api/v1/packages` and `GET /api/v1/packages/:id` serve it on every row. It answers "may an org disable this package here?": `true` for a writable base and for a module of an installed app (a `type: 'module'` package that directly depends on an installed `type: 'app'` package, outside a `system` / `cloud` scope), `false` for every other read-only package.
- **One predicate.** The runtime computes the served value with the same predicate `PATCH /api/v1/packages/:id/disable` asks before it switches a package off, so the row and the door cannot disagree. The caller's own permission and a catalog wall are still asked at the door; the verdict is about the package only.
- **`writable` is unchanged.** A module of an installed app is `disableable: true` and `writable: false`: it may be switched off, never authored into. A client offers Disable / Enable from `disableable`, and authoring from `writable`.
- **Who sees it.** A customer admin on the Packages page: once the Console reads this key, it can offer Disable on a suite module the server already accepts.
4 changes: 4 additions & 0 deletions content/docs/references/api/package-api-assembled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Get installed package response
| **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 |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |
| **disableable** | `boolean` | ✅ | Whether this server lets an org disable this package: the disable door's own verdict, true for a writable base and for a module of an installed app, false for every other read-only package. A client offers Disable / Enable from it, never from writable. Computed per read by the server's own predicate, never stored |

### Nested Shape: `GetInstalledPackageResponse.data[option 2]`

Expand All @@ -230,6 +231,7 @@ Get installed package response
| **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 |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |
| **disableable** | `boolean` | ✅ | Whether this server lets an org disable this package: the disable door's own verdict, true for a writable base and for a module of an installed app, false for every other read-only package. A client offers Disable / Enable from it, never from writable. Computed per read by the server's own predicate, never stored |


---
Expand Down Expand Up @@ -262,6 +264,7 @@ This schema accepts one of the following structures:
| **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 |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |
| **disableable** | `boolean` | ✅ | Whether this server lets an org disable this package: the disable door's own verdict, true for a writable base and for a module of an installed app, false for every other read-only package. A client offers Disable / Enable from it, never from writable. Computed per read by the server's own predicate, never stored |

### Nested Shape: `InstalledPackageAtEitherStage[option 1].manifest`

Expand Down Expand Up @@ -325,6 +328,7 @@ This schema accepts one of the following structures:
| **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 |
| **writable** | `boolean` | ✅ | Whether this server lets an org author into and manage this package (a writable base); false for a read-only package. Computed per read by the server's own predicate, never stored |
| **disableable** | `boolean` | ✅ | Whether this server lets an org disable this package: the disable door's own verdict, true for a writable base and for a module of an installed app, false for every other read-only package. A client offers Disable / Enable from it, never from writable. Computed per read by the server's own predicate, never stored |

### Nested Shape: `InstalledPackageAtEitherStage[option 2].manifest`

Expand Down
9 changes: 5 additions & 4 deletions packages/client/src/return-type-precision.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -645,10 +645,11 @@ export async function returnTypePrecisionPins12034(): Promise<void> {
void (await scoped.packages.get('com.acme.crm')).package;
}

// A SERVED row is the registry record plus the read door's own verdict,
// `writable` (#23972 declared it on the served row, not on the record).
declare const authoringRow: InstalledPackage & { writable: boolean };
declare const assembledRow: AssembledInstalledPackage & { writable: boolean };
// A SERVED row is the registry record plus the read door's own verdicts,
// `writable` (#23972 declared it on the served row, not on the record) and
// `disableable` (#28318, declared beside it).
declare const authoringRow: InstalledPackage & { writable: boolean; disableable: boolean };
declare const assembledRow: AssembledInstalledPackage & { writable: boolean; disableable: boolean };

/**
* [#17536] The four `/packages` READ members declare the stage the DOOR is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,16 @@ const DELIBERATELY_NOT_SERVED: readonly string[] = [];
* Not a record field: it is a property of the running engine, recomputed per
* read and never stored. `isWritablePackage` is the same predicate the
* authoring and lifecycle gates use.
* - `disableable` — the disable door's verdict (`withDisableableVerdict`,
* #28318). Not a record field either, for the same reason: it is computed
* per read by `isDisableablePackage`, the predicate `PATCH
* /packages/:id/disable` asks, and never stored.
*
* Compared by SET EQUALITY against the measured `served − record`, so both
* directions are loud: a stamp that stops reaching the wire (the reorder this
* card is about) and a stamp that arrives without a decision.
*/
const DOOR_COMPUTED_STAMPS: readonly string[] = ['writable'];
const DOOR_COMPUTED_STAMPS: readonly string[] = ['writable', 'disableable'];

/** Authenticated caller holding the ADR-0106 D4 read capability. */
const reader = (): any => ({
Expand Down
Loading
Loading