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
29 changes: 29 additions & 0 deletions .changeset/25792-retire-dependency-spellings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@objectstack/spec': minor
---

**BREAKING** — the two unread dependency spellings leave the authoring surface: `PackageDependency.optional` (an entry of the marketplace snapshot's `manifestJson.dependencies[]`) and `AppManifest.dependencies` (the bare-id list on the app install manifest) are `retiredKey()` tombstones (#25792, ADR-0141 D8 items 2 and 3).

A `major`-class change, recorded as `minor` under the launch-window convention. ADR-0141 D8 left both fates to "a measured reader count"; the count was zero for both, so both retire at once, with no retirement stage (ADR-0049 enforce-or-remove).

Clause-②: no (narrowing)
Metadata-protocol: narrowed

**Why.** A dependency is declared in one place, the package's `manifest.dependencies` record (`{ [packageId]: versionRange }`), and the ADR-0141 D2 pre-install check reads that record alone and treats every entry as required. Two more spellings stayed declared beside it and nothing read either. `optional: true` promised "missing dep is a warning, not an error", but the record has no optional arm, so no install path could honour it. `AppManifest.dependencies` restated the record without its range, and no install path consulted it. An author, often an AI, who wrote either got nothing enforced and no error. Measured at `origin/main` `6390fe52ce`: zero readers and zero writers of either key outside `packages/spec` (`packages/`, `ui/`, `examples/`, `apps/`), and none in HotCRM at `9464673`, whose three modules declare their dependency in the record.

### FROM → TO

| removed | from | what to write instead |
| --- | --- | --- |
| `optional` on a `PackageDependency` entry (`PackageManifestSchema.dependencies[]`) | `@objectstack/spec/marketplace` | delete the key. An entry carries `packageId` and `versionRange` only. Declare a dependency in `manifest.dependencies` only when the package cannot install without it. |
| `dependencies` on `AppManifest` | `@objectstack/spec/system` | delete the key. Declare each dependency with its range in the package's `manifest.dependencies`, for example `{ 'com.acme.core': '^1.0.0' }`. |

**The one-line fix: delete the key.** A TypeScript author fails `tsc` first (each key is typed as the retired mark), and a parse of `PackageDependencySchema`, `PackageManifestSchema` or `AppManifestSchema` refuses the key with its prescription, the old default (`false`, `[]`) included. The parsed output no longer carries either key, so `PackageDependencyParsed` loses `optional` and `AppManifestParsed` loses `dependencies`.

⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever read either key, so every install answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately).

⚠️ **The out-of-repo consumer population beyond HotCRM is NOT MEASURED.** `@objectstack/spec` is published, and the control plane's reading of `sys_package_version.manifest_json` is not readable from this repository.

The ADR-0087 D3 semantic entry `dependency-unread-spellings-retired` carries the judgement, and both keys are registered in `RETIRED_KEYS_BY_MAJOR[18]`. There is no D2 conversion: neither shape is a stack collection member, so a conversion would have no source to rewrite.

<!-- adr-0087: registered dependency-unread-spellings-retired -->
6 changes: 3 additions & 3 deletions content/docs/references/marketplace/package-version.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Package dependency declaration
| :--- | :--- | :--- | :--- |
| **packageId** | `string` | ✅ | Manifest ID of the dependency |
| **versionRange** | `string` | ✅ | Semver version range (e.g. ^1.0.0) |
| **optional** | `boolean` | optional (default: `false`) | Whether this dependency is optional |
| **optional** | `never` | optional | [REMOVED] `PackageDependency.optional` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — no install path ever honoured it: the pre-install check reads the package's `manifest.dependencies` record, which has no optional arm, and treats every entry as required. Delete the key. Declare a dependency in `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`) only when the package cannot install without it. |


---
Expand All @@ -83,7 +83,7 @@ Package manifest snapshot embedded in a package version
| **description** | `string` | optional | Short description |
| **scope** | `Enum<'platform' \| 'environment'>` | optional (default: `"environment"`) | Package scope |
| **minPlatformVersion** | `string` | optional | Minimum required platform version (semver) |
| **dependencies** | `{ packageId: string; versionRange: string; optional?: boolean }[]` | optional (default: `[]`) | Package dependencies |
| **dependencies** | `{ packageId: string; versionRange: string }[]` | optional (default: `[]`) | Package dependencies |
| **metadataTypes** | `string[]` | optional (default: `[]`) | Metadata types provided by this package |
| **migrations** | `string[]` | optional (default: `[]`) | Migration script identifiers (ordered) |
| **configurationSchema** | `Record<string, any>` | optional | JSON Schema for per-installation configuration properties |
Expand All @@ -97,7 +97,7 @@ Package dependency declaration
| :--- | :--- | :--- | :--- |
| **packageId** | `string` | ✅ | Manifest ID of the dependency |
| **versionRange** | `string` | ✅ | Semver version range (e.g. ^1.0.0) |
| **optional** | `boolean` | optional (default: `false`) | Whether this dependency is optional |
| **optional** | `never` | optional | [REMOVED] `PackageDependency.optional` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — no install path ever honoured it: the pre-install check reads the package's `manifest.dependencies` record, which has no optional arm, and treats every entry as required. Delete the key. Declare a dependency in `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`) only when the package cannot install without it. |


---
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/system/app-install.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ App manifest for marketplace installation
| **flows** | `string[]` | optional (default: `[]`) | Flow names provided |
| **hasSeedData** | `boolean` | optional (default: `false`) | Whether app includes seed data |
| **seedData** | `Record<string, any>[]` | optional (default: `[]`) | Seed data records |
| **dependencies** | `string[]` | optional (default: `[]`) | Required app dependencies |
| **dependencies** | `never` | optional | [REMOVED] `AppManifest.dependencies` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0141 D8) — nothing read it: it listed bare ids with no version range, and no install path consulted it. Delete the key. Declare each dependency with its range in the package's `manifest.dependencies` (`{ 'com.acme.core': '^1.0.0' }`), which the pre-install check enforces. |


