diff --git a/.changeset/plugin-manifest-dead-containers-retired.md b/.changeset/plugin-manifest-dead-containers-retired.md new file mode 100644 index 0000000000..7529155047 --- /dev/null +++ b/.changeset/plugin-manifest-dead-containers-retired.md @@ -0,0 +1,67 @@ +--- +"@objectstack/spec": minor +--- + +feat(spec)!: retire the plugin manifest's three dead top-level containers — `capabilities`, `configuration`, `extensions` (#11332, ADR-0049) + + + +**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep +launch-window convention ships it as `minor`; the prescriptions are registered +under protocol major 18 — `RETIRED_KEYS_BY_MAJOR[18]` + the D3 semantic entry +`plugin-manifest-dead-containers-retired` — where `os migrate meta` users will +look). + +The census (#11332, cloud leg #12400) measured ZERO reads of each container +itself across objectstack, objectui and cloud, with positive controls — which +settles every key beneath them at once, because a key cannot be read if the +object holding it never is. `configuration.properties.secret` is why this is +false compliance rather than tidying: its describe() promised "value is +encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or even +parsed the flag — `secret: true` next to an API key got exactly the same +handling as `secret: false`. `capabilities`' describe() sold "interoperability +and automatic discovery" no discovery path ever performed, and `extensions` +was an untyped `z.record(z.string(), z.unknown())` catch-all nothing +consulted. + +FROM → TO: + +- `manifest.capabilities` (`implements` / `provides` / `requires` / + `extensionPoints` / `extensions`) → *(removed — no replacement block)*. + Real dependency resolution runs off top-level `manifest.dependencies`, + which stays unchanged. `PluginCapabilityManifestSchema` itself stays + exported: the plugin-registry surface (`plugin-registry.zod.ts`) still + declares it. +- `manifest.configuration` (`{ title, properties }`) → pass options to the + plugin's constructor in `defineStack({ plugins: [new MyPlugin({ … })] })` — + the channel hosts already use, and the only one anything reads. +- `manifest.extensions` → the enforced extension channels: + `contributes.kinds` registers metadata kinds, `navigationContributions` + (ADR-0029 D7) injects navigation into other packages' apps, and code-level + extension lives in the plugin itself (`init`/`start`). + +One-line fix: delete the keys (they configured nothing); if you passed +settings via `configuration`, move them to the constructor options your host +already hands the plugin. + +The retirement kit: + +- `retiredKey()` tombstones on all three keys (`ManifestSchema` is not + `.strict()`, so a plain deletion would silently strip the key — the + `manifest.loading` precedent): authoring one is a `tsc` error (input typed + `never`) and a parse error carrying the prescription +- `kernel/Manifest:capabilities` / `kernel/Manifest:configuration` / + `kernel/Manifest:extensions` in `RETIRED_KEYS_BY_MAJOR[18]`, plus the D3 + semantic entry `plugin-manifest-dead-containers-retired`; deliberately NO + D2 conversion — a package manifest is not a stack collection member + (`PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry), so a conversion + would be a transform with no seam that ever runs (the + `kernel/Manifest:loading` reasoning) +- pin tests (`kernel/manifest.test.ts`): per-container rejection carrying the + prescription as the specific zod issue; live neighbours + (`dependencies`, `contributes.kinds`, `navigationContributions`) pinned + green +- the two in-repo authors (driver-memory and plugin-hono-server, each + writing `configuration` + `capabilities` blocks nothing read) stop + authoring the keys; liveness ledger rows move to the tombstone + disposition diff --git a/content/docs/protocol/kernel/plugin-spec.mdx b/content/docs/protocol/kernel/plugin-spec.mdx index 04367a2185..cf3622b0ad 100644 --- a/content/docs/protocol/kernel/plugin-spec.mdx +++ b/content/docs/protocol/kernel/plugin-spec.mdx @@ -20,7 +20,7 @@ A **plugin** is the unit of distribution in ObjectStack. It packages ObjectQL sc ## Plugin Manifest -Every plugin **must** have a manifest file that declares its identity, dependencies, and capabilities. +Every plugin **must** have a manifest file that declares its identity, dependencies, and contributions. ### Manifest Location @@ -170,17 +170,21 @@ export default definePlugin({ (`packages/spec/src/kernel/manifest.zod.ts`): `id`, `version`, `type` and `name` are required; the optional fields are `namespace`, `defaultDatasource`, `scope`, `description`, `permissions`, `objects`, `datasources`, `dependencies`, -`configuration`, `contributes`, `data`, `capabilities`, `extensions`, -`navigationContributions`, `loading`, `engine`, `engines`, `runtime`, `packaging` -and `integrity`. +`contributes`, `data`, `navigationContributions`, `engine`, `engines`, `runtime`, +`packaging` and `integrity`. The `configuration`, `capabilities`, `extensions` and +`loading` keys are **retired tombstones** (ADR-0049 enforce-or-remove): nothing ever +read any of them, so authoring one is now a compile error and a parse error carrying +the migration prescription. The `displayName` / `author` / `license` / `homepage` / `optionalDependencies` / `peerDependencies` / `metadata` / `config` / `marketplace` keys above are **proposal-only** — the schema declares none of them. Notably: compatibility ranges live in `engines: { platform, protocol }` (or the legacy `engine: { objectstack }`), -not `marketplace.compatibility`; config defaults live in -`configuration: { title, properties }` (a simplified JSON-Schema map with a per-key -`secret` flag), not `config.defaults` / `config.secrets`; and metadata globs are the +not `marketplace.compatibility`; plugin settings are **not** declared in the manifest +at all — the retired `configuration` block (whose per-key `secret` flag promised +encryption/masking that nothing implemented) configured nothing, and a plugin is +configured by the host that composes it, via the options object passed to its +constructor in `defineStack({ plugins: [...] })`; and metadata globs are the top-level `objects` / `datasources` arrays, not a `metadata` block. diff --git a/content/docs/references/api/package-api.mdx b/content/docs/references/api/package-api.mdx index a56046e597..efe1a61b11 100644 --- a/content/docs/references/api/package-api.mdx +++ b/content/docs/references/api/package-api.mdx @@ -199,11 +199,11 @@ Install package request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | @@ -321,11 +321,11 @@ Upgrade package request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | @@ -405,11 +405,11 @@ Resolve dependencies request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 67eee114ae..dc409726c9 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -1837,11 +1837,11 @@ Install package request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | diff --git a/content/docs/references/kernel/manifest.mdx b/content/docs/references/kernel/manifest.mdx index b97062b0f5..d92c24a58c 100644 --- a/content/docs/references/kernel/manifest.mdx +++ b/content/docs/references/kernel/manifest.mdx @@ -39,11 +39,11 @@ const result = ManifestSchema.parse(data); | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | @@ -63,13 +63,6 @@ Structured plugin permission grants (ADR-0025 §3.2) | **network** | `string[]` | optional | Network hosts the plugin may reach (e.g. "api.acme.com") | | **fs** | `string[]` | optional | Filesystem paths the plugin may access | -### Nested Shape: `Manifest.configuration` - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **title** | `string` | optional | | -| **properties** | `Record; default?: any; description?: string; required?: boolean; … }>` | ✅ | Map of configuration keys to their definitions | - ### Nested Shape: `Manifest.contributes` | Property | Type | Required | Description | @@ -103,16 +96,6 @@ Structured plugin permission grants (ADR-0025 §3.2) | **_packageVersion** | `string` | optional | Owning package version. | | **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. | -### Nested Shape: `Manifest.capabilities` - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **implements** | `{ protocol: object; conformance?: Enum<'full' \| 'partial' \| 'experimental' \| 'deprecated'>; implementedFeatures?: string[]; features?: object[]; … }[]` | optional | List of protocols this plugin conforms to | -| **provides** | `{ id: string; name: string; description?: string; version: object; … }[]` | optional | Services/APIs this plugin offers to others | -| **requires** | `{ pluginId: string; version: string; optional?: boolean; reason?: string; … }[]` | optional | Required plugins and their capabilities | -| **extensionPoints** | `{ id: string; name: string; description?: string; type: Enum<'action' \| 'hook' \| 'widget' \| 'provider' \| 'transformer' \| 'validator' \| 'decorator'>; … }[]` | optional | Points where other plugins can extend this plugin | -| **extensions** | `{ targetPluginId: string; extensionPointId: string; implementation: string; priority?: integer }[]` | optional | Extensions contributed to other plugins | - ### Nested Shape: `Manifest.navigationContributions[number]` A navigation contribution: a package injecting nav items into an app it does not own (ADR-0029 D7) diff --git a/content/docs/references/kernel/package-registry.mdx b/content/docs/references/kernel/package-registry.mdx index 721755b7f8..8821bc04bd 100644 --- a/content/docs/references/kernel/package-registry.mdx +++ b/content/docs/references/kernel/package-registry.mdx @@ -203,11 +203,11 @@ Install package request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | @@ -298,11 +298,11 @@ Installed package with runtime lifecycle state | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | diff --git a/content/docs/references/kernel/package-upgrade.mdx b/content/docs/references/kernel/package-upgrade.mdx index 18bd54c704..1b53cfc8aa 100644 --- a/content/docs/references/kernel/package-upgrade.mdx +++ b/content/docs/references/kernel/package-upgrade.mdx @@ -152,11 +152,11 @@ Upgrade package request | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | @@ -294,11 +294,11 @@ Pre-upgrade state snapshot for rollback capability | **objects** | `string[]` | optional | Glob patterns for ObjectQL schemas files | | **datasources** | `string[]` | optional | Glob patterns for Datasource definitions | | **dependencies** | `Record` | optional | Package dependencies | -| **configuration** | `{ title?: string; properties: Record }` | optional | Plugin configuration settings | +| **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** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability | -| **extensions** | `Record` | optional | Extension points and contributions | +| **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 \| … +8 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 — use the plugin trust tier (`manifest.runtime`) and the permission declarations, which are enforced. | | **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) | diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md index 94e9fe25a8..8b06d600ef 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.counts.md @@ -261,7 +261,7 @@ directory rather than per file. | `cloud/` | 83 | | `identity/` | 32 | | `integration/` | 10 | -| `kernel/` | 263 | +| `kernel/` | 261 | | `qa/` | 6 | | `shared/` | 20 | | `system/` | 364 | diff --git a/packages/drivers/driver-memory/objectstack.config.ts b/packages/drivers/driver-memory/objectstack.config.ts index 0070b176d6..1028c00472 100644 --- a/packages/drivers/driver-memory/objectstack.config.ts +++ b/packages/drivers/driver-memory/objectstack.config.ts @@ -16,236 +16,12 @@ const MemoryDriverPlugin: ObjectStackManifest = { scope: 'project', description: 'A reference specification implementation of the IDataDriver interface using in-memory arrays. Suitable for testing and development.', - configuration: { - title: 'Memory Driver Settings', - properties: { - seedData: { - type: 'boolean', - default: false, - description: 'Pre-populate the database with example data on startup' - } - } - }, - - // Plugin Capability Declaration - capabilities: { - // Protocols This Driver Implements - implements: [ - { - protocol: { - id: 'com.objectstack.protocol.storage.v1', - label: 'Storage Protocol v1', - version: { major: 1, minor: 0, patch: 0 }, - description: 'Standard data storage and retrieval operations', - }, - conformance: 'partial', - implementedFeatures: [ - 'basic_crud', - 'pagination', - ], - features: [ - { - name: 'basic_crud', - enabled: true, - description: 'Create, read, update, delete operations', - }, - { - name: 'pagination', - enabled: true, - description: 'Basic pagination via limit/offset', - }, - { - name: 'query_filters', - enabled: false, - description: 'Advanced query filtering', - }, - { - name: 'aggregations', - enabled: false, - description: 'Count, sum, avg operations', - }, - { - name: 'sorting', - enabled: false, - description: 'Result set sorting', - }, - { - name: 'transactions', - enabled: false, - description: 'ACID transaction support', - }, - { - name: 'joins', - enabled: false, - description: 'Cross-object joins', - }, - ], - certified: false, - }, - ], - - // Interfaces This Driver Provides - provides: [ - { - id: 'com.objectstack.driver.memory.interface.driver', - name: 'IDataDriver', - description: 'Standard ObjectStack driver interface for data operations', - version: { major: 1, minor: 0, patch: 0 }, - stability: 'stable', - methods: [ - { - name: 'connect', - description: 'Initialize driver connection', - parameters: [], - returnType: 'Promise', - async: true, - }, - { - name: 'disconnect', - description: 'Close driver connection', - parameters: [], - returnType: 'Promise', - async: true, - }, - { - name: 'create', - description: 'Create a new record', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'data', - type: 'Record', - required: true, - description: 'Record data', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'find', - description: 'Query records', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'query', - type: 'QueryInput', - required: false, - description: 'Query parameters', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'findOne', - description: 'Find a single record by ID', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'id', - type: 'string', - required: true, - description: 'Record ID', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'update', - description: 'Update a record', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'id', - type: 'string', - required: true, - description: 'Record ID', - }, - { - name: 'data', - type: 'Record', - required: true, - description: 'Updated record data', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'delete', - description: 'Delete a record', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'id', - type: 'string', - required: true, - description: 'Record ID', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'count', - description: 'Count records', - parameters: [ - { - name: 'object', - type: 'string', - required: true, - description: 'Object name', - }, - { - name: 'query', - type: 'QueryInput', - required: false, - description: 'Query parameters', - }, - ], - returnType: 'Promise', - async: true, - }, - ], - }, - ], - - // No external plugin dependencies (this is a core driver) - requires: [], - - // No extension points defined - extensionPoints: [], - - // No extensions contributed - extensions: [], - }, + // `configuration` and `capabilities` were retired (#11332, ADR-0049 + // enforce-or-remove): nothing ever read either container. The settings this + // driver needs are passed by the host that composes it (the options object + // handed to its constructor), and protocol/capability discovery never + // consulted the declaration — dependency resolution runs off top-level + // `dependencies`. // `contributes.drivers` was retired (#10724, ADR-0049): the declaration drove // nothing — this driver is wired by registering the `driver.memory` kernel diff --git a/packages/plugins/plugin-hono-server/objectstack.config.ts b/packages/plugins/plugin-hono-server/objectstack.config.ts index b18d19d301..5b902ebbdf 100644 --- a/packages/plugins/plugin-hono-server/objectstack.config.ts +++ b/packages/plugins/plugin-hono-server/objectstack.config.ts @@ -16,219 +16,12 @@ const HonoServerPlugin: ObjectStackManifest = { scope: 'project', description: 'HTTP server adapter using Hono framework. Exposes ObjectStack Runtime Protocol via REST API endpoints.', - configuration: { - title: 'Hono Server Configuration', - properties: { - port: { - type: 'number', - default: 3000, - description: 'HTTP server port', - }, - staticRoot: { - type: 'string', - description: 'Path to static files directory (optional)', - }, - }, - }, - - // Plugin Capability Declaration - capabilities: { - // Protocols This Plugin Implements - implements: [ - { - protocol: { - id: 'com.objectstack.protocol.http.v1', - label: 'HTTP Server Protocol v1', - version: { major: 1, minor: 0, patch: 0 }, - description: 'Standard HTTP server capabilities', - }, - conformance: 'full', - certified: false, - }, - { - protocol: { - id: 'com.objectstack.protocol.api.rest.v1', - label: 'REST API Protocol v1', - version: { major: 1, minor: 0, patch: 0 }, - description: 'RESTful API endpoint implementation', - }, - conformance: 'full', - features: [ - { - name: 'meta_protocol', - enabled: true, - description: 'Metadata discovery endpoints', - }, - { - name: 'data_protocol', - enabled: true, - description: 'CRUD data operations', - }, - { - name: 'ui_protocol', - enabled: true, - description: 'UI view metadata endpoints', - }, - ], - certified: false, - }, - ], - - // Interfaces This Plugin Provides - provides: [ - { - id: 'com.objectstack.server.hono.interface.http_server', - name: 'IHttpServer', - description: 'HTTP server service interface', - version: { major: 1, minor: 0, patch: 0 }, - stability: 'stable', - methods: [ - { - name: 'get', - description: 'Register GET route handler', - parameters: [ - { - name: 'path', - type: 'string', - required: true, - description: 'Route path pattern', - }, - { - name: 'handler', - type: 'Function', - required: true, - description: 'Route handler function', - }, - ], - returnType: 'void', - async: false, - }, - { - name: 'post', - description: 'Register POST route handler', - parameters: [ - { - name: 'path', - type: 'string', - required: true, - description: 'Route path pattern', - }, - { - name: 'handler', - type: 'Function', - required: true, - description: 'Route handler function', - }, - ], - returnType: 'void', - async: false, - }, - { - name: 'patch', - description: 'Register PATCH route handler', - parameters: [ - { - name: 'path', - type: 'string', - required: true, - description: 'Route path pattern', - }, - { - name: 'handler', - type: 'Function', - required: true, - description: 'Route handler function', - }, - ], - returnType: 'void', - async: false, - }, - { - name: 'delete', - description: 'Register DELETE route handler', - parameters: [ - { - name: 'path', - type: 'string', - required: true, - description: 'Route path pattern', - }, - { - name: 'handler', - type: 'Function', - required: true, - description: 'Route handler function', - }, - ], - returnType: 'void', - async: false, - }, - { - name: 'listen', - description: 'Start the HTTP server', - parameters: [ - { - name: 'port', - type: 'number', - required: true, - description: 'Port number', - }, - ], - returnType: 'Promise', - async: true, - }, - { - name: 'close', - description: 'Stop the HTTP server', - parameters: [], - returnType: 'void', - async: false, - }, - ], - }, - ], - - // Dependencies on Other Plugins/Services - requires: [ - { - pluginId: 'com.objectstack.engine.objectql', - version: '^0.6.0', - optional: true, - reason: 'ObjectStack Runtime Protocol implementation service', - requiredCapabilities: [ - 'com.objectstack.protocol.runtime.v1', - ], - }, - ], - - // Extension Points This Plugin Defines - extensionPoints: [ - { - id: 'com.objectstack.server.hono.extension.middleware', - name: 'HTTP Middleware', - description: 'Register custom HTTP middleware', - type: 'hook', - cardinality: 'multiple', - contract: { - signature: '(req: Request, res: Response, next: Function) => void | Promise', - }, - }, - { - id: 'com.objectstack.server.hono.extension.route', - name: 'Custom Routes', - description: 'Register custom API routes', - type: 'action', - cardinality: 'multiple', - contract: { - input: 'RouteDefinition', - signature: '(app: HonoApp) => void', - }, - }, - ], - - // No extensions contributed to other plugins - extensions: [], - }, + // `configuration` and `capabilities` were retired (#11332, ADR-0049 + // enforce-or-remove): nothing ever read either container. The port and + // static-root settings this adapter needs are passed by the host that + // composes it (the options object handed to its constructor), and + // protocol/capability discovery never consulted the declaration — + // dependency resolution runs off top-level `dependencies`. // `contributes.events` was retired (#10724, ADR-0049): the declaration drove // nothing — this plugin already subscribes to `kernel:ready` / `kernel:listening` diff --git a/packages/spec/authorable-surface/kernel.json b/packages/spec/authorable-surface/kernel.json index b685d2ce3b..f9d3af8850 100644 --- a/packages/spec/authorable-surface/kernel.json +++ b/packages/spec/authorable-surface/kernel.json @@ -279,8 +279,8 @@ "kernel/ListPackagesRequest:type", "kernel/ListPackagesResponse:packages", "kernel/ListPackagesResponse:total", - "kernel/Manifest:capabilities", - "kernel/Manifest:configuration", + "kernel/Manifest:capabilities [RETIRED]", + "kernel/Manifest:configuration [RETIRED]", "kernel/Manifest:contributes", "kernel/Manifest:data", "kernel/Manifest:datasources", @@ -289,7 +289,7 @@ "kernel/Manifest:description", "kernel/Manifest:engine", "kernel/Manifest:engines", - "kernel/Manifest:extensions", + "kernel/Manifest:extensions [RETIRED]", "kernel/Manifest:id", "kernel/Manifest:integrity", "kernel/Manifest:loading [RETIRED]", diff --git a/packages/spec/liveness/manifest.json b/packages/spec/liveness/manifest.json index 19a2230bd2..2c4abb6390 100644 --- a/packages/spec/liveness/manifest.json +++ b/packages/spec/liveness/manifest.json @@ -87,20 +87,10 @@ "note": "Read as a declaration rather than as an install instruction: an unresolvable dependency still counts toward the closure (the loop's own comment says so — the package declared it, so an object stamped with it is reachable by declaration), the transitive walk just stops there. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `protocol.ts:4290-4300` had rotted in range of this 20255-line file onto a docblock about `evaluateRuntimeAuthoringGate` staying pure and about advisories riding the 2xx, ~210 lines short of the read. The note's own inner citation (`the code says so at :4293-4296`) had rotted with it and is now stated symbol-relative instead of by line — a line inside a note is exactly as unfalsifiable as one the scanner cannot parse, since no check reads `note` at all. Re-closed by hand against c459da6bc." }, "configuration": { - "children": { - "title": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Dead by container: `manifest.configuration` has ZERO reads anywhere in objectstack or objectui (repo-wide probe for `(manifest|pkg|package|entry|artifact)?.configuration`, non-test, non-comment — no hits at all), so no key beneath it can be read. The describe() promises a settings surface the plugin \"exposes to the user via UI/ENV\"; nothing renders or resolves it. The live analogue is the settings service's own declaration surface (packages/services/service-settings), which is authored separately and never sourced from here." - }, - "properties": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Same zero as its sibling — the container is never read. This is the larger half of the loss: the per-setting shape declares `type` / `default` / `description` / `required` / `secret` / `enum`, and `secret: true` in particular reads as \"value is encrypted/masked (e.g. API Keys)\" while nothing encrypts, masks or even parses it. That is the false-compliance shape ADR-0049 exists for; filed as #11332 rather than acted on here, because this card seeds the ledger and does not retire keys." - } - } + "status": "dead", + "verifiedAt": "2026-08-29", + "evidenceScope": "cross-repo", + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#11332, ADR-0049), ADR-0087 D3 entry `plugin-manifest-dead-containers-retired`, retired-key entry `kernel/Manifest:configuration`. The container had ZERO reads in objectstack, objectui and cloud (#12400, cloud clean at 15f55df with positive controls), which settled both children at once — the per-child rows this row used to carry (title / properties, dead-by-container, verified 2026-08-23) leave the ledger with the children, because the tombstone removes the drilled shape beneath the key. `properties.*.secret` is why this was recorded as false compliance rather than tidying: its describe() promised \"value is encrypted/masked (e.g. API Keys)\" and nothing encrypted, masked or even parsed it. The enforced channel is host composition — the options object passed to the plugin's constructor in defineStack plugins." }, "contributes": { "children": { @@ -181,44 +171,16 @@ "note": "Deprecated in favour of `defineStack({ data })` but still read, so `live` rather than `dead`. 2026-08-28: RE-ANCHORED (#13003) and REPOINTED — `app-plugin.ts:946-947` had rotted in range onto the JOB scheduling block (`ok++` after a `svc.schedule(...)` with its retry policy), ~55 lines short of the read. That is the SAME rot batch 1 recorded one file over for `object.actions`, which cited `app-plugin.ts:929` and landed in the same job block: two ledger entries, two different keys, one drifted region. ANCHOR CHOICE, stated because it is weaker than the batch's others: the read is inline in `AppPlugin.start`, which has no enclosing named helper (contrast `collectBundleActions`, which batch 1 could anchor for `object.actions`), so the anchor names `seedDatasets` — the local the spread lands in, unique in the file and specific to this collection — rather than the 1840-line class. Re-closed by hand against c459da6bc. Its subtree is SeedSchema, classified in full by the governed `seed` type — recorded as a RESOLVED deferral (`manifest/data` → `seed`) in scripts/liveness/undrilled-containers.baseline.json rather than duplicated here, so a key added to SeedSchema cannot get a verdict on one side and not the other." }, "capabilities": { - "children": { - "implements": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Dead by container: `manifest.capabilities` (PluginCapabilityManifestSchema) has ZERO reads in objectstack or objectui. The `.capabilities` hits the probe returns belong to other surfaces entirely — driver loader contracts (packages/metadata/src/metadata-manager.ts:798), the QuickJS sandbox argument set (packages/runtime/src/sandbox/quickjs-runner.ts:120), REST discovery (packages/rest/src/rest-server.ts:3333) — none of them reached from a manifest." - }, - "provides": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Same zero as its siblings — the container is never read, so no key beneath it can be. The describe() sells plugin interoperability and automatic discovery; no discovery path consults it." - }, - "requires": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Same zero. Note the live analogue one level up: real dependency resolution runs off top-level `manifest.dependencies` (packages/metadata-protocol/src/protocol.ts:4290), not off this declaration." - }, - "extensionPoints": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Same zero. Declares extension points nothing enumerates — the sibling of `contributes.fieldTypes`, which declares an extension point with no registration seam." - }, - "extensions": { - "status": "dead", - "verifiedAt": "2026-08-23", - "evidenceScope": "cross-repo", - "note": "Same zero, and not to be confused with the manifest's own top-level `extensions` key, which is separately dead below." - } - } + "status": "dead", + "verifiedAt": "2026-08-29", + "evidenceScope": "cross-repo", + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#11332, ADR-0049), ADR-0087 D3 entry `plugin-manifest-dead-containers-retired`, retired-key entry `kernel/Manifest:capabilities`. The container (PluginCapabilityManifestSchema) had ZERO reads in objectstack, objectui and cloud (#12400, cloud clean at 15f55df with positive controls), which settled all five children at once — the per-child rows this row used to carry (implements / provides / requires / extensionPoints / extensions, each dead-by-container, verified 2026-08-23) leave the ledger with the children, because the tombstone removes the drilled shape beneath the key. Provenance worth keeping: the bare `.capabilities` hits a re-measurement will find belong to other surfaces (driver loader contracts at packages/metadata/src/metadata-manager.ts, the QuickJS sandbox argument set at packages/runtime/src/sandbox/quickjs-runner.ts, REST discovery at packages/rest/src/rest-server.ts, the ADR-0066 stack-level `capabilities` collection); real dependency resolution runs off top-level `manifest.dependencies` (packages/metadata-protocol/src/protocol.ts), never off `capabilities.requires`. `PluginCapabilityManifestSchema` stays published — the plugin-registry surface still declares it." }, "extensions": { "status": "dead", - "verifiedAt": "2026-08-23", + "verifiedAt": "2026-08-29", "evidenceScope": "cross-repo", - "note": "`z.record(z.string(), z.unknown())` — an untyped escape hatch with zero reads in objectstack or objectui (repo-wide probe, non-test, non-comment: no hits). Worth separating from the other dead keys when a removal is cut: because the value type is `unknown`, this key is where anything the platform does not yet model gets parked, so its emptiness is evidence that authors are not parking things here either." + "note": "RETIRED, and the row must STAY: `retiredKey()` keeps the key in the walked shape (a tombstone, not a strict removal; the `loading` precedent). Removed in @objectstack/spec 17.x (#11332, ADR-0049), ADR-0087 D3 entry `plugin-manifest-dead-containers-retired`, retired-key entry `kernel/Manifest:extensions`. Prior verdict kept as provenance: `z.record(z.string(), z.unknown())` — an untyped escape hatch with zero reads in all three repos; because the value type was `unknown`, this key is where anything the platform does not yet model would get parked, so its emptiness was evidence that authors were not parking things here either — an untyped catch-all with no users was the cheapest removal in the family. The enforced extension channels are `contributes.kinds`, `navigationContributions` (ADR-0029 D7), and plugin code itself." }, "navigationContributions": { "children": { diff --git a/packages/spec/liveness/state-counts.md b/packages/spec/liveness/state-counts.md index 2024a7e7f3..2781beeaf1 100644 --- a/packages/spec/liveness/state-counts.md +++ b/packages/spec/liveness/state-counts.md @@ -57,5 +57,5 @@ for both corollaries. | `api` | 25 | 0 | 0 | 2 | 27 | | `capability` | 12 | 0 | 0 | 0 | 12 | | `qa` | 4 | 0 | 5 | 0 | 9 | -| `manifest` | 22 | 0 | 21 | 0 | 43 | -| **total** | **819** | **5** | **80** | **10** | **914** | +| `manifest` | 22 | 0 | 16 | 0 | 38 | +| **total** | **819** | **5** | **75** | **10** | **909** | diff --git a/packages/spec/src/kernel/manifest.test.ts b/packages/spec/src/kernel/manifest.test.ts index 0e7c52b324..69f6abe71d 100644 --- a/packages/spec/src/kernel/manifest.test.ts +++ b/packages/spec/src/kernel/manifest.test.ts @@ -98,27 +98,9 @@ describe('ManifestSchema', () => { expect(() => ManifestSchema.parse(manifest)).not.toThrow(); }); - it('should accept manifest with extensions', () => { - const manifest: ObjectStackManifest = { - id: 'com.example.custom', - version: '1.0.0', - type: 'plugin', - name: 'Custom Extensions', - extensions: { - 'ui.components': [ - { - id: 'custom-widget', - component: 'CustomWidget', - }, - ], - 'api.hooks': { - 'before_save': 'validateData', - }, - }, - }; - - expect(() => ManifestSchema.parse(manifest)).not.toThrow(); - }); + // The `extensions` acceptance pin was removed with the key (#11332): the + // untyped catch-all had zero readers, so accepting it pinned a silent + // no-op. The rejection is pinned with the dead-container retirement below. }); describe('Real-World Manifest Examples', () => { @@ -147,24 +129,7 @@ describe('ManifestSchema', () => { './objects/contact.object.ts', './objects/campaign.object.ts', ], - extensions: { - 'dashboard.widgets': [ - { - id: 'sales-pipeline', - name: 'Sales Pipeline', - component: 'SalesPipelineWidget', - }, - { - id: 'revenue-forecast', - name: 'Revenue Forecast', - component: 'RevenueForecastWidget', - }, - ], - 'workflows': { - 'lead_conversion': './workflows/lead-conversion.yml', - 'opportunity_close': './workflows/opportunity-close.yml', - }, - }, + // `extensions` retired (#11332) — nothing ever read the container. }; expect(() => ManifestSchema.parse(crmManifest)).not.toThrow(); @@ -209,21 +174,7 @@ describe('ManifestSchema', () => { 'system.auth.configure', 'system.user.create', ], - extensions: { - 'auth.providers': { - id: 'saml', - name: 'SAML 2.0', - configSchema: 'saml-config.schema.json', - handler: 'SAMLAuthHandler', - }, - 'admin.settings': [ - { - page: 'saml-settings', - label: 'SAML Configuration', - component: 'SAMLSettingsPage', - }, - ], - }, + // `extensions` retired (#11332) — nothing ever read the container. }; expect(() => ManifestSchema.parse(authPlugin)).not.toThrow(); @@ -239,14 +190,7 @@ describe('ManifestSchema', () => { permissions: [ 'system.datasource.manage', ], - extensions: { - 'datasource.types': { - id: 'postgresql', - name: 'PostgreSQL', - driver: 'PostgreSQLDriver', - features: ['transactions', 'jsonb', 'full-text-search'], - }, - }, + // `extensions` retired (#11332) — nothing ever read the container. }; expect(() => ManifestSchema.parse(dbDriver)).not.toThrow(); @@ -310,21 +254,8 @@ describe('ManifestSchema', () => { type: 'adapter', name: 'Express Adapter', description: 'Express.js HTTP server adapter for ObjectStack runtime', - configuration: { - title: 'Express Server Settings', - properties: { - port: { - type: 'number', - default: 3000, - description: 'HTTP server port', - }, - corsEnabled: { - type: 'boolean', - default: true, - description: 'Enable CORS middleware', - }, - }, - }, + // `configuration` retired (#11332) — the settings block had no reader; + // a plugin is configured by its host at composition time. }; expect(() => ManifestSchema.parse(expressAdapter)).not.toThrow(); @@ -544,3 +475,69 @@ describe('contributes.kinds[].globs retirement (#11169, ADR-0049 — maintainer- expect(parsed.contributes!.kinds![0]).not.toHaveProperty('globs'); }); }); + +describe('dead-container retirement (#11332, ADR-0049 — tombstoned, not deleted)', () => { + // Three top-level manifest containers had ZERO reads of the container itself + // monorepo-wide (objectstack + objectui + cloud, controlled census), which + // settles every key beneath them at once — a key cannot be read if the + // object holding it never is. `ManifestSchema` is NOT `.strict()`, so a + // plain deletion would have silently stripped the keys — `retiredKey()` is + // what makes each rejection carry the prescription, and the prescription is + // what these pins assert (the specific zod issue, never just "it threw"). + const base = { id: 'com.example.retired', version: '1.0.0', type: 'plugin', name: 'Retired' }; + const authored: Array<[container: string, value: unknown, mustMention: RegExp]> = [ + [ + 'capabilities', + { implements: [], provides: [], extensionPoints: [] }, + // The prescription names the live analogue: dependency resolution runs + // off top-level `manifest.dependencies`, never off this block. + /manifest\.dependencies/, + ], + [ + 'configuration', + { title: 'Cfg', properties: { apiKey: { type: 'string', secret: true } } }, + // The false promise is the point of the removal: the prescription must + // record that `secret` never encrypted or masked anything. + /secret/, + ], + [ + 'extensions', + { 'ui.components': [{ id: 'w' }] }, + // The prescription redirects to the enforced extension channels. + /contributes\.kinds/, + ], + ]; + + it.each(authored)('REJECTS an authored `%s` with the prescription as the issue', (container, value, mustMention) => { + const result = ManifestSchema.safeParse({ ...base, [container]: value }); + expect(result.success).toBe(false); + if (result.success) return; + // The SPECIFIC zod issue: located at the retired key, carrying the + // fully-qualified key, the removal record, and the imperative fix. + const issue = result.error.issues.find((i) => i.path[0] === container); + expect(issue).toBeDefined(); + expect(issue!.message).toMatch( + new RegExp(`manifest\\.${container}.*removed in @objectstack/spec 17.*Delete the key`, 's'), + ); + expect(issue!.message).toMatch(mustMention); + }); + + it('parses cleanly with the retired containers simply absent', () => { + const parsed = ManifestSchema.parse(base); + expect(parsed).not.toHaveProperty('capabilities'); + expect(parsed).not.toHaveProperty('configuration'); + expect(parsed).not.toHaveProperty('extensions'); + }); + + it('still parses the live neighbours — `dependencies` and `navigationContributions` are untouched', () => { + const parsed = ManifestSchema.parse({ + ...base, + dependencies: { '@objectstack/plugin-auth': '^2.0.0' }, + navigationContributions: [ + { app: 'setup', items: [{ id: 'nav_x', type: 'url', label: 'X', url: '/x' }] }, + ], + }); + expect(parsed.dependencies).toEqual({ '@objectstack/plugin-auth': '^2.0.0' }); + expect(parsed.navigationContributions).toHaveLength(1); + }); +}); diff --git a/packages/spec/src/kernel/manifest.zod.ts b/packages/spec/src/kernel/manifest.zod.ts index 3890bc98a3..ce6a87a5a0 100644 --- a/packages/spec/src/kernel/manifest.zod.ts +++ b/packages/spec/src/kernel/manifest.zod.ts @@ -1,7 +1,6 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { z } from 'zod'; -import { PluginCapabilityManifestSchema } from './plugin-capability.zod'; import { CORE_PLUGIN_TYPES } from './plugin.zod'; import { retiredKey } from '../shared/retired-key'; import { SeedSchema } from '../data/seed.zod'; @@ -289,30 +288,32 @@ export const ManifestSchema = z.object({ dependencies: z.record(z.string(), z.string()).optional().describe('Package dependencies'), /** - * Plugin Configuration Schema. - * Defines the settings this plugin exposes to the user via UI/ENV. - * Uses a simplified JSON Schema format. - * - * @example - * { - * "title": "Stripe Config", - * "properties": { - * "apiKey": { "type": "string", "secret": true }, - * "currency": { "type": "string", "default": "USD" } - * } - * } + * REMOVED (#11332, ADR-0049 enforce-or-remove). + * + * `configuration` declared a per-plugin settings surface — `{ title, + * properties }`, a simplified JSON-Schema map — and NOTHING read the + * container anywhere: no settings UI rendered it, no loader resolved a + * setting from it. Its `properties.*.secret` flag is why this is a + * false-compliance finding rather than tidying: the describe() promised + * "value is encrypted/masked (e.g. API Keys)" while nothing encrypted, + * masked, resolved or even parsed the value — an author writing + * `secret: true` next to an API key got exactly the same handling as + * `secret: false`. Tombstoned rather than deleted because `ManifestSchema` + * is not `.strict()`: a plain deletion would silently strip the key (the + * `loading` precedent below). */ - configuration: z.object({ - title: z.string().optional(), - properties: z.record(z.string(), z.object({ - type: z.enum(['string', 'number', 'boolean', 'array', 'object']).describe('Data type of the setting'), - default: z.unknown().optional().describe('Default value'), - description: z.string().optional().describe('Tooltip description'), - required: z.boolean().optional().describe('Is this setting required?'), - secret: z.boolean().optional().describe('If true, value is encrypted/masked (e.g. API Keys)'), - enum: z.array(z.string()).optional().describe('Allowed values for select inputs'), - })).describe('Map of configuration keys to their definitions') - }).optional().describe('Plugin configuration settings'), + configuration: retiredKey( + '`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.', + ), /** * Contribution Points (VS Code Style). @@ -482,18 +483,51 @@ export const ManifestSchema = z.object({ data: z.array(SeedSchema).optional().describe('Initial seed data (prefer top-level data field)'), /** - * Plugin Capability Manifest. - * Declares protocols implemented, interfaces provided, dependencies, and extension points. - * This enables plugin interoperability and automatic discovery. + * REMOVED (#11332, ADR-0049 enforce-or-remove). + * + * `capabilities` carried the whole `PluginCapabilityManifestSchema` block — + * `implements`, `provides`, `requires`, `extensionPoints`, `extensions` — + * and NOTHING read the container anywhere, which settles every key beneath + * it at once. Its describe() sold "plugin interoperability and automatic + * discovery"; no discovery path ever consulted it, and real dependency + * resolution runs off top-level `manifest.dependencies`, not + * `capabilities.requires`. The bare `.capabilities` hits a re-measurement + * will find belong to other surfaces entirely (driver loader contracts, + * the QuickJS sandbox argument set, REST discovery, the ADR-0066 + * stack-level `capabilities` collection) — none is reached from a + * manifest. `PluginCapabilityManifestSchema` itself stays exported: the + * plugin-registry surface (`plugin-registry.zod.ts`) still declares it. + * Tombstoned rather than deleted because `ManifestSchema` is not + * `.strict()` (the `loading` precedent below). */ - capabilities: PluginCapabilityManifestSchema.optional() - .describe('Plugin capability declarations for interoperability'), + capabilities: retiredKey( + '`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.', + ), - /** - * Extension points contributed by this package. - * Allows packages to extend UI components, add functionality, etc. + /** + * REMOVED (#11332, ADR-0049 enforce-or-remove). + * + * `extensions` was an untyped escape hatch — `z.record(z.string(), + * z.unknown())` — with zero readers anywhere, so whatever an author parked + * here was stored and never consulted. Its emptiness in-repo was itself + * evidence: an untyped catch-all with no users is a cheaper removal than + * one with unknown users. Tombstoned rather than deleted because + * `ManifestSchema` is not `.strict()` (the `loading` precedent below). */ - extensions: z.record(z.string(), z.unknown()).optional().describe('Extension points and contributions'), + extensions: retiredKey( + '`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`).', + ), /** * Navigation contributions (ADR-0029 D7). diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__capabilities.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__capabilities.ts new file mode 100644 index 0000000000..4c67c40dc7 --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__capabilities.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead +// top-level containers (triage graded 2026-08-23; cloud leg measured clean +// 2026-08-29 on #12400 with positive controls). The census found ZERO reads +// of the `capabilities` container itself in objectstack, objectui and cloud, +// which settles all five keys beneath it (`implements`, `provides`, +// `requires`, `extensionPoints`, `extensions`) at once — a key cannot be read +// if the object holding it never is. Its describe() sold "interoperability +// and automatic discovery"; no discovery path consulted it, and real +// dependency resolution runs off top-level `manifest.dependencies`. ONE +// tombstoned key, because `capabilities` was the single carrier (the +// `kernel/Manifest:loading` shape); `PluginCapabilityManifestSchema` itself +// stays published — the plugin-registry surface still declares it — so +// nothing lands in `RETIRED_DEFS_BY_MAJOR`. +// +// Registered under 18, not 17: v17.0.0 was cut before this landed, so the +// tombstone ships on the 17.x line (launch-window convention) and the +// prescription lives at the major boundary where `migrate meta` users look. +// +// Registered here but NOT in `src/conversions/registry.ts`, for the reason +// `kernel/Manifest:loading` gives: a package manifest is not a stack +// collection member (`PLURAL_TO_SINGULAR` has no `packages` / `plugins` +// entry — re-verified at claim; the map's `capabilities` entry is the +// unrelated ADR-0066 stack-level collection), so a D2 conversion would be a +// transform with no seam that ever runs. The prescription reaches authors +// through the tombstone at `os plugin build` → `ManifestSchema.safeParse` +// and through the D3 semantic entry +// `plugin-manifest-dead-containers-retired`. +export const entry = 'kernel/Manifest:capabilities'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__configuration.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__configuration.ts new file mode 100644 index 0000000000..22b1334a3f --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__configuration.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead +// top-level containers; census and registration major recorded once in the +// sibling entry `kernel/Manifest:capabilities`, the why-no-D2-conversion +// reasoning in `kernel/Manifest:loading` (the precedent); the D3 semantic +// entry is `plugin-manifest-dead-containers-retired`. +// +// `configuration` declared a per-plugin settings surface (`{ title, +// properties }`, a simplified JSON-Schema map) that no settings UI rendered +// and no loader resolved. Its `properties.*.secret` flag is the +// false-compliance shape ADR-0049 exists for: the describe() promised "value +// is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or +// even parsed the flag — `secret: true` next to an API key got exactly the +// same handling as `secret: false`. The enforced channel is host +// composition: the options object passed to the plugin's constructor in +// `defineStack({ plugins: [new MyPlugin({ … })] })`. +export const entry = 'kernel/Manifest:configuration'; diff --git a/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__extensions.ts b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__extensions.ts new file mode 100644 index 0000000000..5d4ff8ffed --- /dev/null +++ b/packages/spec/src/migrations/entries/retired-keys/18.kernel__Manifest__extensions.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +// #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead +// top-level containers; census and registration major recorded once in the +// sibling entry `kernel/Manifest:capabilities`, the why-no-D2-conversion +// reasoning in `kernel/Manifest:loading` (the precedent); the D3 semantic +// entry is `plugin-manifest-dead-containers-retired`. +// +// `extensions` was an untyped escape hatch — `z.record(z.string(), +// z.unknown())` — with zero readers, so whatever an author parked here was +// stored and never consulted. Because the value type is `unknown`, this key +// is where anything the platform does not yet model would get parked; its +// measured emptiness is therefore its own evidence — authors are not parking +// things here either, making an untyped catch-all with no users the cheapest +// removal in the family. The enforced extension channels are +// `contributes.kinds` (metadata kinds), `navigationContributions` +// (ADR-0029 D7), and plugin code itself (`init`/`start`). +export const entry = 'kernel/Manifest:extensions'; diff --git a/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts new file mode 100644 index 0000000000..3db83d6a21 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.plugin-manifest-dead-containers-retired.ts @@ -0,0 +1,62 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +export const entry: SemanticMigration = { + id: 'plugin-manifest-dead-containers-retired', + surface: + 'manifest.capabilities / manifest.configuration / manifest.extensions (three top-level ' + + 'containers; retiring the container settles every key beneath it — ' + + '`capabilities.{implements,provides,requires,extensionPoints,extensions}` and ' + + '`configuration.{title,properties}` — at once)', + replacement: + 'delete the keys — each declared purpose either has its one enforced channel or never ' + + 'existed: `configuration` (a `{ title, properties }` settings surface no UI rendered ' + + 'and no loader resolved) → pass options to the plugin\'s constructor in ' + + '`defineStack({ plugins: [new MyPlugin({ … })] })`, the channel hosts already use; ' + + '`capabilities` (protocol/interface declarations sold as "interoperability and ' + + 'automatic discovery") → nothing — no discovery path ever existed; real dependency ' + + 'resolution runs off top-level `manifest.dependencies`, which stays; `extensions` (an ' + + 'untyped `z.record(z.string(), z.unknown())` catch-all) → the enforced extension ' + + 'channels: `contributes.kinds` registers metadata kinds, `navigationContributions` ' + + '(ADR-0029 D7) injects navigation, and code-level extension lives in the plugin itself ' + + '(`init`/`start`)', + reason: + 'ADR-0049 enforce-or-remove; #11332 (triage graded 2026-08-23, cloud precondition ' + + 'discharged 2026-08-29 on #12400). #11332 measured, monorepo-wide and non-test with ' + + 'control probes, ZERO reads of each container itself, which settles all eight keys ' + + 'beneath them — a key cannot be read if the object holding it never is. The census ' + + 'stands on three repos: objectstack (re-verified on current main at claim time; every ' + + 'bare `.capabilities` hit classifies to a different surface — driver loader contracts, ' + + 'the QuickJS sandbox argument set, REST discovery, the ADR-0066 stack-level ' + + '`capabilities` collection), objectui (0 container reads; control: `manifest.(id|name|' + + 'namespace|version)` reads findable), and cloud (measured clean 2026-08-29 at ' + + '`15f55df`: zero reads of all three, controls positive). ' + + '`configuration.properties.secret` made this false compliance rather than tidying: its ' + + 'describe() promised "value is encrypted/masked (e.g. API Keys)" and nothing ever ' + + 'encrypted, masked or parsed it, so the key\'s own text was an unkept assurance about ' + + 'credential handling. ' + + 'Why D3 semantic and not a D2 conversion: the conversion chain walks a normalized ' + + 'STACK and `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry (re-verified — its ' + + '`capabilities` entry is the unrelated ADR-0066 stack collection), so a manifest is ' + + 'not a stack collection member and a conversion would be a transform with no seam that ' + + 'ever runs (the `kernel/Manifest:loading` precedent, recorded verbatim in its ' + + 'retired-key entry). `PluginCapabilityManifestSchema` stays published: the ' + + 'plugin-registry surface (`plugin-registry.zod.ts`) still declares it, so this is a ' + + 'carrier-key tombstone with no def removal.', + acceptanceCriteria: + 'No `objectstack.config.ts` manifest and no packaged `manifest.json` authors any of the ' + + 'three containers (the two in-repo authors — driver-memory and plugin-hono-server, ' + + 'both writing `configuration` and `capabilities` blocks nothing read — were cleaned ' + + 'with this retirement). The enforced channel is the one place a manifest is parsed ' + + 'with an author present: `os plugin build` runs `ManifestSchema.safeParse` and exits ' + + 'non-zero printing the per-key tombstone prescription; TypeScript authors fail earlier ' + + 'still (each key is typed `never`). Live neighbours are untouched and must be verified ' + + 'as such: `manifest.dependencies` keeps resolving dependencies, `contributes.kinds` ' + + 'keeps registering, `navigationContributions` keeps merging. ⚠️ Runtime behaviour is ' + + 'deliberately UNCHANGED: nothing ever read the three containers, so removing them ' + + 'removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries one ' + + 'degrades to a single `[metadata_spec_invalid]` log line at registration (the ' + + 'registry\'s `validate()` is a diagnostic, not a gate) rather than a boot failure; ' + + 'clear it by deleting the key from the source manifest and reinstalling.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 02b7716e01..a516f8dd43 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -6981,6 +6981,64 @@ const step18: MigrationStep = { + 'is a diagnostic, not a gate) rather than a boot failure; clear it by deleting the ' + 'key from the source manifest and reinstalling.', }, + { + id: 'plugin-manifest-dead-containers-retired', + surface: + 'manifest.capabilities / manifest.configuration / manifest.extensions (three top-level ' + + 'containers; retiring the container settles every key beneath it — ' + + '`capabilities.{implements,provides,requires,extensionPoints,extensions}` and ' + + '`configuration.{title,properties}` — at once)', + replacement: + 'delete the keys — each declared purpose either has its one enforced channel or never ' + + 'existed: `configuration` (a `{ title, properties }` settings surface no UI rendered ' + + 'and no loader resolved) → pass options to the plugin\'s constructor in ' + + '`defineStack({ plugins: [new MyPlugin({ … })] })`, the channel hosts already use; ' + + '`capabilities` (protocol/interface declarations sold as "interoperability and ' + + 'automatic discovery") → nothing — no discovery path ever existed; real dependency ' + + 'resolution runs off top-level `manifest.dependencies`, which stays; `extensions` (an ' + + 'untyped `z.record(z.string(), z.unknown())` catch-all) → the enforced extension ' + + 'channels: `contributes.kinds` registers metadata kinds, `navigationContributions` ' + + '(ADR-0029 D7) injects navigation, and code-level extension lives in the plugin itself ' + + '(`init`/`start`)', + reason: + 'ADR-0049 enforce-or-remove; #11332 (triage graded 2026-08-23, cloud precondition ' + + 'discharged 2026-08-29 on #12400). #11332 measured, monorepo-wide and non-test with ' + + 'control probes, ZERO reads of each container itself, which settles all eight keys ' + + 'beneath them — a key cannot be read if the object holding it never is. The census ' + + 'stands on three repos: objectstack (re-verified on current main at claim time; every ' + + 'bare `.capabilities` hit classifies to a different surface — driver loader contracts, ' + + 'the QuickJS sandbox argument set, REST discovery, the ADR-0066 stack-level ' + + '`capabilities` collection), objectui (0 container reads; control: `manifest.(id|name|' + + 'namespace|version)` reads findable), and cloud (measured clean 2026-08-29 at ' + + '`15f55df`: zero reads of all three, controls positive). ' + + '`configuration.properties.secret` made this false compliance rather than tidying: its ' + + 'describe() promised "value is encrypted/masked (e.g. API Keys)" and nothing ever ' + + 'encrypted, masked or parsed it, so the key\'s own text was an unkept assurance about ' + + 'credential handling. ' + + 'Why D3 semantic and not a D2 conversion: the conversion chain walks a normalized ' + + 'STACK and `PLURAL_TO_SINGULAR` has no `packages` / `plugins` entry (re-verified — its ' + + '`capabilities` entry is the unrelated ADR-0066 stack collection), so a manifest is ' + + 'not a stack collection member and a conversion would be a transform with no seam that ' + + 'ever runs (the `kernel/Manifest:loading` precedent, recorded verbatim in its ' + + 'retired-key entry). `PluginCapabilityManifestSchema` stays published: the ' + + 'plugin-registry surface (`plugin-registry.zod.ts`) still declares it, so this is a ' + + 'carrier-key tombstone with no def removal.', + acceptanceCriteria: + 'No `objectstack.config.ts` manifest and no packaged `manifest.json` authors any of the ' + + 'three containers (the two in-repo authors — driver-memory and plugin-hono-server, ' + + 'both writing `configuration` and `capabilities` blocks nothing read — were cleaned ' + + 'with this retirement). The enforced channel is the one place a manifest is parsed ' + + 'with an author present: `os plugin build` runs `ManifestSchema.safeParse` and exits ' + + 'non-zero printing the per-key tombstone prescription; TypeScript authors fail earlier ' + + 'still (each key is typed `never`). Live neighbours are untouched and must be verified ' + + 'as such: `manifest.dependencies` keeps resolving dependencies, `contributes.kinds` ' + + 'keeps registering, `navigationContributions` keeps merging. ⚠️ Runtime behaviour is ' + + 'deliberately UNCHANGED: nothing ever read the three containers, so removing them ' + + 'removes no behaviour. A package ALREADY INSTALLED whose stored manifest carries one ' + + 'degrades to a single `[metadata_spec_invalid]` log line at registration (the ' + + 'registry\'s `validate()` is a diagnostic, not a gate) rather than a boot failure; ' + + 'clear it by deleting the key from the source manifest and reinstalling.', + }, { id: 'plugin-manifest-kind-globs-retired', surface: 'manifest.contributes.kinds[].globs (the `kind` bucket itself and its `id` are untouched)', @@ -7990,6 +8048,50 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // narrowings ride minor releases) and the prescription lives at the major // boundary where `migrate meta` users look (the #8495 / PR #8666 precedent). 'kernel/KernelContext:previewMode', + // #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead + // top-level containers (triage graded 2026-08-23; cloud leg measured clean + // 2026-08-29 on #12400 with positive controls). The census found ZERO reads + // of the `capabilities` container itself in objectstack, objectui and cloud, + // which settles all five keys beneath it (`implements`, `provides`, + // `requires`, `extensionPoints`, `extensions`) at once — a key cannot be read + // if the object holding it never is. Its describe() sold "interoperability + // and automatic discovery"; no discovery path consulted it, and real + // dependency resolution runs off top-level `manifest.dependencies`. ONE + // tombstoned key, because `capabilities` was the single carrier (the + // `kernel/Manifest:loading` shape); `PluginCapabilityManifestSchema` itself + // stays published — the plugin-registry surface still declares it — so + // nothing lands in `RETIRED_DEFS_BY_MAJOR`. + // + // Registered under 18, not 17: v17.0.0 was cut before this landed, so the + // tombstone ships on the 17.x line (launch-window convention) and the + // prescription lives at the major boundary where `migrate meta` users look. + // + // Registered here but NOT in `src/conversions/registry.ts`, for the reason + // `kernel/Manifest:loading` gives: a package manifest is not a stack + // collection member (`PLURAL_TO_SINGULAR` has no `packages` / `plugins` + // entry — re-verified at claim; the map's `capabilities` entry is the + // unrelated ADR-0066 stack-level collection), so a D2 conversion would be a + // transform with no seam that ever runs. The prescription reaches authors + // through the tombstone at `os plugin build` → `ManifestSchema.safeParse` + // and through the D3 semantic entry + // `plugin-manifest-dead-containers-retired`. + 'kernel/Manifest:capabilities', + // #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead + // top-level containers; census and registration major recorded once in the + // sibling entry `kernel/Manifest:capabilities`, the why-no-D2-conversion + // reasoning in `kernel/Manifest:loading` (the precedent); the D3 semantic + // entry is `plugin-manifest-dead-containers-retired`. + // + // `configuration` declared a per-plugin settings surface (`{ title, + // properties }`, a simplified JSON-Schema map) that no settings UI rendered + // and no loader resolved. Its `properties.*.secret` flag is the + // false-compliance shape ADR-0049 exists for: the describe() promised "value + // is encrypted/masked (e.g. API Keys)" while nothing encrypted, masked or + // even parsed the flag — `secret: true` next to an API key got exactly the + // same handling as `secret: false`. The enforced channel is host + // composition: the options object passed to the plugin's constructor in + // `defineStack({ plugins: [new MyPlugin({ … })] })`. + 'kernel/Manifest:configuration', // #10724 — ADR-0049 enforce-or-remove on the plugin manifest's `contributes` // block; one of NINE members tombstoned together. Census, registration major, // and the why-no-D2-conversion reasoning are recorded once in the sibling @@ -8159,6 +8261,22 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly> // `translations` collection (`defineTranslationBundle`), governed by // `packages/spec/liveness/translation.json`. 'kernel/Manifest:contributes.translations', + // #11332 — ADR-0049 enforce-or-remove on the plugin manifest's three dead + // top-level containers; census and registration major recorded once in the + // sibling entry `kernel/Manifest:capabilities`, the why-no-D2-conversion + // reasoning in `kernel/Manifest:loading` (the precedent); the D3 semantic + // entry is `plugin-manifest-dead-containers-retired`. + // + // `extensions` was an untyped escape hatch — `z.record(z.string(), + // z.unknown())` — with zero readers, so whatever an author parked here was + // stored and never consulted. Because the value type is `unknown`, this key + // is where anything the platform does not yet model would get parked; its + // measured emptiness is therefore its own evidence — authors are not parking + // things here either, making an untyped catch-all with no users the cheapest + // removal in the family. The enforced extension channels are + // `contributes.kinds` (metadata kinds), `navigationContributions` + // (ADR-0029 D7), and plugin code itself (`init`/`start`). + 'kernel/Manifest:extensions', // #13135 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-29 on // #12057, adopting retirement; re-charter #13135 executes the widened // surface). `persistence.overlayWritable` gated exactly one method —