From eb4cd2956ce46071da4840088eb4f126bc401ba9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 12:16:30 +0000 Subject: [PATCH] =?UTF-8?q?refactor(spec)!:=20retire=20the=20plugin-runtim?= =?UTF-8?q?e=20family=20=E2=80=94=20the=20"Dynamic=20Loading"=20vocabulary?= =?UTF-8?q?=20no=20runtime=20implemented=20(#4834,=20ADR-0049)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-0049 enforce-or-remove, maintainer-ruled REMOVE. `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`, `PluginSourceSchema` and `DynamicPluginOperationSchema` (+ seven type aliases) declared runtime load / unload / reload of plugins without a kernel restart — sources, integrity hashes, sandboxing, drain strategies, dependent-cascade policy. None of those operations exists. Bare-name scan of objectstack, cloud (5df2c69) and objectui (785b8a5): zero references outside this package's declaration, its unit tests and the generated artifacts, each empty result control-verified against a known-present neighbour. The whole module is deleted. #3896 removed this module's discovery/sandbox config island and left these five in place explicitly ("operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction") — a suspension that lived only in a changeset paragraph with no issue carrying it. #4834 is that decision. Route: whole-def removal (#4650 route 3). No `retiredKey()` tombstones — nothing parses these schemas, so a prescription could never be received; the same disposition this module set in #3896. The `json-schema.manifest.json` ratchet (#2978) demanded the five def keys be deleted deliberately, and gen:schema then reported all 23 authorable-surface deletions as carrying their own proof ("def no longer emitted by this build"). This subsumes the kernel half of #4657: `DynamicLoadRequest.activationEvents`'s `retiredKey()` tombstone goes with the shape that carried it, which is strictly stronger than the tombstone ("this request shape does not exist" > "this one key does not exist"). The studio half (`StudioPluginManifest.activationEvents`) is untouched and still enforced. The #4657 D3 entry is corrected rather than deleted — its studio half is live and its history must keep replaying. - ADR-0087 D3 semantic migration `plugin-runtime-family-retired` (no `sys_metadata` source exists for a D2 rewrite: these are root request/result payload shapes, embedded in no parent schema) - compiler-API export pin with three anti-vacuity guards, `holdersOf(name)` exactly `[]` across all 16 entries — sabotage-verified - PLUGIN_STANDARDS.md §5.3 + capability table now say "Not built", not ✅ - baselines regenerated from rebuilt source; changeset carries the merged two-layer migration narrative Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- .changeset/retire-plugin-runtime-family.md | 116 ++++++++ content/docs/references/kernel/index.mdx | 1 - content/docs/references/kernel/meta.json | 1 - .../docs/references/kernel/plugin-runtime.mdx | 131 --------- docs/protocol-upgrade-guide.md | 9 +- packages/spec/PLUGIN_STANDARDS.md | 30 +- packages/spec/PROTOCOL_MAP.md | 1 - packages/spec/api-surface.json | 12 - packages/spec/authorable-surface.json | 23 -- packages/spec/json-schema.manifest.json | 5 - packages/spec/spec-changes.json | 18 +- .../activation-events-retirement.test.ts | 53 ++-- packages/spec/src/kernel/index.ts | 34 ++- .../kernel/plugin-runtime-retirement.test.ts | 140 +++++++++ .../spec/src/kernel/plugin-runtime.test.ts | 200 ------------- .../spec/src/kernel/plugin-runtime.zod.ts | 275 ------------------ packages/spec/src/migrations/registry.ts | 83 +++++- 17 files changed, 440 insertions(+), 692 deletions(-) create mode 100644 .changeset/retire-plugin-runtime-family.md delete mode 100644 content/docs/references/kernel/plugin-runtime.mdx create mode 100644 packages/spec/src/kernel/plugin-runtime-retirement.test.ts delete mode 100644 packages/spec/src/kernel/plugin-runtime.test.ts delete mode 100644 packages/spec/src/kernel/plugin-runtime.zod.ts diff --git a/.changeset/retire-plugin-runtime-family.md b/.changeset/retire-plugin-runtime-family.md new file mode 100644 index 0000000000..8a44268715 --- /dev/null +++ b/.changeset/retire-plugin-runtime-family.md @@ -0,0 +1,116 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: remove the `plugin-runtime.zod` family — the "Dynamic Loading" vocabulary no runtime ever implemented (#4834, ADR-0049) + +`DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, +`DynamicPluginResultSchema`, `PluginSourceSchema` and +`DynamicPluginOperationSchema` — with every type alias — are **removed from +`@objectstack/spec` and `@objectstack/spec/kernel`**. The module they lived in +is deleted. + +Together they declared the platform's "Dynamic Loading" capability: runtime +load / unload / reload of plugins **without restarting the kernel**, resolved +from `npm` / `local` / `url` / `registry` / `git` sources, with Subresource +Integrity hashes, per-plugin sandboxing, `graceful` / `forceful` / `drain` +unload strategies and `cascade` / `warn` / `block` dependent policy. **None of +those operations exists.** A bare-name scan of objectstack, cloud and objectui +(at the commits above) found zero references outside this package's own +declaration, its unit tests and the generated artifacts: no runtime ever +received a `DynamicLoadRequest`, performed a load or an unload, or produced a +`DynamicPluginResult`. Plugins are composed at boot — `defineStack` registers +them and the kernel runs `init` → `start` — and the set is fixed until the +process restarts. + +This closes a suspension that has been open, and undocumented outside one +paragraph, since #3896. That change removed this module's discovery/sandbox +config island and wrote down that "the remainder … also has no runtime consumer +today; it is left in place because those are operation contracts, not security +promises, and the enforce-or-remove call on them is a design decision rather +than a correction." That decision lived only in a changeset, carried by no +issue. #4834 is the decision, and the answer is **remove**: `experimental` was +weighed and rejected because it is `.describe()` prose that cannot stop an +`import` — the weakest of ADR-0049's three channels — and because a +request/result vocabulary published into the IDE bundle is precisely what an AI +author (ADR-0033) reads as proof the platform hot-loads plugins, then builds a +request that parses clean and is received by nobody (#3950). + +Migration (FROM → TO): + +- `import { DynamicLoadRequestSchema, DynamicUnloadRequestSchema, + DynamicPluginResultSchema, PluginSourceSchema, DynamicPluginOperationSchema } + from '@objectstack/spec/kernel'` (or from `@objectstack/spec`) → + **no replacement export.** Every one is `TS2305: Module … has no exported + member` after upgrade, on every public entry. Same for the type aliases + `DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, + `PluginSource`, `DynamicPluginOperation`, `DynamicLoadRequestInput`, + `DynamicUnloadRequestInput`. +- A **`DynamicLoadRequest` / `DynamicUnloadRequest` value** you built → delete + it, along with whatever was going to send it. There was never a recipient; + the code that constructed one was already a no-op with extra steps. To get a + plugin into a running system, put it in the stack (`defineStack`) and restart. +- A **`DynamicPluginResult`** you typed a handler against → delete the handler. + Nothing ever produced one. +- **`activationEvents` inside a `DynamicLoadRequest`** — the key #4657 + tombstoned one release-candidate earlier — now has a *stronger* answer than + that tombstone gave. #4657 told you: delete this key from your + `DynamicLoadRequest`. **The correct instruction is now: delete the entire + `DynamicLoadRequest`.** The shape that carried the key is gone, so its + `retiredKey()` prescription is gone with it — legitimately, because "this + request shape does not exist" is strictly stronger than "this one key of it + does not exist". If you are upgrading from v16 and wrote `activationEvents` + in *either* form (v16 strings `['onMetadataType:flow']`, or the v17-rc + structured `[{ type, pattern }]` from #4653), you do not need to migrate the + key at all — the value it sat in has no shape and no recipient. + **The studio half of #4657 is unaffected**: `StudioPluginManifest` + (`defineStudioPlugin`) is a live authoring surface and still rejects + `activationEvents` with its own prescription. Delete the key there. +- Runtime plugin loading is a **new capability**, not a restoration: if it is + ever built it returns via the enforce route of ADR-0049 through a new ADR — + loader first, vocabulary second. The shapes it needs are unlikely to be these + ones, which is itself a reason not to keep them as a design constraint on + work that has not started. + +Self-check (#4535 §5): + +1. **TS2305 / TS2339 — what exactly breaks?** TS2305 on twelve names, at two + entry points (`@objectstack/spec` root and `@objectstack/spec/kernel`) — the + five `*Schema` consts and the seven type aliases listed above. No TS2339: + nothing removed was a *property* of a surviving shape, because the removed + defs were embedded in no parent schema. `PluginSource` was reachable only as + `DynamicLoadRequest.source` and `DynamicPluginOperation` only as + `DynamicPluginResult.operation`, both of which go in the same change. +2. **Metadata migration — is there any?** No, and none is possible. All five + are root request/result payload shapes: no metadata-type root reaches them + (`gen:schema`'s reachability BFS says so — see the gate output below), no + `sys_metadata` row can carry one, and no `.stack.ts` / `objectstack.config.ts` + authoring surface embeds one. There is therefore no source for an ADR-0087 + **D2** conversion to rewrite, and `os migrate meta` would have nothing to + match. Registered instead as an ADR-0087 **D3** semantic migration, + `plugin-runtime-family-retired`, which is where a removal with no rewritable + source belongs — the same disposition as #4616, #4767 and #4783. The + pre-existing D3 entry `plugin-activation-events-retired` (#4657) is + **corrected, not deleted**: its studio half is still live and its historical + record must keep replaying, so its kernel half now records this supersession + rather than continuing to promise a tombstone that no longer exists. +3. **Shape change — what kind?** Pure removal of five whole defs; zero + additions, zero narrowings, zero renames. Runtime behaviour is unchanged in + the strongest sense available: not "equivalent", but *identical*, because no + code path anywhere consumed any of it. + +The retirement kit: whole-def removal (#4650 route 3 — the defs stop being +emitted, adjudicated by the `json-schema.manifest.json` ratchet (#2978) and +`check:api-surface`, not by the per-key tombstone ratchet, which reported all 23 +`authorable-surface.json` deletions as carrying their own proof); no +`retiredKey()` tombstones, deliberately — nothing parses these schemas any more, +and a prescription nobody can receive is noise (the precedent this same module +set in #3896); ADR-0087 D3 semantic migration + the corrected #4657 entry; +baselines (`json-schema.manifest.json` −5 defs, `authorable-surface.json` −23 +lines, `api-surface.json` −12 names, `spec-changes.json`, +`docs/protocol-upgrade-guide.md`, `content/docs/references/kernel/`) regenerated +from the rebuilt source rather than hand-edited; `PLUGIN_STANDARDS.md` §5.3 and +its capability table now say **Not built** instead of ✅; compiler-API export pin +(`plugin-runtime-retirement.test.ts` — zero holders for all twelve names across +every entry in the `package.json` exports map, with three anti-vacuity guards), +sabotage-verified. diff --git a/content/docs/references/kernel/index.mdx b/content/docs/references/kernel/index.mdx index 5eb897a29f..314286cdd8 100644 --- a/content/docs/references/kernel/index.mdx +++ b/content/docs/references/kernel/index.mdx @@ -30,7 +30,6 @@ This section contains all protocol schemas for the kernel layer of ObjectStack. - diff --git a/content/docs/references/kernel/meta.json b/content/docs/references/kernel/meta.json index 5bd1cc6acc..5431806b67 100644 --- a/content/docs/references/kernel/meta.json +++ b/content/docs/references/kernel/meta.json @@ -6,7 +6,6 @@ "plugin-lifecycle-advanced", "plugin-loading", "plugin-registry", - "plugin-runtime", "plugin-structure", "plugin-validator", "---Plugin Security & Dependencies---", diff --git a/content/docs/references/kernel/plugin-runtime.mdx b/content/docs/references/kernel/plugin-runtime.mdx deleted file mode 100644 index 84a258cda3..0000000000 --- a/content/docs/references/kernel/plugin-runtime.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Plugin Runtime -description: Plugin Runtime protocol schemas ---- - -{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} - -# Plugin Runtime Management Protocol - -Defines the protocol for dynamic plugin loading, unloading, and discovery - -at runtime. Addresses the "Dynamic Loading" gap in the microkernel architecture - -by enabling plugins to be loaded and unloaded without restarting the kernel. - -Inspired by: - -- OSGi Dynamic Module System (bundle lifecycle) - -- Kubernetes Operator pattern (reconciliation loop) - -This protocol enables: - -- Runtime load/unload of plugins without kernel restart - -- Plugin discovery from registries and local filesystem - -- Safe unload with dependency awareness - - -**Source:** `packages/spec/src/kernel/plugin-runtime.zod.ts` - - -## TypeScript Usage - -```typescript -import { DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel'; -import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel'; - -// Validate data -const result = DynamicLoadRequestSchema.parse(data); -``` - ---- - -## DynamicLoadRequest - -Request to dynamically load a plugin at runtime - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **pluginId** | `string` | ✅ | Unique plugin identifier | -| **source** | `{ type: Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>; location: string; version?: string; integrity?: string }` | ✅ | Plugin source location for dynamic resolution | -| **activationEvents** | `any` | optional | [REMOVED] `dynamicLoadRequest.activationEvents` was removed in @objectstack/spec 17.0.0 (#4657, ADR-0049) — no runtime ever read it: every plugin activates immediately on load, so the declared lazy-activation window never existed. Delete the key; eager activation is the only behaviour there has ever been. Lazy activation, if built, returns via the enforce route of ADR-0049 with a vocabulary its executor actually honours. | -| **config** | `Record` | optional | Runtime configuration overrides | -| **priority** | `integer` | ✅ | Loading priority (lower is higher) | -| **sandbox** | `boolean` | ✅ | Run in an isolated sandbox | -| **timeout** | `integer` | ✅ | Maximum time to complete loading in ms | - - ---- - -## DynamicPluginOperation - -Runtime plugin operation type - -### Allowed Values - -* `load` -* `unload` -* `reload` -* `enable` -* `disable` - - ---- - -## DynamicPluginResult - -Result of a dynamic plugin operation - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **success** | `boolean` | ✅ | | -| **operation** | `Enum<'load' \| 'unload' \| 'reload' \| 'enable' \| 'disable'>` | ✅ | Runtime plugin operation type | -| **pluginId** | `string` | ✅ | | -| **durationMs** | `integer` | optional | | -| **version** | `string` | optional | | -| **error** | `{ code: string; message: string; details?: Record }` | optional | | -| **warnings** | `string[]` | optional | | - - ---- - -## DynamicUnloadRequest - -Request to dynamically unload a plugin at runtime - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **pluginId** | `string` | ✅ | Plugin to unload | -| **strategy** | `Enum<'graceful' \| 'forceful' \| 'drain'>` | ✅ | How to handle in-flight work during unload | -| **timeout** | `integer` | ✅ | Maximum time to complete unloading in ms | -| **cleanupCache** | `boolean` | ✅ | Remove cached code and assets after unload | -| **dependentAction** | `Enum<'cascade' \| 'warn' \| 'block'>` | ✅ | How to handle plugins that depend on this one | - - ---- - -## PluginSource - -Plugin source location for dynamic resolution - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>` | ✅ | Plugin source type | -| **location** | `string` | ✅ | Package name, file path, URL, or git repository | -| **version** | `string` | optional | Semver version range (e.g., "^1.0.0") | -| **integrity** | `string` | optional | Subresource Integrity hash (e.g., "sha384-...") | - - ---- - diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index a78eec4297..9134f4b9f4 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -182,6 +182,8 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block ( The same pass closes `activationEvents` (#4657): both keys that carried it — `DynamicLoadRequest.activationEvents` on the kernel side and `StudioPluginManifest.activationEvents` on the studio side — declared lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in any repo ever implemented: every plugin has always activated immediately on load/registration, and cloud-v1's own ROADMAP recorded the capability as unimplemented, planned for v0.4.0. #4653 had just converged the two `ActivationEventSchema` declarations onto one structured `{ type, pattern }` vocabulary in this same unreleased major; with the maintainer's enforce-or-remove ruling landing on REMOVE, that converged vocabulary retires before ever shipping — composed across the two changes, a v16 author simply deletes the key in whichever form they carried. Neither parent is stored metadata (`StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin`; `DynamicLoadRequest` is a runtime request shape with no caller in any repo), so there is no source for the chain to rewrite — one semantic TODO, the `validateOnly` shape. The kernel key is tombstoned (its schema is not `.strict()`; a plain delete would strip it silently), the studio key is rejected by the strict manifest parse with its own guidance prescription, and the orphaned `ActivationEventSchema` def is removed with them. Behaviour is byte-identical: eager activation was always the only behaviour. +That kernel-side tombstone was then SUPERSEDED inside the same unreleased major by #4834, which finished the enforce-or-remove pass one level up: the entire `plugin-runtime.zod` family — `DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation` — is removed, because the "Dynamic Loading" capability it described (runtime load / unload / reload without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy) has no server anywhere: no runtime in objectstack, cloud or objectui ever received one of these requests or produced one of these results. #3896 had suspended the call on these five deliberately — "operation contracts, not security promises" — in a changeset paragraph no issue carried; #4834 is that decision, answered REMOVE. So a v16 author who wrote `activationEvents` inside a `DynamicLoadRequest` value does not delete a key: the whole value has no shape and no recipient, and importing `DynamicLoadRequestSchema` at all is TS2305 in v17. The studio half of the `activationEvents` retirement is untouched and still rejects the key with its own prescription — `defineStudioPlugin` remains a live authoring surface. Behaviour is again byte-identical: nothing ever executed a dynamic plugin operation. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -277,8 +279,11 @@ The same pass closes `activationEvents` (#4657): both keys that carried it — ` - Why not automatic: The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a capability the contract no longer declares, and the follow-up audit (#4634) checked every bit in the record the same way, across objectstack and cloud (objectui confirmed clean): of 34 declared bits, THREE have a decision-making reader — `queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), `autonumber` (engine defers generation to the driver), `batchSchemaSync` (engine ANDs it with method presence, because a subclass can inherit `syncSchemasBatch` from a base whose transport batches while its own cannot) — and THIRTY-ONE were written by every driver and read by nothing. Their `.describe()` strings promised engine adaptation ("if false, ObjectQL will filter/sort/paginate in memory") that was never built, and zero readers let the values go WRONG unnoticed: SqlDriver declared `streaming: false` while implementing `findStream`; InMemoryDriver declared `streaming: true` over a full-table read (ADR-0078 false affordance, on the capability record itself). The real mechanism everywhere else is METHOD presence: transactions gate on `driver.beginTransaction`, aggregate pushdown on `typeof driver.aggregate`, schema sync on `typeof driver.syncSchema`, and the REQUIRED CRUD/bulk methods are called unconditionally. A driver is CODE, never stack metadata — `supports` literals live in driver classes and `DriverConfig.capabilities` is plugin TS configuration, neither ever a `sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was retired separately in #4583) — so there is no source for the D2 chain to rewrite and this entry is the D3 record. The keys are tombstoned rather than deleted because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed (DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): a plain delete would silently strip a vendor's authored bit, replacing one silent no-op with another. `batchSchemaSync` also drops its `.default(false)` for `.optional()` — absence already meant false at both readers, and the default forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634. - Done when: No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 retired bits — a driver class that still writes one fails tsc against `IDataDriver.supports` (the bit is `never`), and a parsed config fails with the per-key prescription. The three in-repo drivers (memory / mongodb / sql) declare only live bits; cloud's TursoDriver keeps compiling via its `...super.supports` spread (its stale explicit overrides are cleanup, tracked cloud-side). Engine behaviour is byte-identical: every removed bit had zero readers, and the three live bits keep their readers (engine.ts autonumber defer / aggregate dispatch, plugin.ts + engine.ts batched schema sync, verify date-bucket parity). - **`plugin-activation-events-retired`** — `kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents` → (removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours) - - Why not automatic: Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. - - Done when: No `DynamicLoadRequest` or `defineStudioPlugin` input authors `activationEvents` — authoring it is a tsc error (`never` on the kernel side; an unknown key on the strict studio manifest) and a parse error carrying the prescription on both. No code imports `ActivationEventSchema` / `ActivationEvent` from `@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). Runtime behaviour is byte-identical: plugins loaded eagerly before and after. + - Why not automatic: Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation ("plugins remain dormant until an activation event fires") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer "delete this key" but "this request shape does not exist" (see `plugin-runtime-family-retired` below). The studio half of this entry is unaffected and still enforced by the strict manifest parse. + - Done when: No `defineStudioPlugin` input authors `activationEvents` — authoring it is an unknown key on the strict studio manifest and a parse error carrying the prescription. On the kernel side the stronger #4834 criterion applies instead: there is no `DynamicLoadRequest` type or schema left to author it into at all. No code imports `ActivationEventSchema` / `ActivationEvent` from `@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). Runtime behaviour is byte-identical: plugins loaded eagerly before and after. +- **`plugin-runtime-family-retired`** — `kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation` → (removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second) + - Why not automatic: The five schemas declared the "Dynamic Loading" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — "operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834. + - Done when: No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` or any of their type aliases (`DynamicLoadRequest`, `DynamicUnloadRequest`, `DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`, `DynamicLoadRequestInput`, `DynamicUnloadRequestInput`) from `@objectstack/spec` or `@objectstack/spec/kernel` — every one is TS2305 after upgrade, on every public entry (pinned by symbol identity in `plugin-runtime-retirement.test.ts`). Nothing regresses at runtime, because nothing called anything: a caller that believed it was hot-loading a plugin was already only building an object. Boot-time composition through `defineStack` is unchanged. --- diff --git a/packages/spec/PLUGIN_STANDARDS.md b/packages/spec/PLUGIN_STANDARDS.md index 5d65f81993..07777b9fd6 100644 --- a/packages/spec/PLUGIN_STANDARDS.md +++ b/packages/spec/PLUGIN_STANDARDS.md @@ -156,15 +156,25 @@ Isolation levels: `none`, `process`, `vm`, `iframe`, `web-worker`. - Transports: `message-port`, `unix-socket`, `tcp`, `memory` - Configurable message size limit, timeout, and service ACL (`allowedServices`) -### 5.3 Dynamic Loading (`plugin-runtime.zod.ts`) - -Plugins can be loaded and unloaded at runtime **without restarting the kernel**: - -- **`DynamicLoadRequestSchema`** — Load a plugin from `npm`, `local`, `url`, `registry`, or `git` sources with optional integrity verification -- **`DynamicUnloadRequestSchema`** — Graceful/forceful/drain unload with dependency awareness (`cascade`, `warn`, or `block` dependents) -- ~~`ActivationEventSchema`~~ — REMOVED in v17 (#4657, ADR-0049): the lazy-activation trigger vocabulary had no runtime reader in any repo — every plugin activates immediately on load — so it was retired with the `activationEvents` keys that embedded it. Lazy activation, if built, returns via the enforce route of ADR-0049 -- **`PluginDiscoveryConfigSchema`** — Runtime discovery from registries and local directories with polling and trust filtering -- **`DynamicLoadingConfigSchema`** — Subsystem configuration: max dynamic plugins, default sandbox policy, allowed sources, integrity requirements +### 5.3 Dynamic Loading — ~~`plugin-runtime.zod.ts`~~ REMOVED in v17 + +The whole module is gone (#4834, ADR-0049 enforce-or-remove). It declared runtime +load / unload / reload of plugins **without restarting the kernel** — sources +(`npm` / `local` / `url` / `registry` / `git`), integrity hashes, sandboxing, +graceful/forceful/drain unload, dependent-cascade policy — and **no runtime in +any repo ever implemented one of those operations**: a bare-name scan of +objectstack, cloud and objectui found zero references outside the declaration +itself, its own unit tests and the generated artifacts. Removed with the module: +`DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, +`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` +and every type alias. The `activationEvents` tombstone #4657 left on +`DynamicLoadRequest` goes with the shape that carried it, as did the +discovery/sandbox config island #3896 had already retired. + +**What is real:** plugins are composed at boot. `defineStack` registers them and +the kernel runs `init` → `start`; the set is fixed until the process restarts. +Runtime loading, if it is ever built, returns via the enforce route of ADR-0049 +through a new ADR — loader first, vocabulary second. ### 5.4 Plugin System Assessment Summary @@ -178,4 +188,4 @@ Plugins can be loaded and unloaded at runtime **without restarting the kernel**: | Health Checks | ✅ | `plugin-lifecycle-advanced.zod.ts` — Per-plugin health + system aggregation | | Hot Reload | ✅ | `plugin-loading.zod.ts` — Dev + production-safe with rollback and draining | | Plugin Isolation | ✅ | `plugin-loading.zod.ts` — Configurable scope + IPC for process boundaries | -| Dynamic Loading | ✅ | `plugin-runtime.zod.ts` — Runtime load/unload (eager activation; the unenforced activation-events vocabulary was removed in #4657) | +| Dynamic Loading | ❌ | **Not built.** The `plugin-runtime.zod.ts` vocabulary that declared it was removed in v17 (#4834, ADR-0049) — it had no runtime reader in any repo. Plugins are composed at boot; the set is fixed until restart | diff --git a/packages/spec/PROTOCOL_MAP.md b/packages/spec/PROTOCOL_MAP.md index 795fabcefc..5c40193877 100644 --- a/packages/spec/PROTOCOL_MAP.md +++ b/packages/spec/PROTOCOL_MAP.md @@ -187,7 +187,6 @@ This document serves as the **Grand Map** of the ObjectStack specification. It l | [`service-registry.zod.ts`](src/kernel/service-registry.zod.ts) | | **Service Registry**. Internal dependency injection. | | [`metadata-loader.zod.ts`](src/kernel/metadata-loader.zod.ts) | | **Loader**. Logic for loading definitions from disk/DB. | | [`plugin-loading.zod.ts`](src/kernel/plugin-loading.zod.ts) | ⭐ | **Plugin Loading**. Loading strategies, production-safe hot reload (`environment`, `productionSafety`), full plugin sandboxing (`scope`, `ipc`), code splitting, caching, and performance monitoring. | -| [`plugin-runtime.zod.ts`](src/kernel/plugin-runtime.zod.ts) | ⭐ | **Dynamic Loading**. Runtime load/unload of plugins (`DynamicLoadRequest`, `DynamicUnloadRequest`), activation events, plugin discovery from registries/directories, and source resolution (npm/local/url/registry/git). | | [`plugin-registry.zod.ts`](src/kernel/plugin-registry.zod.ts) | | **Plugin Registry**. Metadata for available plugins. | | [`plugin-security.zod.ts`](src/kernel/plugin-security.zod.ts) | | **Plugin Security**. Security verification for plugins. | | [`plugin-versioning.zod.ts`](src/kernel/plugin-versioning.zod.ts) | | **Versioning**. Semantic versioning rules for plugins. | diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index fdc04193d7..af816023f9 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1488,16 +1488,6 @@ "DisablePackageResponseSchema (const)", "DistributedStateConfig (type)", "DistributedStateConfigSchema (const)", - "DynamicLoadRequest (type)", - "DynamicLoadRequestInput (type)", - "DynamicLoadRequestSchema (const)", - "DynamicPluginOperation (type)", - "DynamicPluginOperationSchema (const)", - "DynamicPluginResult (type)", - "DynamicPluginResultSchema (const)", - "DynamicUnloadRequest (type)", - "DynamicUnloadRequestInput (type)", - "DynamicUnloadRequestSchema (const)", "EVENT_PRIORITY_VALUES (const)", "EnablePackageRequest (type)", "EnablePackageRequestSchema (const)", @@ -1748,8 +1738,6 @@ "PluginSecurityManifest (type)", "PluginSecurityManifestSchema (const)", "PluginSecurityProtocol (const)", - "PluginSource (type)", - "PluginSourceSchema (const)", "PluginStartupResult (type)", "PluginStartupResultSchema (const)", "PluginStateSnapshot (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index ccc3346a9d..e1e005ce6b 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -4281,25 +4281,6 @@ "kernel/DistributedStateConfig:provider", "kernel/DistributedStateConfig:replication", "kernel/DistributedStateConfig:ttl", - "kernel/DynamicLoadRequest:activationEvents [RETIRED]", - "kernel/DynamicLoadRequest:config", - "kernel/DynamicLoadRequest:pluginId", - "kernel/DynamicLoadRequest:priority", - "kernel/DynamicLoadRequest:sandbox", - "kernel/DynamicLoadRequest:source", - "kernel/DynamicLoadRequest:timeout", - "kernel/DynamicPluginResult:durationMs", - "kernel/DynamicPluginResult:error", - "kernel/DynamicPluginResult:operation", - "kernel/DynamicPluginResult:pluginId", - "kernel/DynamicPluginResult:success", - "kernel/DynamicPluginResult:version", - "kernel/DynamicPluginResult:warnings", - "kernel/DynamicUnloadRequest:cleanupCache", - "kernel/DynamicUnloadRequest:dependentAction", - "kernel/DynamicUnloadRequest:pluginId", - "kernel/DynamicUnloadRequest:strategy", - "kernel/DynamicUnloadRequest:timeout", "kernel/EnablePackageRequest:id", "kernel/EnablePackageResponse:message", "kernel/EnablePackageResponse:package", @@ -4909,10 +4890,6 @@ "kernel/PluginSecurityManifest:trustLevel", "kernel/PluginSecurityManifest:vulnerabilities", "kernel/PluginSecurityManifest:vulnerabilityDisclosure", - "kernel/PluginSource:integrity", - "kernel/PluginSource:location", - "kernel/PluginSource:type", - "kernel/PluginSource:version", "kernel/PluginStartupResult:duration", "kernel/PluginStartupResult:error", "kernel/PluginStartupResult:health", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 798a505df7..51571b27f9 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -907,10 +907,6 @@ "kernel/DisablePackageRequest", "kernel/DisablePackageResponse", "kernel/DistributedStateConfig", - "kernel/DynamicLoadRequest", - "kernel/DynamicPluginOperation", - "kernel/DynamicPluginResult", - "kernel/DynamicUnloadRequest", "kernel/EnablePackageRequest", "kernel/EnablePackageResponse", "kernel/Event", @@ -1021,7 +1017,6 @@ "kernel/PluginSandboxing", "kernel/PluginSearchFilters", "kernel/PluginSecurityManifest", - "kernel/PluginSource", "kernel/PluginStartupResult", "kernel/PluginStateSnapshot", "kernel/PluginStatistics", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 493d99d7d2..ee84b4b631 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -480,7 +480,14 @@ "replacement": "(removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours)", "migrationId": "plugin-activation-events-retired", "toMajor": 17, - "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657." + "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired` below). The studio half of this entry is unaffected and still enforced by the strict manifest parse." + }, + { + "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation", + "replacement": "(removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second)", + "migrationId": "plugin-runtime-family-retired", + "toMajor": 17, + "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834." } ], "removed": [] @@ -1020,7 +1027,14 @@ "replacement": "(removed — delete the key. Every plugin activates immediately on load/registration, which is the only behaviour that has ever existed; `activate()` still runs at registration time. Lazy activation, if built, returns via the enforce route of ADR-0049 through a new ADR, with a vocabulary its executor actually honours)", "migrationId": "plugin-activation-events-retired", "toMajor": 17, - "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657." + "rationale": "Both `activationEvents` keys — and the `ActivationEventSchema` trigger vocabulary they embedded (`onCommand` / `onRoute` / … / `onView` after the #4653 convergence) — promised lazy plugin activation (\"plugins remain dormant until an activation event fires\") that no runtime in objectstack, cloud, cloud-v1 or objectui ever implemented: nothing anywhere read the key, every plugin activates immediately, and cloud-v1's own ROADMAP recorded lazy activation as unimplemented (planned v0.4.0). That is the ADR-0049 false-compliance shape in the semantically-lying direction: an author writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` expected deferral and got eager activation with a clean parse. Neither parent shape is stored metadata — `StudioPluginManifest` is TS configuration parsed by `defineStudioPlugin` (a root schema, never part of a stack tree) and `DynamicLoadRequest` is a runtime request shape with no caller — so no `sys_metadata` row can carry the key and there is no source for the D2 chain to rewrite; this entry is the D3 record. The kernel key is tombstoned via `retiredKey()` (its schema is not `.strict()`; a plain delete would strip an authored value silently), the studio key is rejected by the strict manifest parse with a guidance prescription (as are its former VS Code-flavoured aliases `activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / `ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys (#3950: an exported schema with no consumer is read as a capability). #4657. SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole `DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — was removed, which took this key's `retiredKey()` tombstone with it. That is strictly stronger than the tombstone, not weaker: there is no longer a `DynamicLoadRequest` to author the key INTO, so the prescription an author needs is no longer \"delete this key\" but \"this request shape does not exist\" (see `plugin-runtime-family-retired` below). The studio half of this entry is unaffected and still enforced by the strict manifest parse." + }, + { + "surface": "kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult / kernel.pluginSource / kernel.dynamicPluginOperation", + "replacement": "(removed — there is no replacement shape, because there is no operation to describe. Plugins are composed at boot: `defineStack` registers them and the kernel runs register → init → start; the set is fixed until the process restarts. Delete the import and the value. Runtime plugin loading, if it is ever built, returns via the enforce route of ADR-0049 through a new ADR — loader first, vocabulary second)", + "migrationId": "plugin-runtime-family-retired", + "toMajor": 17, + "rationale": "The five schemas declared the \"Dynamic Loading\" capability — runtime load / unload / reload of plugins without a kernel restart, with sandboxing, integrity hashes, drain strategies and dependent-cascade policy — and NOTHING implemented it. A bare-name scan of objectstack, cloud and objectui found zero references outside this package's own declaration, its unit tests and the generated artifacts: no runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the published IDE bundle as proof the platform hot-loads plugins and constructs a request that parses clean and is received by nobody (#3950: an exported schema with no consumer is read as a capability). The #3896 follow-up removed this module's discovery/sandbox config island and left these five in place explicitly — \"operation contracts, not security promises; the enforce-or-remove call on them is a design decision rather than a correction\" — but that suspension lived only in a changeset paragraph with no issue carrying it. #4834 is that decision, answered REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose and cannot stop an import, the weakest of the three ADR-0049 channels. None of the five is stored metadata — they are root request/result payload shapes embedded in no parent schema and parsed against no metadata document — so no `sys_metadata` row can carry one and there is no source for the D2 chain to rewrite; this entry is the D3 record. The removal also subsumes the kernel half of `plugin-activation-events-retired` (#4657): that tombstone goes with the shape that carried it. ADR-0049, #4834." } ], "removed": [] diff --git a/packages/spec/src/kernel/activation-events-retirement.test.ts b/packages/spec/src/kernel/activation-events-retirement.test.ts index 82cc32aba6..a928f6548b 100644 --- a/packages/spec/src/kernel/activation-events-retirement.test.ts +++ b/packages/spec/src/kernel/activation-events-retirement.test.ts @@ -23,9 +23,13 @@ import { describe, it, expect } from 'vitest'; // load-bearing pin is the program below, with anti-vacuity guards — the // #4737 `ActionLocation` retirement's machinery, pointed at absence instead of // ownership. Sabotage-verified in the PR: resurrecting the declaration in -// `plugin-runtime.zod.ts` turns it red, re-exporting ANY schema under the bare -// name from `./studio` turns it red, and pointing the enumeration at nothing -// trips the anti-vacuity guards rather than passing silently. +// `./kernel` turns it red, re-exporting ANY schema under the bare name from +// `./studio` turns it red, and pointing the enumeration at nothing trips the +// anti-vacuity guards rather than passing silently. +// +// (#4834 superseded the kernel half: `DynamicLoadRequest` — the shape that +// carried the tombstoned key — was removed whole, so this file's kernel-side +// parse assertion is gone. See the block above the studio test.) describe('[#4657] ActivationEventSchema removal — no entry exports the name', () => { it('resolves the export surface: the retired names have ZERO holders across every public entry', async () => { const ts = (await import('typescript')).default; @@ -87,8 +91,12 @@ describe('[#4657] ActivationEventSchema removal — no entry exports the name', const studioNames = exportsOf('./studio').map((e) => e.getName()); expect(kernelNames.length, './kernel must export a non-trivial surface').toBeGreaterThan(40); expect(studioNames.length, './studio must export a non-trivial surface').toBeGreaterThan(40); - expect(kernelNames).toContain('DynamicLoadRequestSchema'); - expect(kernelNames).toContain('PluginSourceSchema'); + // (The kernel anchors used to be `DynamicLoadRequestSchema` / + // `PluginSourceSchema` — the parents of the retired key. #4834 removed that + // whole family, so the anchors move to surviving kernel neighbours; the + // assertion they serve is unchanged.) + expect(kernelNames).toContain('PluginSchema'); + expect(kernelNames).toContain('PluginContextSchema'); expect(studioNames).toContain('StudioPluginManifestSchema'); expect(studioNames).toContain('StudioPluginContributionsSchema'); @@ -110,29 +118,28 @@ describe('[#4657] ActivationEventSchema removal — no entry exports the name', expect('ActivationEventSchema' in ns, `${label} must not export ActivationEventSchema`).toBe(false); } // Anti-vacuity: the namespaces just probed are real and non-trivial. - expect('DynamicLoadRequestSchema' in kernel).toBe(true); + expect('PluginSchema' in kernel).toBe(true); expect('StudioPluginManifestSchema' in studio).toBe(true); }); - it('the live paths still parse — and reject the retired key with its prescription', async () => { - const { DynamicLoadRequestSchema } = await import('./plugin-runtime.zod'); + // ─── The kernel half of #4657 is SUPERSEDED by #4834 ─────────────────────── + // + // `DynamicLoadRequest.activationEvents` was tombstoned here with a + // `retiredKey()` prescription. #4834 removed the ENTIRE `DynamicLoadRequest` + // family (ADR-0049 enforce-or-remove on the whole "Dynamic Loading" + // capability, four-repo zero-consumer), which took the tombstone with it — + // legitimately: "the whole request shape is gone" is strictly stronger than + // "this one key is gone", and there is no longer a parse for a prescription + // to be delivered at. What replaces it as the kernel-side pin is + // `plugin-runtime-retirement.test.ts` (zero holders for all five names), + // plus the zero-holder assertion above, which covers `ActivationEventSchema` + // itself on both entries. Only the studio half still has a live parse: + it('the studio manifest still parses — and rejects the retired key with its prescription', async () => { const { StudioPluginManifestSchema } = await import('../studio/plugin.zod'); - // Kernel side: tombstoned (non-strict schema — a plain delete would have - // Zod silently strip an authored value, replacing one silent no-op with - // another). - const load = { pluginId: 'com.acme.analytics', source: { type: 'npm', location: '@acme/x' } }; - expect(DynamicLoadRequestSchema.parse(load)).not.toHaveProperty('activationEvents'); - expect(() => - DynamicLoadRequestSchema.parse({ - ...load, - activationEvents: [{ type: 'onStartup', pattern: '*' }], - }), - ).toThrow(/activationEvents.*removed.*#4657/s); - - // Studio side: strict parse + guidance — the SAME document differing only - // in this one key stays legal without it (so the negative cannot pass for - // an unrelated reason). + // Strict parse + guidance — the SAME document differing only in this one + // key stays legal without it (so the negative cannot pass for an unrelated + // reason). const manifest = { id: 'objectstack.my-plugin', name: 'My Plugin' }; expect(StudioPluginManifestSchema.parse(manifest)).not.toHaveProperty('activationEvents'); expect(() => diff --git a/packages/spec/src/kernel/index.ts b/packages/spec/src/kernel/index.ts index c58c77a90d..a3dd53187e 100644 --- a/packages/spec/src/kernel/index.ts +++ b/packages/spec/src/kernel/index.ts @@ -41,7 +41,39 @@ export * from './package-upgrade.zod'; export * from './plugin-capability.zod'; export * from './plugin-lifecycle-advanced.zod'; export * from './plugin-loading.zod'; -export * from './plugin-runtime.zod'; +// plugin-runtime.zod (DynamicLoadRequestSchema / DynamicUnloadRequestSchema / +// DynamicPluginResultSchema / PluginSourceSchema / DynamicPluginOperationSchema +// + every type alias) was REMOVED per ADR-0049 enforce-or-remove (#4834). The +// module declared the "Dynamic Loading" capability — runtime load / unload / +// reload of plugins without a kernel restart, with sandboxing, integrity hashes, +// drain strategies and dependent-cascade policy. No runtime in any repo +// (objectstack / cloud / objectui) ever received a DynamicLoadRequest, performed +// a load/unload, or produced a DynamicPluginResult: the operations the vocabulary +// names do not exist. Plugins are composed at boot (`defineStack` → the kernel's +// register/init/start sequence) and the set is fixed until the process restarts. +// +// The #3896 follow-up removed this module's discovery/sandbox config island and +// recorded the remaining five as a deliberate SUSPENSION ("operation contracts, +// not security promises; the enforce-or-remove call is a design decision") — a +// decision that then lived only in a changeset paragraph, with no issue carrying +// it. #4834 IS that decision, resolved REMOVE: a published request/result +// vocabulary with no server behind it is the #3950 shape at its most inviting to +// an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` as proof the +// platform hot-loads plugins and builds a request that parses clean and is +// received by nobody. +// +// This SUPERSEDES the narrower #4657 retirement of +// `DynamicLoadRequest.activationEvents`: that key's `retiredKey()` tombstone is +// removed together with the shape that carried it, because "the whole request +// shape is gone" is strictly stronger than "this one key is gone" — an author +// who wrote `activationEvents` now deletes the entire `DynamicLoadRequest` +// value, not one key of it. The studio half of #4657 +// (`StudioPluginManifest.activationEvents`) is untouched and still rejects the +// key with its own prescription. +// +// Runtime plugin loading, if it is ever built, returns via the enforce route of +// ADR-0049 through a new ADR — write the loader first, then declare exactly the +// operations it performs. The vocabulary it needs is unlikely to be this one. export * from './plugin-security-advanced.zod'; export * from './plugin-structure.zod'; export * from './plugin-validator.zod'; diff --git a/packages/spec/src/kernel/plugin-runtime-retirement.test.ts b/packages/spec/src/kernel/plugin-runtime-retirement.test.ts new file mode 100644 index 0000000000..b1cac906dd --- /dev/null +++ b/packages/spec/src/kernel/plugin-runtime-retirement.test.ts @@ -0,0 +1,140 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; + +// ─── [#4834] the `plugin-runtime.zod` family is REMOVED ───────────────────── +// +// ADR-0049 enforce-or-remove, ruled REMOVE. `DynamicLoadRequestSchema`, +// `DynamicUnloadRequestSchema`, `DynamicPluginResultSchema`, +// `PluginSourceSchema` and `DynamicPluginOperationSchema` (+ every type alias) +// declared the "Dynamic Loading" capability — runtime load / unload / reload of +// plugins without a kernel restart. No runtime in objectstack / cloud / +// objectui ever received one of these requests or produced one of these +// results: the operations the vocabulary names do not exist. #3896 recorded the +// suspension as a deliberate design decision that lived only in a changeset +// paragraph; #4834 is that decision, answered. +// +// Why THIS pin, and not a type-level one: #4642 established that a +// compile-time conditional-type assertion in this package is a no-op (the +// package tsconfig excludes `**/*.test.ts`, and vitest never enables +// `typecheck`), so a `Assert< Equal< … > >` here would be decoration. The +// load-bearing pin is the TypeScript compiler-API program below, which resolves +// the REAL export surface of EVERY public entry from `package.json`'s exports +// map and asserts each retired name has zero holders — by symbol identity, not +// by grepping text. +// +// Every `not`-shaped assertion has an anti-vacuity guard, because the failure +// mode of an absence pin is passing for the wrong reason (a path typo, an entry +// that stops resolving, an empty enumeration). Sabotage-verified in the PR: +// 1. re-declare `PluginSourceSchema` in `./kernel` → red; +// 2. re-export any of the five names from a DIFFERENT entry (`./studio`) → +// red (a re-export can lie about the domain even when the symbol is +// honest — the C14/C15 lesson); +// 3. point the entry enumeration at nothing → the anti-vacuity guards trip +// instead of the suite passing silently. +describe('[#4834] plugin-runtime family removal — no entry exports any of the five names', () => { + /** The five schema names + every type alias they published. */ + const RETIRED = [ + 'DynamicLoadRequestSchema', + 'DynamicUnloadRequestSchema', + 'DynamicPluginResultSchema', + 'PluginSourceSchema', + 'DynamicPluginOperationSchema', + 'DynamicLoadRequest', + 'DynamicUnloadRequest', + 'DynamicPluginResult', + 'PluginSource', + 'DynamicPluginOperation', + 'DynamicLoadRequestInput', + 'DynamicUnloadRequestInput', + ] as const; + + it('resolves the export surface: the retired names have ZERO holders across every public entry', async () => { + const ts = (await import('typescript')).default; + const { resolve, dirname } = await import('node:path'); + const { fileURLToPath } = await import('node:url'); + const { readFileSync } = await import('node:fs'); + + const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..'); + // Every public entry point, read from package.json's exports map so a + // future entry cannot silently escape the absence assertions below. + const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as { + exports: Record; + }; + const entries: Record = {}; + for (const sub of Object.keys(pkg.exports)) { + if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts'); + else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`); + // './openapi.json' / './package.json' are not TypeScript entry points. + } + // Anti-vacuity (1): the enumeration found the real surface — including the + // entry that used to own every retired name, and the root barrel that + // re-exported it. + for (const needed of ['.', './kernel']) { + expect(Object.keys(entries), `exports map must include ${needed}`).toContain(needed); + } + expect(Object.keys(entries).length).toBeGreaterThan(10); + + const program = ts.createProgram(Object.values(entries), { + module: ts.ModuleKind.ESNext, + moduleResolution: ts.ModuleResolutionKind.Bundler, + skipLibCheck: true, + noEmit: true, + }); + const checker = program.getTypeChecker(); + + const exportsOf = (sub: string) => { + const sf = program.getSourceFile(entries[sub]); + const moduleSym = sf && checker.getSymbolAtLocation(sf); + // Anti-vacuity (2): without this guard a resolution failure would make + // every absence assertion below pass for free — exactly how a gate goes + // dormant (#4642). + expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy(); + return checker.getExportsOfModule(moduleSym!); + }; + + /** Every entry that exports `name` — for a removal this must be []. */ + const holdersOf = (name: string): string[] => { + const out: string[] = []; + for (const sub of Object.keys(entries)) { + if (exportsOf(sub).some((e) => e.getName() === name)) out.push(sub); + } + return out; + }; + + // Anti-vacuity (3): `holdersOf` actually finds holders when a name IS + // exported — proven on a surviving kernel neighbour, so `[]` below means + // "absent", not "the probe is broken". + expect(holdersOf('PluginSchema')).toContain('./kernel'); + const kernelNames = exportsOf('./kernel').map((e) => e.getName()); + expect(kernelNames.length, './kernel must still export a non-trivial surface').toBeGreaterThan(40); + + // The removal itself: NO public entry exports any of the twelve names — + // not the old owner, and not some other entry that might "helpfully" adopt + // them. Exact equality with `[]`, so a partial move cannot slip through. + for (const name of RETIRED) { + expect(holdersOf(name), `${name} must have zero holders`).toEqual([]); + } + }); + + it('keeps the runtime namespaces consistent with the compiler view', async () => { + const kernel = await import('./index'); + const root = await import('../index'); + + for (const [label, ns] of [['./kernel', kernel], ['.', root]] as const) { + for (const name of RETIRED) { + expect(name in ns, `${label} must not export ${name}`).toBe(false); + } + } + // Anti-vacuity: the namespaces just probed are real and non-trivial. + expect('PluginSchema' in kernel).toBe(true); + expect(Object.keys(kernel).length).toBeGreaterThan(40); + }); + + it('the module itself is gone — nothing can import it by path either', async () => { + // The names could be absent from the barrels while the module still sat on + // disk, importable by deep path and still emitting json-schema defs. It + // does not. + await expect(import('./plugin-runtime.zod')).rejects.toThrow(); + }); +}); diff --git a/packages/spec/src/kernel/plugin-runtime.test.ts b/packages/spec/src/kernel/plugin-runtime.test.ts deleted file mode 100644 index 4d80edd74e..0000000000 --- a/packages/spec/src/kernel/plugin-runtime.test.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - DynamicPluginOperationSchema, - PluginSourceSchema, - DynamicLoadRequestSchema, - DynamicUnloadRequestSchema, - DynamicPluginResultSchema, -} from './plugin-runtime.zod'; - -describe('Plugin Runtime Management Protocol', () => { - describe('DynamicPluginOperationSchema', () => { - it('should accept valid operations', () => { - expect(DynamicPluginOperationSchema.parse('load')).toBe('load'); - expect(DynamicPluginOperationSchema.parse('unload')).toBe('unload'); - expect(DynamicPluginOperationSchema.parse('reload')).toBe('reload'); - expect(DynamicPluginOperationSchema.parse('enable')).toBe('enable'); - expect(DynamicPluginOperationSchema.parse('disable')).toBe('disable'); - }); - - it('should reject invalid operations', () => { - expect(() => DynamicPluginOperationSchema.parse('invalid')).toThrow(); - }); - }); - - describe('PluginSourceSchema', () => { - it('should accept npm source', () => { - const source = { - type: 'npm', - location: '@objectstack/plugin-analytics', - version: '^2.0.0', - }; - const result = PluginSourceSchema.parse(source); - expect(result.type).toBe('npm'); - expect(result.version).toBe('^2.0.0'); - }); - - it('should accept local source', () => { - const source = { - type: 'local', - location: '/opt/plugins/custom-plugin', - }; - const result = PluginSourceSchema.parse(source); - expect(result.type).toBe('local'); - expect(result.version).toBeUndefined(); - }); - - it('should accept url source with integrity', () => { - const source = { - type: 'url', - location: 'https://plugins.example.com/analytics-1.0.0.tgz', - integrity: 'sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC', - }; - const result = PluginSourceSchema.parse(source); - expect(result.integrity).toBeDefined(); - }); - - it('should accept all source types', () => { - const types = ['npm', 'local', 'url', 'registry', 'git']; - types.forEach((type) => { - const result = PluginSourceSchema.parse({ type, location: 'test' }); - expect(result.type).toBe(type); - }); - }); - }); - - // `ActivationEventSchema` was REMOVED (#4657, ADR-0049) — no runtime ever - // read an activation event, so the vocabulary retired with the keys that - // embedded it. Export-surface pins live in - // activation-events-retirement.test.ts; the tombstone pins are below. - - describe('DynamicLoadRequestSchema', () => { - it('should accept minimal load request', () => { - const request = { - pluginId: 'com.acme.analytics', - source: { - type: 'npm' as const, - location: '@acme/analytics-plugin', - version: '^1.0.0', - }, - }; - const result = DynamicLoadRequestSchema.parse(request); - expect(result.pluginId).toBe('com.acme.analytics'); - expect(result.priority).toBe(100); // default - expect(result.sandbox).toBe(false); // default - expect(result.timeout).toBe(60000); // default - }); - - it('should accept full load request', () => { - const request = { - pluginId: 'com.acme.analytics', - source: { - type: 'registry' as const, - location: 'acme-analytics', - version: '~2.1.0', - }, - config: { apiKey: 'abc123', region: 'us-east' }, - priority: 50, - sandbox: true, - timeout: 120000, - }; - const result = DynamicLoadRequestSchema.parse(request); - expect(result.sandbox).toBe(true); - expect(result.priority).toBe(50); - }); - - // ─── [#4657] `activationEvents` tombstone pins (ADR-0049) ──────────── - // The key promised lazy activation no runtime ever implemented — every - // plugin activates immediately on load. The schema is not `.strict()`, - // so the removal is a `retiredKey()` tombstone: a plain delete would have - // Zod silently STRIP an authored value (#2169 shape) instead of teaching. - it('rejects an authored activationEvents with the retirement prescription', () => { - expect(() => - DynamicLoadRequestSchema.parse({ - pluginId: 'com.acme.analytics', - source: { type: 'npm' as const, location: '@acme/analytics-plugin' }, - activationEvents: [{ type: 'onRoute', pattern: '/api/v1/analytics/*' }], - }), - ).toThrow(/activationEvents.*removed.*17\.0\.0.*#4657.*Delete the key/s); - }); - - it('parses clean without the key — and the result does not carry it', () => { - const result = DynamicLoadRequestSchema.parse({ - pluginId: 'com.acme.analytics', - source: { type: 'npm' as const, location: '@acme/analytics-plugin' }, - }); - // Absence stays absence: the tombstone must not materialize a value. - expect(result).not.toHaveProperty('activationEvents'); - }); - }); - - describe('DynamicUnloadRequestSchema', () => { - it('should accept minimal unload request', () => { - const request = { - pluginId: 'com.acme.analytics', - }; - const result = DynamicUnloadRequestSchema.parse(request); - expect(result.strategy).toBe('graceful'); // default - expect(result.timeout).toBe(30000); // default - expect(result.cleanupCache).toBe(false); // default - expect(result.dependentAction).toBe('block'); // default - }); - - it('should accept full unload request', () => { - const request = { - pluginId: 'com.acme.analytics', - strategy: 'drain' as const, - timeout: 60000, - cleanupCache: true, - dependentAction: 'cascade' as const, - }; - const result = DynamicUnloadRequestSchema.parse(request); - expect(result.strategy).toBe('drain'); - expect(result.cleanupCache).toBe(true); - expect(result.dependentAction).toBe('cascade'); - }); - - it('should accept all unload strategies', () => { - const strategies = ['graceful', 'forceful', 'drain']; - strategies.forEach((strategy) => { - const result = DynamicUnloadRequestSchema.parse({ - pluginId: 'test', - strategy, - }); - expect(result.strategy).toBe(strategy); - }); - }); - }); - - describe('DynamicPluginResultSchema', () => { - it('should accept successful result', () => { - const result = DynamicPluginResultSchema.parse({ - success: true, - operation: 'load', - pluginId: 'com.acme.analytics', - durationMs: 1500, - version: '1.2.3', - }); - expect(result.success).toBe(true); - expect(result.version).toBe('1.2.3'); - }); - - it('should accept failed result with error', () => { - const result = DynamicPluginResultSchema.parse({ - success: false, - operation: 'load', - pluginId: 'com.acme.analytics', - error: { - code: 'DEPENDENCY_MISSING', - message: 'Required dependency "postgres" is not loaded', - details: { missing: ['postgres'] }, - }, - warnings: ['Plugin was partially loaded before failure'], - }); - expect(result.success).toBe(false); - expect(result.error?.code).toBe('DEPENDENCY_MISSING'); - expect(result.warnings).toHaveLength(1); - }); - }); - -}); diff --git a/packages/spec/src/kernel/plugin-runtime.zod.ts b/packages/spec/src/kernel/plugin-runtime.zod.ts deleted file mode 100644 index 678bf6d7c7..0000000000 --- a/packages/spec/src/kernel/plugin-runtime.zod.ts +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. - -import { z } from 'zod'; - -/** - * # Plugin Runtime Management Protocol - * - * Defines the protocol for dynamic plugin loading, unloading, and discovery - * at runtime. Addresses the "Dynamic Loading" gap in the microkernel architecture - * by enabling plugins to be loaded and unloaded without restarting the kernel. - * - * Inspired by: - * - OSGi Dynamic Module System (bundle lifecycle) - * - Kubernetes Operator pattern (reconciliation loop) - * - * This protocol enables: - * - Runtime load/unload of plugins without kernel restart - * - Plugin discovery from registries and local filesystem - * - Safe unload with dependency awareness - */ - -/** - * Dynamic Plugin Operation Type - * Operations that can be performed on plugins at runtime - */ -import { lazySchema } from '../shared/lazy-schema'; -import { retiredKey } from '../shared/retired-key'; -export const DynamicPluginOperationSchema = lazySchema(() => z.enum([ - 'load', // Load and initialize a plugin at runtime - 'unload', // Gracefully unload a running plugin - 'reload', // Unload then load (e.g., version upgrade) - 'enable', // Enable a loaded but disabled plugin - 'disable', // Disable a running plugin without unloading -]).describe('Runtime plugin operation type')); - -/** - * Plugin Source - * Where to resolve a plugin for dynamic loading - */ -export const PluginSourceSchema = lazySchema(() => z.object({ - /** - * Source type - */ - type: z.enum([ - 'npm', // npm registry package - 'local', // Local filesystem path - 'url', // Remote URL (tarball or module) - 'registry', // ObjectStack plugin registry - 'git', // Git repository - ]).describe('Plugin source type'), - - /** - * Source location (package name, path, URL, or git repo) - */ - location: z.string().describe('Package name, file path, URL, or git repository'), - - /** - * Version constraint (semver range) - */ - version: z.string().optional().describe('Semver version range (e.g., "^1.0.0")'), - - /** - * Integrity hash for verification - */ - integrity: z.string().optional().describe('Subresource Integrity hash (e.g., "sha384-...")'), -}).describe('Plugin source location for dynamic resolution')); - -/** - * REMOVED — `ActivationEventSchema` / `ActivationEvent` (#4657, ADR-0049). - * - * The schema declared a lazy-activation trigger vocabulary (`onCommand`, - * `onRoute`, …, `onView` after the #4653 convergence) that NO runtime in any - * repo (objectstack / cloud / cloud-v1 / objectui) ever read: every plugin has - * always activated immediately on load/registration — cloud-v1's own ROADMAP - * recorded lazy activation as ❌ unimplemented. A published trigger vocabulary - * with zero executors is the ADR-0049 false-compliance shape: an author (very - * often an AI, ADR-0033) writes `activationEvents` expecting deferral and gets - * eager activation with a clean parse. - * - * Both keys that embedded it are retired in the same change — - * `DynamicLoadRequestSchema.activationEvents` (tombstoned below) and - * `StudioPluginManifestSchema.activationEvents` (rejected by that schema's - * strict parse with its own prescription) — which left the def an orphaned - * value schema: an export with no consumer is read as a capability by whoever - * finds it (#3950), so it goes with the keys rather than outliving them. Its - * `json-schema.manifest.json` entries (`kernel/ActivationEvent`, - * `studio/ActivationEvent`) and `authorable-surface.json` lines are dropped - * deliberately in the same PR; the removal is pinned by - * `activation-events-retirement.test.ts`. - * - * Lazy activation, if ever built, returns via the enforce route of ADR-0049: - * write the executor first, then declare exactly the vocabulary it honours. - */ - -const ACTIVATION_EVENTS_RETIRED = - '`dynamicLoadRequest.activationEvents` was removed in @objectstack/spec 17.0.0 ' - + '(#4657, ADR-0049) — no runtime ever read it: every plugin activates immediately ' - + 'on load, so the declared lazy-activation window never existed. Delete the key; ' - + 'eager activation is the only behaviour there has ever been. Lazy activation, if ' - + 'built, returns via the enforce route of ADR-0049 with a vocabulary its executor ' - + 'actually honours.'; - -/** - * Dynamic Load Request - * Request to load a plugin at runtime - */ -export const DynamicLoadRequestSchema = lazySchema(() => z.object({ - /** - * Plugin identifier to load - */ - pluginId: z.string().describe('Unique plugin identifier'), - - /** - * Plugin source - */ - source: PluginSourceSchema, - - /** - * RETIRED (#4657, ADR-0049) — tombstoned, not deleted: this schema is not - * `.strict()`, so a plain delete would have Zod silently STRIP the key and - * replace one silent no-op with another (#2169 shape). The tombstone keeps - * the removal audible in both channels: `tsc` (input type `never`) and the - * parse (the prescription itself). - */ - activationEvents: retiredKey(ACTIVATION_EVENTS_RETIRED), - - /** - * Configuration overrides for the plugin - */ - config: z.record(z.string(), z.unknown()).optional() - .describe('Runtime configuration overrides'), - - /** - * Loading priority (lower = higher priority) - */ - priority: z.number().int().min(0).default(100) - .describe('Loading priority (lower is higher)'), - - /** - * Whether to enable sandboxing for this dynamically loaded plugin - */ - sandbox: z.boolean().default(false) - .describe('Run in an isolated sandbox'), - - /** - * Timeout for the load operation in milliseconds - */ - timeout: z.number().int().min(1000).default(60000) - .describe('Maximum time to complete loading in ms'), -}).describe('Request to dynamically load a plugin at runtime')); - -/** - * Dynamic Unload Request - * Request to unload a plugin at runtime - */ -export const DynamicUnloadRequestSchema = lazySchema(() => z.object({ - /** - * Plugin identifier to unload - */ - pluginId: z.string().describe('Plugin to unload'), - - /** - * Unload strategy - */ - strategy: z.enum([ - 'graceful', // Wait for in-flight requests, then unload - 'forceful', // Unload immediately, cancel pending work - 'drain', // Stop accepting new work, finish existing, then unload - ]).default('graceful').describe('How to handle in-flight work during unload'), - - /** - * Timeout for the unload operation in milliseconds - */ - timeout: z.number().int().min(1000).default(30000) - .describe('Maximum time to complete unloading in ms'), - - /** - * Whether to remove cached artifacts - */ - cleanupCache: z.boolean().default(false) - .describe('Remove cached code and assets after unload'), - - /** - * Action for dependents: plugins that depend on this one - */ - dependentAction: z.enum([ - 'cascade', // Also unload dependent plugins - 'warn', // Warn about dependents but proceed - 'block', // Block unload if dependents exist - ]).default('block').describe('How to handle plugins that depend on this one'), -}).describe('Request to dynamically unload a plugin at runtime')); - -/** - * Dynamic Plugin Operation Result - * Result of a dynamic load/unload/reload operation - */ -export const DynamicPluginResultSchema = lazySchema(() => z.object({ - /** - * Whether the operation succeeded - */ - success: z.boolean(), - - /** - * The operation that was performed - */ - operation: DynamicPluginOperationSchema, - - /** - * Plugin identifier - */ - pluginId: z.string(), - - /** - * Operation duration in milliseconds - */ - durationMs: z.number().int().min(0).optional(), - - /** - * Resulting plugin version (for load/reload) - */ - version: z.string().optional(), - - /** - * Error details if operation failed - */ - error: z.object({ - code: z.string().describe('Machine-readable error code'), - message: z.string().describe('Human-readable error message'), - details: z.record(z.string(), z.unknown()).optional(), - }).optional(), - - /** - * Warnings (e.g., dependents affected) - */ - warnings: z.array(z.string()).optional(), -}).describe('Result of a dynamic plugin operation')); - -/** - * REMOVED — the plugin DISCOVERY + DYNAMIC-LOADING config surface (#3896 follow-up). - * - * `DynamicLoadingConfigSchema`, `PluginDiscoveryConfigSchema` and - * `PluginDiscoverySourceSchema` declared a plugin sandboxing / integrity / - * source-allow-list / approval-before-load control set. None of it was ever - * composed into a parent schema, exported from the package root, or read by any - * runtime: the three schemas were an ISLAND, reachable only from their own - * round-trip tests — yet still published into `json-schema/` and the authorable - * key surface, where an author (very often an AI, ADR-0033) would read them as a - * capability this platform has. - * - * That is the ADR-0049 false-compliance shape, and the precedent for a - * SAFETY-shaped one is to REMOVE rather than mark dead: `tool.requiresConfirmation` - * was pruned in #3715 for exactly this reason — "unenforced on every path, so it - * was false compliance, not merely dead". - * - * No `retiredKey()` tombstones. A tombstone earns its keep by making the removal - * audible at a parse the author actually reaches, and NOTHING parses these - * schemas — a prescription nobody can receive is noise, and the silent-strip the - * key-vanish guard protects against was already these keys' permanent condition, - * not something this change introduces. The guard's baseline entries are dropped - * deliberately in this PR instead; see the changeset. - * - * Rebuilding this surface is a design job, not a schema job: write the runtime - * first, then declare only what it enforces. - */ - -// Export types -export type DynamicPluginOperation = z.infer; -export type PluginSource = z.infer; -export type DynamicLoadRequest = z.infer; -export type DynamicUnloadRequest = z.infer; -export type DynamicPluginResult = z.infer; - -// Export input types for schemas with defaults -export type DynamicLoadRequestInput = z.input; -export type DynamicUnloadRequestInput = z.input; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 2966fc711b..9e843831b5 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -737,7 +737,23 @@ const step17: MigrationStep = { + '(its schema is not `.strict()`; a plain delete would strip it silently), the studio ' + 'key is rejected by the strict manifest parse with its own guidance prescription, and ' + 'the orphaned `ActivationEventSchema` def is removed with them. Behaviour is ' - + 'byte-identical: eager activation was always the only behaviour.', + + 'byte-identical: eager activation was always the only behaviour.\n\n' + + 'That kernel-side tombstone was then SUPERSEDED inside the same unreleased major by ' + + '#4834, which finished the enforce-or-remove pass one level up: the entire ' + + '`plugin-runtime.zod` family — `DynamicLoadRequest`, `DynamicUnloadRequest`, ' + + '`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation` — is removed, because ' + + 'the "Dynamic Loading" capability it described (runtime load / unload / reload without ' + + 'a kernel restart, with sandboxing, integrity hashes, drain strategies and ' + + 'dependent-cascade policy) has no server anywhere: no runtime in objectstack, cloud or ' + + 'objectui ever received one of these requests or produced one of these results. #3896 ' + + 'had suspended the call on these five deliberately — "operation contracts, not security ' + + 'promises" — in a changeset paragraph no issue carried; #4834 is that decision, ' + + 'answered REMOVE. So a v16 author who wrote `activationEvents` inside a ' + + '`DynamicLoadRequest` value does not delete a key: the whole value has no shape and no ' + + 'recipient, and importing `DynamicLoadRequestSchema` at all is TS2305 in v17. The ' + + 'studio half of the `activationEvents` retirement is untouched and still rejects the ' + + 'key with its own prescription — `defineStudioPlugin` remains a live authoring surface. ' + + 'Behaviour is again byte-identical: nothing ever executed a dynamic plugin operation.', conversionIds: [ 'action-execute-to-target', 'field-conditionalRequired-to-requiredWhen', @@ -1249,15 +1265,72 @@ const step17: MigrationStep = { + 'with a guidance prescription (as are its former VS Code-flavoured aliases ' + '`activation` / `events` / `onActivate`), and the orphaned `ActivationEventSchema` / ' + '`ActivationEvent` exports are removed from `./kernel` and `./studio` with the keys ' - + '(#3950: an exported schema with no consumer is read as a capability). #4657.', + + '(#3950: an exported schema with no consumer is read as a capability). #4657. ' + + 'SUPERSEDED ON THE KERNEL SIDE by #4834 (same unreleased major): the whole ' + + '`DynamicLoadRequest` shape — and the rest of the plugin-runtime family with it — ' + + 'was removed, which took this key\'s `retiredKey()` tombstone with it. That is ' + + 'strictly stronger than the tombstone, not weaker: there is no longer a ' + + '`DynamicLoadRequest` to author the key INTO, so the prescription an author needs ' + + 'is no longer "delete this key" but "this request shape does not exist" (see ' + + '`plugin-runtime-family-retired` below). The studio half of this entry is ' + + 'unaffected and still enforced by the strict manifest parse.', acceptanceCriteria: - 'No `DynamicLoadRequest` or `defineStudioPlugin` input authors `activationEvents` — ' - + 'authoring it is a tsc error (`never` on the kernel side; an unknown key on the ' - + 'strict studio manifest) and a parse error carrying the prescription on both. No ' + 'No `defineStudioPlugin` input authors `activationEvents` — authoring it is an ' + + 'unknown key on the strict studio manifest and a parse error carrying the ' + + 'prescription. On the kernel side the stronger #4834 criterion applies instead: ' + + 'there is no `DynamicLoadRequest` type or schema left to author it into at all. No ' + 'code imports `ActivationEventSchema` / `ActivationEvent` from ' + '`@objectstack/spec/kernel` or `@objectstack/spec/studio` (TS2305 after upgrade). ' + 'Runtime behaviour is byte-identical: plugins loaded eagerly before and after.', }, + { + id: 'plugin-runtime-family-retired', + surface: + 'kernel.dynamicLoadRequest / kernel.dynamicUnloadRequest / kernel.dynamicPluginResult ' + + '/ kernel.pluginSource / kernel.dynamicPluginOperation', + replacement: + '(removed — there is no replacement shape, because there is no operation to describe. ' + + 'Plugins are composed at boot: `defineStack` registers them and the kernel runs ' + + 'register → init → start; the set is fixed until the process restarts. Delete the ' + + 'import and the value. Runtime plugin loading, if it is ever built, returns via the ' + + 'enforce route of ADR-0049 through a new ADR — loader first, vocabulary second)', + reason: + 'The five schemas declared the "Dynamic Loading" capability — runtime load / unload / ' + + 'reload of plugins without a kernel restart, with sandboxing, integrity hashes, ' + + 'drain strategies and dependent-cascade policy — and NOTHING implemented it. A ' + + 'bare-name scan of objectstack, cloud and objectui found zero references outside ' + + "this package's own declaration, its unit tests and the generated artifacts: no " + + 'runtime ever received a `DynamicLoadRequest`, performed a load/unload, or produced ' + + 'a `DynamicPluginResult`. That is the ADR-0049 false-compliance shape at its most ' + + 'inviting to an AI author (ADR-0033), who reads `DynamicLoadRequestSchema` in the ' + + 'published IDE bundle as proof the platform hot-loads plugins and constructs a ' + + 'request that parses clean and is received by nobody (#3950: an exported schema ' + + 'with no consumer is read as a capability). The #3896 follow-up removed this ' + + "module's discovery/sandbox config island and left these five in place explicitly — " + + '"operation contracts, not security promises; the enforce-or-remove call on them is ' + + 'a design decision rather than a correction" — but that suspension lived only in a ' + + 'changeset paragraph with no issue carrying it. #4834 is that decision, answered ' + + 'REMOVE. `experimental` was considered and rejected: it is only `.describe()` prose ' + + 'and cannot stop an import, the weakest of the three ADR-0049 channels. None of the ' + + 'five is stored metadata — they are root request/result payload shapes embedded in ' + + 'no parent schema and parsed against no metadata document — so no `sys_metadata` ' + + 'row can carry one and there is no source for the D2 chain to rewrite; this entry ' + + 'is the D3 record. The removal also subsumes the kernel half of ' + + '`plugin-activation-events-retired` (#4657): that tombstone goes with the shape ' + + 'that carried it. ADR-0049, #4834.', + acceptanceCriteria: + 'No code imports `DynamicLoadRequestSchema`, `DynamicUnloadRequestSchema`, ' + + '`DynamicPluginResultSchema`, `PluginSourceSchema`, `DynamicPluginOperationSchema` ' + + 'or any of their type aliases (`DynamicLoadRequest`, `DynamicUnloadRequest`, ' + + '`DynamicPluginResult`, `PluginSource`, `DynamicPluginOperation`, ' + + '`DynamicLoadRequestInput`, `DynamicUnloadRequestInput`) from ' + + '`@objectstack/spec` or `@objectstack/spec/kernel` — every one is TS2305 after ' + + 'upgrade, on every public entry (pinned by symbol identity in ' + + '`plugin-runtime-retirement.test.ts`). Nothing regresses at runtime, because ' + + 'nothing called anything: a caller that believed it was hot-loading a plugin was ' + + 'already only building an object. Boot-time composition through `defineStack` is ' + + 'unchanged.', + }, ], };