---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,17 @@ other two on the runtime card:
The resolver-side `ResolvedPackageDependencySchema` (`@objectstack/spec/kernel`) is unaffected: it is a resolution outcome,
not a declaration, and `PackageDependencySchema`'s own header already tells the two apart.

**Addendum (2026-10-27, #25792 item 6) — items 2 and 3 decided on the measured count: both retire.** Measured at
`origin/main` `6390fe52ce`: `PackageDependencySchema.optional` has **0** readers and **0** writers outside `packages/spec`,
and `AppManifestSchema.dependencies` has **0** readers and **0** writers there, with no parse of `AppManifestSchema` at all
(this repository's `packages/`, `ui/`, `examples/` and `apps/`; HotCRM at `9464673`, whose three module manifests declare
the record, the live control). Inside `packages/spec` the only authors were the two schemas and three of their own tests.
Each key is a `retiredKey()` tombstone now (`marketplace/PackageDependency:optional` and `system/AppManifest:dependencies`
in `RETIRED_KEYS_BY_MAJOR[18]`), with the D3 record `dependency-unread-spellings-retired` and no D2 conversion, because
neither shape is a stack collection member. The projection above therefore carries `packageId` = the key and
`versionRange` = the value, nothing else: item 1's conversion writes no `optional`, and `manifest.dependencies` is the
one way to declare a dependency.

---

## Consequences
Expand Down
1 change: 0 additions & 1 deletion packages/spec/authorable-defaults/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
"marketplace/Package:isStarter = false",
"marketplace/Package:publisher = \"private\"",
"marketplace/Package:visibility = \"org\"",
"marketplace/PackageDependency:optional = false",
"marketplace/PackageManifest:dependencies = []",
"marketplace/PackageManifest:metadataTypes = []",
"marketplace/PackageManifest:migrations = []",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/authorable-defaults/system.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"system/AppInstallResult:createdTables = []",
"system/AppInstallResult:installedObjects = []",
"system/AppInstallResult:seededRecords = 0",
"system/AppManifest:dependencies = []",
"system/AppManifest:flows = []",
"system/AppManifest:hasSeedData = false",
"system/AppManifest:objects = []",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-surface/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"marketplace/Package:translations",
"marketplace/Package:updatedAt",
"marketplace/Package:visibility",
"marketplace/PackageDependency:optional",
"marketplace/PackageDependency:optional [RETIRED]",
"marketplace/PackageDependency:packageId",
"marketplace/PackageDependency:versionRange",
"marketplace/PackageManifest:configurationSchema",
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/authorable-surface/system.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"system/AppInstallResult:seededRecords",
"system/AppInstallResult:success",
"system/AppInstallResult:version",
"system/AppManifest:dependencies",
"system/AppManifest:dependencies [RETIRED]",
"system/AppManifest:description",
"system/AppManifest:flows",
"system/AppManifest:hasSeedData",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/src/contracts/app-lifecycle-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ describe('App Lifecycle Service Contract', () => {
views: ['contact_list', 'deal_board'],
flows: ['new_deal_notification'],
hasSeedData: true,
dependencies: [],
};

it('should allow a minimal IAppLifecycleService implementation with all required methods', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import {
//
// ./cloud — since #16325 ./marketplace — the DECLARATION form an author
// writes into a package manifest:
// `{ packageId, versionRange, optional }`, embedded in
// `{ packageId, versionRange, optional }` (`optional` since retired,
// ADR-0141 D8), embedded in
// `PackageManifestSchema.dependencies[]` → `sys_package_version.manifest_json`;
// ./kernel — the RESOLVER form the dependency resolver walks:
// `{ name, versionConstraint, type, resolvedVersion }`, embedded in
Expand Down Expand Up @@ -144,10 +145,10 @@ describe('PackageDependency dual-source retirement (C7)', () => {
packageId: 'com.objectstack.core',
versionRange: '^1.0.0',
});
// `optional` is a retired tombstone (ADR-0141 D8): it materializes nothing.
expect(declared).toEqual({
packageId: 'com.objectstack.core',
versionRange: '^1.0.0',
optional: false, // the `.default(false)` still applies
});
});

Expand All @@ -163,13 +164,14 @@ describe('PackageDependency dual-source retirement (C7)', () => {
const kernelKeys = keysOf(ResolvedPackageDependencySchema as never);
const cloudKeys = keysOf(PackageDependencySchema as never);
expect([...kernelKeys].sort()).toEqual(['name', 'resolvedVersion', 'type', 'versionConstraint']);
// `optional` stays in the shape as a retiredKey() tombstone (ADR-0141 D8).
expect([...cloudKeys].sort()).toEqual(['optional', 'packageId', 'versionRange']);
expect([...kernelKeys].filter((k) => cloudKeys.has(k)), 'key sets must stay disjoint').toEqual([]);

// Neither side is `.strict()`, so cross-pasting a document does not throw —
// it silently strips. That is exactly the failure mode the shared name hid,
// and it is why the pin above forbids a "helpful" re-export alias.
const manifestDoc = { packageId: 'com.objectstack.core', versionRange: '^1.0.0', optional: true };
const manifestDoc = { packageId: 'com.objectstack.core', versionRange: '^1.0.0' };
const strippedByResolver = ResolvedPackageDependencySchema.safeParse(manifestDoc);
expect(strippedByResolver.success, 'resolver form rejects a manifest doc only on required keys').toBe(false);

Expand Down
Loading
Loading