From b7d88fce1fc7e0c7d90b1a108bec6b6d59012de4 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 08:47:55 +0000 Subject: [PATCH 1/2] refactor(spec)!: retire activationEvents (both keys) and the ActivationEventSchema vocabulary (#4657, ADR-0049) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lazy plugin activation was declared on two authorable surfaces and implemented by no runtime in any repo — every plugin activates immediately on load/registration. Enforce-or-remove, ruled remove: - kernel DynamicLoadRequest.activationEvents → retiredKey() tombstone (non-strict schema; a plain delete would strip silently) - studio StudioPluginManifest.activationEvents → strict-parse guidance prescription (with the former activation/events/onActivate aliases) - ActivationEventSchema / ActivationEvent def deleted from ./kernel and ./studio (orphaned value schema, #3950); manifest + authorable-surface baseline lines dropped deliberately (#4650 route: defs no longer emitted / def not root-reachable — gate-adjudicated) - ADR-0087 D3 semantic migration plugin-activation-events-retired (no sys_metadata source exists for a D2 rewrite) - compiler-API export pin with anti-vacuity guards; docs + changeset Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- .changeset/retire-activation-events.md | 65 ++++++++ content/docs/plugins/development.mdx | 16 +- .../docs/references/kernel/plugin-runtime.mdx | 26 +-- content/docs/references/studio/meta.json | 3 +- .../docs/references/studio/plugin-runtime.mdx | 33 ---- content/docs/references/studio/plugin.mdx | 1 - docs/protocol-upgrade-guide.md | 5 + packages/spec/PLUGIN_STANDARDS.md | 4 +- packages/spec/api-surface.json | 4 - packages/spec/authorable-surface.json | 7 +- packages/spec/json-schema.manifest.json | 4 +- packages/spec/spec-changes.json | 14 ++ .../activation-events-retirement.test.ts | 145 +++++++++++++++++ .../spec/src/kernel/plugin-runtime.test.ts | 87 ++++------ .../spec/src/kernel/plugin-runtime.zod.ts | 103 +++++------- packages/spec/src/migrations/registry.ts | 59 ++++++- packages/spec/src/studio/index.ts | 11 +- packages/spec/src/studio/plugin.test.ts | 152 +++++------------- packages/spec/src/studio/plugin.zod.ts | 82 +++++----- 19 files changed, 456 insertions(+), 365 deletions(-) create mode 100644 .changeset/retire-activation-events.md delete mode 100644 content/docs/references/studio/plugin-runtime.mdx create mode 100644 packages/spec/src/kernel/activation-events-retirement.test.ts diff --git a/.changeset/retire-activation-events.md b/.changeset/retire-activation-events.md new file mode 100644 index 0000000000..99bfb554f0 --- /dev/null +++ b/.changeset/retire-activation-events.md @@ -0,0 +1,65 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: remove `activationEvents` (both keys) and the `ActivationEventSchema` vocabulary — lazy activation that no runtime ever implemented (#4657, ADR-0049) + +`activationEvents` promised lazy plugin activation ("plugins remain dormant +until an activation event fires") on two authorable surfaces — +`DynamicLoadRequest.activationEvents` (`@objectstack/spec/kernel`) and +`StudioPluginManifest.activationEvents` (`@objectstack/spec/studio`, the +`defineStudioPlugin` input) — and **no runtime in objectstack / cloud / +cloud-v1 / objectui ever read either key** (four-repo bare-name scan in #4657, +re-verified at implementation time). Every plugin has always activated +immediately on load/registration; cloud-v1's own ROADMAP recorded lazy +activation as ❌ unimplemented (planned v0.4.0). That is ADR-0049's +declared ≠ enforced shape in the semantically-lying direction: an author +writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]` +expected deferral and got eager activation with a clean parse. + +#4653 had just converged the two `ActivationEventSchema` declarations onto one +structured `{ type, pattern }` form inside this same unreleased major; with the +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. + +Migration (FROM → TO): + +- `activationEvents` in a `defineStudioPlugin` input / `StudioPluginManifest` + value — v16 string form (`['*']`, `['onMetadataType:flow']`) or v17-rc + structured form (`[{ type: 'onStartup', pattern: '*' }]`) alike → + **delete the key**. There is no replacement value: eager activation is the + only behaviour there has ever been, and `activate()` still runs at + registration time. The strict manifest parse rejects the key (and its former + VS Code-flavoured aliases `activation` / `events` / `onActivate`) with this + prescription. +- `activationEvents` in a `DynamicLoadRequest` value → **delete the key**. + Tombstoned, not silently stripped — `DynamicLoadRequestSchema` is not + `.strict()`, so a `retiredKey()` tombstone makes authoring it a `tsc` error + and a parse error carrying the prescription. +- `import { ActivationEventSchema, ActivationEvent } from '@objectstack/spec/kernel'` + (or `/studio`) → **no replacement export** (TS2305 after upgrade). Nothing + consumed the vocabulary; an exported schema with no consumer is read as a + capability by whoever finds it (#3950), so the orphaned def goes with the + keys. +- Lazy activation is a **new capability**: if it is ever built it returns via + the enforce route of ADR-0049 through a new ADR — executor first, vocabulary + second — not by re-declaring inert keys. + +Self-check (#4535 §5): TS2305 — yes, two removed exports on two entries; +metadata migration — none possible or needed (`StudioPluginManifest` is TS +configuration parsed by `defineStudioPlugin`, a root schema never stored in +`sys_metadata`; `DynamicLoadRequest` is a runtime request shape with no +caller — no stored row exists for a D2 conversion to rewrite, so the change is +one ADR-0087 D3 semantic record, `plugin-activation-events-retired`); shape +change — two keys removed, zero behaviour change (eager activation before and +after, byte-identical). + +The retirement kit: `retiredKey()` tombstone on the non-strict kernel schema; +strict-parse `guidance` prescriptions on the studio manifest (including the +three former aliases); ADR-0087 D3 semantic migration; baselines +(`authorable-surface.json` — one `[RETIRED]` line, five lines dropped +deliberately with the defs; `json-schema.manifest.json` — `kernel/ActivationEvent` +and `studio/ActivationEvent` def removals; `api-surface.json`) regenerated +deliberately; compiler-API export pin (`activation-events-retirement.test.ts`, +zero holders across every public entry) — sabotage-verified. diff --git a/content/docs/plugins/development.mdx b/content/docs/plugins/development.mdx index e40621b6b4..72123d65aa 100644 --- a/content/docs/plugins/development.mdx +++ b/content/docs/plugins/development.mdx @@ -384,7 +384,6 @@ export const manifest = defineStudioPlugin({ name: 'Flow Designer', version: '2.0.0', description: 'Visual flow builder for automation workflows', - activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }], contributes: { metadataViewers: [{ @@ -434,16 +433,13 @@ export const manifest = defineStudioPlugin({ }); ``` -### Activation Events +### Activation -Control when your plugin loads with activation events: - -| Pattern | Trigger | -|:---|:---| -| `*` | Activate immediately (eager, default) | -| `onMetadataType:object` | When metadata type "object" is loaded | -| `onCommand:myPlugin.doSomething` | When command is invoked | -| `onView:myPlugin.myPanel` | When panel is opened | +Every Studio plugin loads and activates immediately on registration — `activate()` +runs at registration time, unconditionally. The former `activationEvents` manifest +key was removed in v17 (#4657, ADR-0049): it declared lazy activation that no +Studio host ever implemented, so a manifest that still carries it now fails the +parse with the upgrade prescription. Delete the key. ### View Modes diff --git a/content/docs/references/kernel/plugin-runtime.mdx b/content/docs/references/kernel/plugin-runtime.mdx index cdfdad9e4d..84a258cda3 100644 --- a/content/docs/references/kernel/plugin-runtime.mdx +++ b/content/docs/references/kernel/plugin-runtime.mdx @@ -19,16 +19,12 @@ Inspired by: - Kubernetes Operator pattern (reconciliation loop) -- VS Code Extension Host (activation events) - This protocol enables: - Runtime load/unload of plugins without kernel restart - Plugin discovery from registries and local filesystem -- Activation events (load plugin only when needed) - - Safe unload with dependency awareness @@ -38,27 +34,13 @@ This protocol enables: ## TypeScript Usage ```typescript -import { ActivationEventSchema, DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel'; -import type { ActivationEvent, DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel'; +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 = ActivationEventSchema.parse(data); +const result = DynamicLoadRequestSchema.parse(data); ``` ---- - -## ActivationEvent - -Lazy activation trigger for a dynamic plugin - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>` | ✅ | Trigger type for lazy activation | -| **pattern** | `string` | ✅ | Match pattern for the activation trigger | - - --- ## DynamicLoadRequest @@ -71,7 +53,7 @@ Request to dynamically load a plugin at runtime | :--- | :--- | :--- | :--- | | **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** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately | +| **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 | diff --git a/content/docs/references/studio/meta.json b/content/docs/references/studio/meta.json index 4f882cd142..d38164672b 100644 --- a/content/docs/references/studio/meta.json +++ b/content/docs/references/studio/meta.json @@ -3,7 +3,6 @@ "pages": [ "flow-builder", "object-designer", - "plugin", - "plugin-runtime" + "plugin" ] } \ No newline at end of file diff --git a/content/docs/references/studio/plugin-runtime.mdx b/content/docs/references/studio/plugin-runtime.mdx deleted file mode 100644 index 9fe31c2ac0..0000000000 --- a/content/docs/references/studio/plugin-runtime.mdx +++ /dev/null @@ -1,33 +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/. */} - -## TypeScript Usage - -```typescript -import { ActivationEventSchema } from '@objectstack/spec/studio'; -import type { ActivationEvent } from '@objectstack/spec/studio'; - -// Validate data -const result = ActivationEventSchema.parse(data); -``` - ---- - -## ActivationEvent - -Lazy activation trigger for a dynamic plugin - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>` | ✅ | Trigger type for lazy activation | -| **pattern** | `string` | ✅ | Match pattern for the activation trigger | - - ---- - diff --git a/content/docs/references/studio/plugin.mdx b/content/docs/references/studio/plugin.mdx index 3f75c95738..68fe8238c0 100644 --- a/content/docs/references/studio/plugin.mdx +++ b/content/docs/references/studio/plugin.mdx @@ -245,7 +245,6 @@ const result = ActionContributionSchema.parse(data); | **description** | `string` | optional | Plugin description | | **author** | `string` | optional | Author | | **contributes** | `{ metadataViewers: { id: string; metadataTypes: string[]; label: string; priority: number; … }[]; sidebarGroups: { key: string; label: string; icon?: string; metadataTypes: string[]; … }[]; actions: { id: string; label: string; icon?: string; location: Enum<'toolbar' \| 'contextMenu' \| 'commandPalette'>; … }[]; metadataIcons: { metadataType: string; label: string; icon: string }[]; … }` | ✅ | | -| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | ✅ | | --- diff --git a/docs/protocol-upgrade-guide.md b/docs/protocol-upgrade-guide.md index b5a3e265c4..918e9ab696 100644 --- a/docs/protocol-upgrade-guide.md +++ b/docs/protocol-upgrade-guide.md @@ -180,6 +180,8 @@ The object capability block closes out the same ADR-0049 pass: `enable.trash` an The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (#4579): `OpenApi31ExtensionsSchema` (`webhooks` / `callbacks` / `jsonSchemaDialect` / `pathItemReferences`) with `OpenApiWebhookEventSchema` and `CallbackSchema` under it. Declared-but-unenforced end to end: the REST server's `normalizeConfig` forwards only `api`/`crud`/`metadata`/`batch`/`routes`, the served /openapi.json is the pre-generated contract enriched with the live server URL and registered objects, and `gen:openapi` never read a webhook or callback — so a definition authored under `openApi31.webhooks` never appeared in any served document, and zero import-level consumers existed across objectstack / cloud / objectui. `RestServerConfig` is plugin TS configuration (the REST plugin constructor / `plugin-hono-server` `restConfig`), never a stored metadata shape: the stack tree's own `api` block declares only its four scoping/auth knobs, so no `sys_metadata` row can carry `openApi31` and there is no source for the chain to rewrite — one semantic TODO for config authors rather than a stack conversion, the `validateOnly` shape. The key itself is tombstoned (the schema is not `.strict()`; a plain delete would strip it silently), and a config-driven webhooks/callbacks synthesis, if ever wanted, returns via the enforce route of ADR-0049 through a new ADR. +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. + ### Mechanical (applied for you) | Conversion | Surface | Change | Load window | @@ -274,6 +276,9 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block ( - **`driver-capabilities-inert-bits-removed`** — `data.DriverCapabilities.create / data.DriverCapabilities.read / data.DriverCapabilities.update / data.DriverCapabilities.delete / data.DriverCapabilities.bulkCreate / data.DriverCapabilities.bulkUpdate / data.DriverCapabilities.bulkDelete / data.DriverCapabilities.transactions / data.DriverCapabilities.savepoints / data.DriverCapabilities.isolationLevels / data.DriverCapabilities.queryFilters / data.DriverCapabilities.queryAggregations / data.DriverCapabilities.querySorting / data.DriverCapabilities.queryPagination / data.DriverCapabilities.queryWindowFunctions / data.DriverCapabilities.querySubqueries / data.DriverCapabilities.queryCTE / data.DriverCapabilities.joins / data.DriverCapabilities.fullTextSearch / data.DriverCapabilities.jsonQuery / data.DriverCapabilities.geospatialQuery / data.DriverCapabilities.streaming / data.DriverCapabilities.jsonFields / data.DriverCapabilities.arrayFields / data.DriverCapabilities.vectorSearch / data.DriverCapabilities.schemaSync / data.DriverCapabilities.migrations / data.DriverCapabilities.indexes / data.DriverCapabilities.connectionPooling / data.DriverCapabilities.preparedStatements / data.DriverCapabilities.queryCache` → (removed — delete the keys. A driver advertises a capability by implementing the corresponding IDataDriver method; the three bits that survive because method presence cannot carry the signal are `queryDateGranularity`, `autonumber` and `batchSchemaSync`) - 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. --- diff --git a/packages/spec/PLUGIN_STANDARDS.md b/packages/spec/PLUGIN_STANDARDS.md index 75cd3716a4..5d65f81993 100644 --- a/packages/spec/PLUGIN_STANDARDS.md +++ b/packages/spec/PLUGIN_STANDARDS.md @@ -162,7 +162,7 @@ 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`** — Lazy activation triggers, shaped `{ type, pattern }`. Types: `onCommand`, `onRoute`, `onObject`, `onEvent`, `onService`, `onSchedule`, `onStartup`, `onMetadataType`, `onView`. Since v17 this is the platform's **single** activation vocabulary — `@objectstack/spec/studio` re-exports this exact declaration rather than carrying its own `z.string()` (#4653) +- ~~`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 @@ -178,4 +178,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 with activation events and discovery | +| Dynamic Loading | ✅ | `plugin-runtime.zod.ts` — Runtime load/unload (eager activation; the unenforced activation-events vocabulary was removed in #4657) | diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index e1d29a62bf..6e0d9ea324 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -1440,8 +1440,6 @@ "validationMessageTranslationKey (function)" ], "./kernel": [ - "ActivationEvent (type)", - "ActivationEventSchema (const)", "AdvancedPluginLifecycleConfig (type)", "AdvancedPluginLifecycleConfigSchema (const)", "ArtifactChecksum (type)", @@ -4003,8 +4001,6 @@ "ActionContributionLocation (type)", "ActionContributionLocationSchema (const)", "ActionContributionSchema (const)", - "ActivationEvent (type)", - "ActivationEventSchema (const)", "BUILT_IN_NODE_DESCRIPTORS (const)", "CommandContribution (type)", "CommandContributionSchema (const)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index d1d42a6099..cb969b242d 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -4188,8 +4188,6 @@ "integration/WebhookConfig:timeoutMs", "integration/WebhookConfig:triggers", "integration/WebhookConfig:url", - "kernel/ActivationEvent:pattern", - "kernel/ActivationEvent:type", "kernel/AdvancedPluginLifecycleConfig:degradation", "kernel/AdvancedPluginLifecycleConfig:health", "kernel/AdvancedPluginLifecycleConfig:hotReload", @@ -4283,7 +4281,7 @@ "kernel/DistributedStateConfig:provider", "kernel/DistributedStateConfig:replication", "kernel/DistributedStateConfig:ttl", - "kernel/DynamicLoadRequest:activationEvents", + "kernel/DynamicLoadRequest:activationEvents [RETIRED]", "kernel/DynamicLoadRequest:config", "kernel/DynamicLoadRequest:pluginId", "kernel/DynamicLoadRequest:priority", @@ -5369,8 +5367,6 @@ "studio/ActionContribution:label", "studio/ActionContribution:location", "studio/ActionContribution:metadataTypes", - "studio/ActivationEvent:pattern", - "studio/ActivationEvent:type", "studio/CommandContribution:icon", "studio/CommandContribution:id", "studio/CommandContribution:label", @@ -5516,7 +5512,6 @@ "studio/StudioPluginContributions:metadataViewers", "studio/StudioPluginContributions:panels", "studio/StudioPluginContributions:sidebarGroups", - "studio/StudioPluginManifest:activationEvents", "studio/StudioPluginManifest:author", "studio/StudioPluginManifest:contributes", "studio/StudioPluginManifest:description", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 83877e902a..04c7371ef7 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1,5 +1,5 @@ { - "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema — remove a key ONLY for a deliberate retirement. See #2978.", + "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema \u2014 remove a key ONLY for a deliberate retirement. See #2978.", "schemas": [ "ai/AIModelConfig", "ai/AIUsageRecord", @@ -883,7 +883,6 @@ "integration/WebhookConfig", "integration/WebhookEvent", "integration/WebhookSignatureAlgorithm", - "kernel/ActivationEvent", "kernel/AdvancedPluginLifecycleConfig", "kernel/ArtifactChecksum", "kernel/ArtifactFileEntry", @@ -1144,7 +1143,6 @@ "shared/ViewName", "studio/ActionContribution", "studio/ActionContributionLocation", - "studio/ActivationEvent", "studio/CommandContribution", "studio/ERDiagramConfig", "studio/ERLayoutAlgorithm", diff --git a/packages/spec/spec-changes.json b/packages/spec/spec-changes.json index 4bf8ed0153..3f69fef82f 100644 --- a/packages/spec/spec-changes.json +++ b/packages/spec/spec-changes.json @@ -474,6 +474,13 @@ "migrationId": "driver-capabilities-inert-bits-removed", "toMajor": 17, "rationale": "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." + }, + { + "surface": "kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents", + "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." } ], "removed": [] @@ -1007,6 +1014,13 @@ "migrationId": "driver-capabilities-inert-bits-removed", "toMajor": 17, "rationale": "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." + }, + { + "surface": "kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents", + "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." } ], "removed": [] diff --git a/packages/spec/src/kernel/activation-events-retirement.test.ts b/packages/spec/src/kernel/activation-events-retirement.test.ts new file mode 100644 index 0000000000..82cc32aba6 --- /dev/null +++ b/packages/spec/src/kernel/activation-events-retirement.test.ts @@ -0,0 +1,145 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; + +// ─── [#4657] `ActivationEventSchema` / `ActivationEvent` are REMOVED ───────── +// +// ADR-0049 enforce-or-remove, ruled REMOVE: the activation-event vocabulary +// declared lazy plugin activation ("plugins remain dormant until an activation +// event fires") that NO runtime in objectstack / cloud / cloud-v1 / objectui +// ever implemented — every plugin activates immediately on load/registration, +// and cloud-v1's ROADMAP recorded the capability as unimplemented. Both keys +// that embedded the schema are retired in this change +// (`DynamicLoadRequest.activationEvents` → retiredKey() tombstone; +// `StudioPluginManifest.activationEvents` → strict-parse guidance rejection), +// which left the def an orphaned value schema: an exported schema with no +// consumer is read as a capability by whoever finds it (#3950), so the +// declaration and both its entry-point exports (`./kernel`, and the `./studio` +// re-export #4653 had just added) are deleted. +// +// Why a compiler-API pin rather than a type-level one: #4642 established that +// a compile-time conditional-type pin in this package is a no-op (tsconfig +// excludes `**/*.test.ts`; vitest never enables `typecheck`), so the +// 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. +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; + const { resolve, relative } = await import('node:path'); + const { 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 pins 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: the enumeration must have found the real surface — the two + // entries that used to export the retired names most of all. + for (const needed of ['./kernel', './studio']) { + 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); + // Without this guard a resolution failure would make every `not.toContain` + // below pass vacuously — the exact way 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) => { + const out: string[] = []; + for (const sub of Object.keys(entries)) { + if (exportsOf(sub).some((e) => e.getName() === name)) out.push(sub); + } + return out; + }; + + // 1. Anti-vacuity on the two surfaces that carried the names: both still + // export a non-trivial surface (so "does not contain" cannot pass by + // resolving nothing), and the SURVIVING neighbours stand — the parents + // whose keys were retired most of all. + const kernelNames = exportsOf('./kernel').map((e) => e.getName()); + 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'); + expect(studioNames).toContain('StudioPluginManifestSchema'); + expect(studioNames).toContain('StudioPluginContributionsSchema'); + + // 2. The removal itself: NO public entry exports either retired name — not + // the old owners, and not any other entry that might "helpfully" adopt + // them. A re-export of anything under the bare name would tell authors + // the platform has an activation vocabulary again (the C14/C15 lesson: + // a re-export can lie about the domain even when the symbol is honest). + for (const retired of ['ActivationEventSchema', 'ActivationEvent']) { + expect(holdersOf(retired), `${retired} must have zero holders`).toEqual([]); + } + }); + + it('keeps the runtime namespaces consistent with the compiler view', async () => { + const kernel = await import('./index'); + const studio = await import('../studio/index'); + + for (const [label, ns] of [['./kernel', kernel], ['./studio', studio]] as const) { + 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('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'); + 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). + const manifest = { id: 'objectstack.my-plugin', name: 'My Plugin' }; + expect(StudioPluginManifestSchema.parse(manifest)).not.toHaveProperty('activationEvents'); + expect(() => + StudioPluginManifestSchema.parse({ + ...manifest, + activationEvents: [{ type: 'onStartup', pattern: '*' }], + }), + ).toThrow(/activationEvents.*removed.*#4657/s); + }); +}); diff --git a/packages/spec/src/kernel/plugin-runtime.test.ts b/packages/spec/src/kernel/plugin-runtime.test.ts index 2f41f38bac..4d80edd74e 100644 --- a/packages/spec/src/kernel/plugin-runtime.test.ts +++ b/packages/spec/src/kernel/plugin-runtime.test.ts @@ -2,7 +2,6 @@ import { describe, it, expect } from 'vitest'; import { DynamicPluginOperationSchema, PluginSourceSchema, - ActivationEventSchema, DynamicLoadRequestSchema, DynamicUnloadRequestSchema, DynamicPluginResultSchema, @@ -64,57 +63,10 @@ describe('Plugin Runtime Management Protocol', () => { }); }); - describe('ActivationEventSchema', () => { - it('should accept command activation', () => { - const event = { - type: 'onCommand', - pattern: 'analytics.generateReport', - }; - const result = ActivationEventSchema.parse(event); - expect(result.type).toBe('onCommand'); - expect(result.pattern).toBe('analytics.generateReport'); - }); - - it('should accept route activation', () => { - const event = { - type: 'onRoute', - pattern: '/api/v1/analytics/*', - }; - const result = ActivationEventSchema.parse(event); - expect(result.type).toBe('onRoute'); - }); - - it('should accept all activation types', () => { - const types = [ - 'onCommand', 'onRoute', 'onObject', - 'onEvent', 'onService', 'onSchedule', 'onStartup', - // [#4653] Widened to the union of the two pre-v17 vocabularies when - // `./studio` converged onto this declaration. - 'onMetadataType', 'onView', - ]; - types.forEach((type) => { - const result = ActivationEventSchema.parse({ type, pattern: '*' }); - expect(result.type).toBe(type); - }); - }); - - // [#4653] The whole point of converging on the structured form: a mistyped - // trigger is rejected at authoring time. The pre-v17 studio `z.string()` - // accepted every one of these silently. - it('rejects a mistyped trigger instead of silently accepting it', () => { - for (const type of ['onMetadatType', 'onview', 'banana', '']) { - expect(() => ActivationEventSchema.parse({ type, pattern: 'flow' })).toThrow(); - } - }); - - // [#4653] The studio string form is not silently coerced — it fails loudly. - // That is the migration's whole failure mode, so it is pinned here. - it('rejects the pre-v17 studio string form', () => { - for (const legacy of ['*', 'onMetadataType:flow', 'onCommand:my.cmd']) { - expect(() => ActivationEventSchema.parse(legacy)).toThrow(); - } - }); - }); + // `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', () => { @@ -133,7 +85,7 @@ describe('Plugin Runtime Management Protocol', () => { expect(result.timeout).toBe(60000); // default }); - it('should accept full load request with activation events', () => { + it('should accept full load request', () => { const request = { pluginId: 'com.acme.analytics', source: { @@ -141,20 +93,39 @@ describe('Plugin Runtime Management Protocol', () => { location: 'acme-analytics', version: '~2.1.0', }, - activationEvents: [ - { type: 'onRoute' as const, pattern: '/api/v1/analytics/*' }, - { type: 'onCommand' as const, pattern: 'analytics.*' }, - ], config: { apiKey: 'abc123', region: 'us-east' }, priority: 50, sandbox: true, timeout: 120000, }; const result = DynamicLoadRequestSchema.parse(request); - expect(result.activationEvents).toHaveLength(2); 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', () => { diff --git a/packages/spec/src/kernel/plugin-runtime.zod.ts b/packages/spec/src/kernel/plugin-runtime.zod.ts index 5c31c29d57..678bf6d7c7 100644 --- a/packages/spec/src/kernel/plugin-runtime.zod.ts +++ b/packages/spec/src/kernel/plugin-runtime.zod.ts @@ -12,12 +12,10 @@ import { z } from 'zod'; * Inspired by: * - OSGi Dynamic Module System (bundle lifecycle) * - Kubernetes Operator pattern (reconciliation loop) - * - VS Code Extension Host (activation events) - * + * * This protocol enables: * - Runtime load/unload of plugins without kernel restart * - Plugin discovery from registries and local filesystem - * - Activation events (load plugin only when needed) * - Safe unload with dependency awareness */ @@ -26,6 +24,7 @@ import { z } from 'zod'; * 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 @@ -67,67 +66,39 @@ export const PluginSourceSchema = lazySchema(() => z.object({ }).describe('Plugin source location for dynamic resolution')); /** - * Activation Event - * Defines when a dynamically available plugin should be activated. - * Plugins remain dormant until an activation event fires. - * - * [#4653] **This is the platform's single activation vocabulary.** Until v17 - * the name `ActivationEventSchema` resolved to two DIFFERENT declarations - * depending on the import path (#4411's trap): this structured - * `{ type, pattern }` on `./kernel`, and a bare `z.string()` on `./studio`. - * A studio plugin author wrote `activationEvents: ['onMetadataType:flow']` and - * got no validation at all — `z.string()` accepts `'onMetadatType:flow'`, and - * every other typo, forever. `./studio` now re-exports THIS declaration, so - * there is one trigger vocabulary and one place to extend it. + * REMOVED — `ActivationEventSchema` / `ActivationEvent` (#4657, ADR-0049). * - * The enum below is the **union of both sides' pre-v17 vocabularies**, because - * dropping either side's values would have silently removed a capability its - * authors were already using: + * 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. * - * | value | came from | - * |:-----------------|:-----------------------------------------------------------| - * | `onCommand` | kernel enum + studio docs (`onCommand:myPlugin.doSomething`) | - * | `onRoute` | kernel enum | - * | `onObject` | kernel enum | - * | `onEvent` | kernel enum | - * | `onService` | kernel enum | - * | `onSchedule` | kernel enum | - * | `onStartup` | kernel enum; also the target of studio's eager `'*'` | - * | `onMetadataType` | studio docs/tests (`onMetadataType:object`) — kernel lacked it | - * | `onView` | studio docs/tests (`onView:myPlugin.myPanel`) — kernel lacked it | + * 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`. * - * Deliberately NOT adopted: `priority`, and the `onInstall` / `onWebhook` - * values that cloud-v1's unreleased marketplace runtime carries. Nothing in - * any repo reads them, and adding an unenforced key is the exact debt ADR-0049 - * is retiring — they can be proposed when there is an executor that honours - * them. + * Lazy activation, if ever built, returns via the enforce route of ADR-0049: + * write the executor first, then declare exactly the vocabulary it honours. */ -export const ActivationEventSchema = lazySchema(() => z.object({ - /** - * Event type - */ - type: z.enum([ - 'onCommand', // Activate when a specific command is executed - 'onRoute', // Activate when a URL route is matched - 'onObject', // Activate when a specific object type is accessed - 'onEvent', // Activate when a system event fires - 'onService', // Activate when a service is requested - 'onSchedule', // Activate on a cron schedule - 'onStartup', // Activate immediately on startup (eager) - 'onMetadataType', // Activate when a metadata type is loaded - 'onView', // Activate when a view / panel is opened - ]).describe('Trigger type for lazy activation'), - /** - * Pattern to match (command name, route glob, object name, event pattern, etc.) - * - * The pre-v17 studio string form packed this into the same token after a - * colon — `'onCommand:myPlugin.doSomething'` is `{ type: 'onCommand', - * pattern: 'myPlugin.doSomething' }`, and eager `'*'` is - * `{ type: 'onStartup', pattern: '*' }`. - */ - pattern: z.string().describe('Match pattern for the activation trigger'), -}).describe('Lazy activation trigger for a dynamic plugin')); +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 @@ -143,13 +114,16 @@ export const DynamicLoadRequestSchema = lazySchema(() => z.object({ * Plugin source */ source: PluginSourceSchema, - + /** - * Activation events (if omitted, plugin activates immediately) + * 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: z.array(ActivationEventSchema).optional() - .describe('Lazy activation triggers; if omitted plugin starts immediately'), - + activationEvents: retiredKey(ACTIVATION_EVENTS_RETIRED), + /** * Configuration overrides for the plugin */ @@ -292,7 +266,6 @@ export const DynamicPluginResultSchema = lazySchema(() => z.object({ // Export types export type DynamicPluginOperation = z.infer; export type PluginSource = z.infer; -export type ActivationEvent = z.infer; export type DynamicLoadRequest = z.infer; export type DynamicUnloadRequest = z.infer; export type DynamicPluginResult = z.infer; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index f5f4b1ee9c..2966fc711b 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -718,7 +718,26 @@ const step17: MigrationStep = { + 'rewrite — one semantic TODO for config authors rather than a stack conversion, the ' + '`validateOnly` shape. The key itself is tombstoned (the schema is not `.strict()`; a ' + 'plain delete would strip it silently), and a config-driven webhooks/callbacks synthesis, ' - + 'if ever wanted, returns via the enforce route of ADR-0049 through a new ADR.', + + 'if ever wanted, returns via the enforce route of ADR-0049 through a new ADR.\n\n' + + '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.', conversionIds: [ 'action-execute-to-target', 'field-conditionalRequired-to-requiredWhen', @@ -1201,6 +1220,44 @@ const step17: MigrationStep = { + 'live bits keep their readers (engine.ts autonumber defer / aggregate dispatch, ' + 'plugin.ts + engine.ts batched schema sync, verify date-bucket parity).', }, + { + id: 'plugin-activation-events-retired', + surface: + 'kernel.dynamicLoadRequest.activationEvents / studio.studioPluginManifest.activationEvents', + 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)', + reason: + '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.', + 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 ' + + '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.', + }, ], }; diff --git a/packages/spec/src/studio/index.ts b/packages/spec/src/studio/index.ts index b50ef96c4e..14a4c963c4 100644 --- a/packages/spec/src/studio/index.ts +++ b/packages/spec/src/studio/index.ts @@ -26,10 +26,12 @@ export { PanelLocationSchema, CommandContributionSchema, StudioPluginContributionsSchema, - // [#4653] `ActivationEventSchema` / `ActivationEvent` are RE-EXPORTS of the - // single declaration in `kernel/plugin-runtime.zod.ts`, not a second source. - // Studio plugin authors keep importing them from `@objectstack/spec/studio`. - ActivationEventSchema, + // [#4657] `ActivationEventSchema` / `ActivationEvent` are REMOVED (ADR-0049 + // enforce-or-remove): no runtime ever read an activation event — every + // plugin activates immediately — so the vocabulary was retired with the + // `activationEvents` keys that embedded it. Do not re-export a substitute + // under these names; the removal is pinned by + // kernel/activation-events-retirement.test.ts. StudioPluginManifestSchema, // Types @@ -43,7 +45,6 @@ export { type CommandContribution, type StudioPluginContributions, type StudioPluginManifest, - type ActivationEvent, // Helpers defineStudioPlugin, diff --git a/packages/spec/src/studio/plugin.test.ts b/packages/spec/src/studio/plugin.test.ts index c43e79171c..9d00a5ae9a 100644 --- a/packages/spec/src/studio/plugin.test.ts +++ b/packages/spec/src/studio/plugin.test.ts @@ -10,7 +10,6 @@ import { PanelContributionSchema, CommandContributionSchema, StudioPluginContributionsSchema, - ActivationEventSchema, StudioPluginManifestSchema, defineStudioPlugin, } from './plugin.zod'; @@ -200,33 +199,12 @@ describe('StudioPluginContributionsSchema', () => { }); }); -describe('ActivationEventSchema', () => { - it('should accept valid activation events', () => { - // [#4653] The four events this file documented pre-v17, in the structured - // form they converged onto. Every one still expresses what it used to. - const events = [ - { type: 'onStartup', pattern: '*' }, // was '*' - { type: 'onMetadataType', pattern: 'object' }, // was 'onMetadataType:object' - { type: 'onCommand', pattern: 'myPlugin.do' }, // was 'onCommand:myPlugin.do' - { type: 'onView', pattern: 'myPanel' }, // was 'onView:myPanel' - ]; - events.forEach(e => { - expect(() => ActivationEventSchema.parse(e)).not.toThrow(); - }); - }); - - it('should reject the pre-v17 bare-string form', () => { - // Loud, not silently coerced — the manual migration depends on this. - expect(() => ActivationEventSchema.parse('onMetadataType:object')).toThrow(); - expect(() => ActivationEventSchema.parse('*')).toThrow(); - expect(() => ActivationEventSchema.parse(123)).toThrow(); - }); - - it('should reject an unknown trigger type', () => { - // The capability the old `z.string()` declaration could never provide. - expect(() => ActivationEventSchema.parse({ type: 'onMetadatType', pattern: 'flow' })).toThrow(); - }); -}); +// `ActivationEventSchema` was REMOVED (#4657, ADR-0049): no Studio host ever +// read an activation event — plugins load and activate immediately on +// registration — so the vocabulary retired with the `activationEvents` keys +// that embedded it. Export-surface pins live in +// ../kernel/activation-events-retirement.test.ts; the manifest-level +// rejection pins are in the StudioPluginManifestSchema block below. describe('StudioPluginManifestSchema', () => { const minimalManifest = { @@ -237,8 +215,9 @@ describe('StudioPluginManifestSchema', () => { it('should accept minimal manifest with defaults', () => { const result = StudioPluginManifestSchema.parse(minimalManifest); expect(result.version).toBe('0.0.1'); - // [#4653] Eager activation, structured. FROM `['*']`. - expect(result.activationEvents).toEqual([{ type: 'onStartup', pattern: '*' }]); + // [#4657] No activation-events key materializes — eager activation is the + // runtime's unconditional behaviour, not a manifest setting. + expect(result).not.toHaveProperty('activationEvents'); expect(result.contributes).toBeDefined(); expect(result.description).toBeUndefined(); expect(result.author).toBeUndefined(); @@ -260,20 +239,40 @@ describe('StudioPluginManifestSchema', () => { modes: ['preview', 'design', 'data'], }], }, - activationEvents: [{ type: 'onMetadataType', pattern: 'object' }], }; expect(() => StudioPluginManifestSchema.parse(manifest)).not.toThrow(); }); - it('rejects a manifest still carrying the pre-v17 string activation events', () => { - // The migration is manual (no conversion can reach a studio plugin - // manifest — it is a root schema, never part of a stack), so the ONLY - // thing standing between a stale manifest and a wrong-shaped plugin is - // this parse failing. Pinned so it can never soften into a coercion. + // ─── [#4657] `activationEvents` retirement pins (ADR-0049) ─────────── + it('rejects activationEvents with the retirement prescription — structured form', () => { + // The post-#4653 structured form: the shape an up-to-date v17-rc author + // would have written. The strict parse must carry the prescription, not a + // bare "unrecognized key". + expect(() => StudioPluginManifestSchema.parse({ + ...minimalManifest, + activationEvents: [{ type: 'onMetadataType', pattern: 'object' }], + })).toThrow(/activationEvents.*removed.*17\.0\.0.*#4657.*Delete the key/s); + }); + + it('rejects activationEvents with the retirement prescription — pre-v17 string form', () => { + // A v16 manifest jumping straight to the release gets the same + // prescription: the VALUE shape no longer matters, the KEY is retired. expect(() => StudioPluginManifestSchema.parse({ ...minimalManifest, activationEvents: ['onMetadataType:object'], - })).toThrow(); + })).toThrow(/activationEvents.*removed.*#4657/s); + }); + + it('rejects the former VS Code-flavoured aliases with the same prescription', () => { + // `activation` / `events` / `onActivate` used to alias `activationEvents`; + // an alias must never point at a key the schema cannot accept, so each now + // carries the retirement guidance itself. + for (const key of ['activation', 'events', 'onActivate']) { + expect(() => StudioPluginManifestSchema.parse({ + ...minimalManifest, + [key]: ['*'], + })).toThrow(/activationEvents.*removed.*#4657/s); + } }); it('should reject invalid id format', () => { @@ -303,8 +302,8 @@ describe('defineStudioPlugin', () => { }); expect(result.id).toBe('objectstack.flow-designer'); expect(result.version).toBe('0.0.1'); - // [#4653] FROM `['*']` — eager activation, now structured. - expect(result.activationEvents).toEqual([{ type: 'onStartup', pattern: '*' }]); + // [#4657] No activation-events key — plugins activate eagerly, always. + expect(result).not.toHaveProperty('activationEvents'); }); it('should throw on invalid input', () => { @@ -312,74 +311,7 @@ describe('defineStudioPlugin', () => { }); }); -// ─── [#4653] Dual-source regression pin ────────────────────────────── -// -// RUNTIME assertions, deliberately. #4642 established that a compile-time pin -// in `packages/spec` is a no-op: `tsconfig.json` excludes `**/*.test.ts` and -// `vitest.config.ts` never enables `typecheck`, so neither path type-checks a -// test file — a conditional-type pin here would be dead text. These run. -// -// What they defend: `ActivationEventSchema` naming ONE declaration across both -// published entries. Re-introducing a local declaration in `studio/plugin.zod.ts` -// (the pre-v17 `z.string()`, or any other) re-creates the #4411 trap where the -// validation an author gets depends on which subpath they imported from, and -// puts the name straight back on `dual-source-exports.baseline.json`. -describe('[#4653] ActivationEventSchema is single-source across ./kernel and ./studio', () => { - it('both entry points export the very same declaration', async () => { - const kernelEntry = await import('../kernel/index'); - const studioEntry = await import('../studio/index'); - - // Identity, not shape: `lazySchema` returns one Proxy per declaration site, - // so two declarations can never be `toBe`-equal however alike they look. - // This is exactly what check:dual-source-exports measures (symbol identity - // after alias resolution) — asserted here at runtime so a re-split fails - // `pnpm test` too, not only the gate. - expect(studioEntry.ActivationEventSchema).toBe(kernelEntry.ActivationEventSchema); - }); - - it('the shared declaration is the structured kernel form on BOTH entries', async () => { - const kernelEntry = await import('../kernel/index'); - const studioEntry = await import('../studio/index'); - - for (const [entry, schema] of [ - ['./kernel', kernelEntry.ActivationEventSchema], - ['./studio', studioEntry.ActivationEventSchema], - ] as const) { - // Structured form accepted... - expect( - schema.parse({ type: 'onMetadataType', pattern: 'flow' }), - `${entry} must accept the structured form`, - ).toEqual({ type: 'onMetadataType', pattern: 'flow' }); - // ...bare string rejected, on both paths, identically. - expect( - () => schema.parse('onMetadataType:flow'), - `${entry} must reject the pre-v17 string form`, - ).toThrow(); - } - }); - - it('the trigger vocabulary is the union of both pre-v17 vocabularies', async () => { - const studioEntry = await import('../studio/index'); - // 7 from kernel + `onMetadataType` / `onView` rescued from studio's docs. - // Losing either of the last two would silently drop a capability studio - // authors were already using. - const union = [ - 'onCommand', 'onRoute', 'onObject', 'onEvent', - 'onService', 'onSchedule', 'onStartup', - 'onMetadataType', 'onView', - ]; - for (const type of union) { - expect( - () => studioEntry.ActivationEventSchema.parse({ type, pattern: '*' }), - `'${type}' must stay in the vocabulary`, - ).not.toThrow(); - } - // And it is exactly that set — a tenth value would mean an undeclared - // vocabulary change slipped in (e.g. cloud-v1's `onInstall` / `onWebhook`, - // deliberately not adopted: nothing reads them, see ADR-0049 / #4657). - const options = (studioEntry.ActivationEventSchema as unknown as { - shape: { type: { options: string[] } }; - }).shape.type.options; - expect([...options].sort()).toEqual([...union].sort()); - }); -}); +// The [#4653] single-source pin that lived here retired WITH the schema it +// defended: `ActivationEventSchema` is removed from both entries (#4657, +// ADR-0049), and the export-surface pin — no entry may export the name at all +// — lives in ../kernel/activation-events-retirement.test.ts. diff --git a/packages/spec/src/studio/plugin.zod.ts b/packages/spec/src/studio/plugin.zod.ts index 73a8e8f9a4..61fc509df6 100644 --- a/packages/spec/src/studio/plugin.zod.ts +++ b/packages/spec/src/studio/plugin.zod.ts @@ -61,8 +61,6 @@ import { z } from 'zod'; /** Supported view modes for metadata viewers */ import { lazySchema } from '../shared/lazy-schema'; import { strictObject } from '../shared/strict-object'; -// [#4653] The one activation vocabulary — see the note above the re-export below. -import { ActivationEventSchema } from '../kernel/plugin-runtime.zod'; /** * Shared history for this file (#4001). @@ -288,34 +286,26 @@ export const StudioPluginContributionsSchema = lazySchema(() => strictObject({ export type StudioPluginContributions = z.infer; -// ─── Activation Events ─────────────────────────────────────────────── - -/** - * [#4653] The local `z.string()` declaration that used to live here is gone — - * `ActivationEventSchema` now names ONE declaration platform-wide, the - * structured `{ type, pattern }` in `kernel/plugin-runtime.zod.ts`, re-exported - * below (dual-source cleanup, #4535 C5). - * - * Why this side gave way, when the string form was the friendlier one: it - * validated nothing. `z.string()` accepted `''`, `'banana'` and — the case that - * matters — `'onMetadatType:flow'`, so the vocabulary this file documented - * (`*`, `onMetadataType:`, `onCommand:`, `onView:`) lived only in prose and a - * misspelling stayed silent forever. The kernel form gates the trigger on an - * enum at authoring time, which is the whole point of declaring it. Its enum - * was widened to the union of both vocabularies in the same change, so nothing - * a studio author could express before is unexpressible now. - * - * FROM (pre-v17) TO (v17+) - * activationEvents: ['*'] [{ type: 'onStartup', pattern: '*' }] - * activationEvents: ['onMetadataType:flow'] [{ type: 'onMetadataType', pattern: 'flow' }] - * activationEvents: ['onCommand:my.cmd'] [{ type: 'onCommand', pattern: 'my.cmd' }] - * activationEvents: ['onView:my.panel'] [{ type: 'onView', pattern: 'my.panel' }] - * - * A manifest still carrying the string form fails `StudioPluginManifestSchema` - * loudly at parse (it is a `strictObject`); there is no silent coercion, and no - * automatic conversion is possible — see the changeset for why. - */ -export { ActivationEventSchema, type ActivationEvent } from '../kernel/plugin-runtime.zod'; +// ─── Activation Events — REMOVED (#4657, ADR-0049) ─────────────────── +// +// The `activationEvents` key (and the `ActivationEventSchema` it embedded, +// which #4653 had just converged onto the kernel's structured `{ type, +// pattern }` form) is retired: no Studio host — no runtime in any repo — ever +// read it. Every plugin has always loaded and activated immediately on +// registration, so the key's declared semantics ("when to load this plugin") +// were a lie an author had no way to detect: `activationEvents: +// [{ type: 'onMetadataType', pattern: 'flow' }]` parsed clean and deferred +// nothing. The strict manifest parse below now rejects the key with the +// prescription (see `guidance`). Lazy activation, if ever built, returns via +// the enforce route of ADR-0049: executor first, vocabulary second. + +const STUDIO_ACTIVATION_EVENTS_RETIRED = + '`studioPluginManifest.activationEvents` was removed in @objectstack/spec 17.0.0 ' + + '(#4657, ADR-0049) — no Studio host ever read it: every plugin loads and activates ' + + 'immediately on registration, so the declared lazy-activation window never existed. ' + + 'Delete the key; `activate()` still runs at registration time. Lazy activation, if ' + + 'built, returns via the enforce route of ADR-0049 with a vocabulary its executor ' + + 'actually honours.'; // ─── Studio Plugin Manifest ────────────────────────────────────────── @@ -337,9 +327,19 @@ export const StudioPluginManifestSchema = lazySchema(() => strictObject({ displayName: 'name', title: 'name', publisher: 'author', vendor: 'author', contributions: 'contributes', contribute: 'contributes', - activation: 'activationEvents', events: 'activationEvents', onActivate: 'activationEvents', + // NOTE: `activation` / `events` / `onActivate` used to alias + // `activationEvents`; the target key is retired (#4657), so they moved to + // `guidance` below — an alias must never point at a key the schema cannot + // accept (the `triggerPhrases` suggester trap, see shared/strict-object.ts). }, guidance: { + // Retired key (#4657, ADR-0049) — the rejection carries the upgrade. + activationEvents: STUDIO_ACTIVATION_EVENTS_RETIRED, + // The VS Code spellings that used to alias it get the same prescription: + // pointing them at a removed key would bounce the author off a second error. + activation: STUDIO_ACTIVATION_EVENTS_RETIRED, + events: STUDIO_ACTIVATION_EVENTS_RETIRED, + onActivate: STUDIO_ACTIVATION_EVENTS_RETIRED, // VS Code manifest keys with no counterpart here. `main` is the dangerous // one: an author declares an entry point, gets a plugin that loads and // contributes nothing, and it looks exactly like a broken `activate()`. @@ -351,7 +351,9 @@ export const StudioPluginManifestSchema = lazySchema(() => strictObject({ keywords: 'there is no keyword index for Studio plugins', repository: 'manifest metadata is limited to `id` / `name` / `version` / `description` / `author`', icon: 'the manifest carries no icon — icons are Lucide names on the CONTRIBUTION (`contributes.metadataIcons`, or an action / panel / command `icon`)', - dependencies: 'Studio plugins declare no dependency graph; order activation with `activationEvents` instead', + dependencies: + 'Studio plugins declare no dependency graph — every plugin loads and activates ' + + 'immediately on registration, and contributions are independent of each other', }, }, { /** @@ -384,17 +386,11 @@ export const StudioPluginManifestSchema = lazySchema(() => strictObject({ commands: [], }), - /** - * Activation events — when to load this plugin. - * - * [#4653] The default is the structured equivalent of the pre-v17 `['*']`: - * eager activation. `'*'` did not need its own `type` — it always meant - * "activate immediately", which is exactly what `onStartup` already means on - * the kernel side, so eager survives as `onStartup` with the `'*'` pattern - * rather than as a tenth enum value that would duplicate it. - */ - activationEvents: z.array(ActivationEventSchema) - .default([{ type: 'onStartup', pattern: '*' }]), + // `activationEvents` was REMOVED here (#4657, ADR-0049) — see the section + // comment above and the `guidance` entry that rejects it with the + // prescription. Its pre-removal default (`[{ type: 'onStartup', pattern: + // '*' }]`, i.e. eager) simply wrote down the only behaviour that ever + // existed, so removing the key changes nothing at runtime. })); export type StudioPluginManifest = z.infer; From 6d3d38831aa6cf908196205582b10e21d3709dd9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 09:27:11 +0000 Subject: [PATCH 2/2] chore: regenerate spec artifacts wholesale on the post-#4616 merge base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ratchet files reset to origin/main and regenerated (never textually merged); the delta vs main is exactly this PR's intent — 2 manifest def removals, 6 authorable lines dropped / 1 [RETIRED], 4 api-surface export removals — and #4616's system/*Template*/*Notification* deletions remain absent. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- packages/spec/api-surface.json | 8 -------- packages/spec/authorable-surface.json | 22 ---------------------- packages/spec/json-schema.manifest.json | 6 +----- 3 files changed, 1 insertion(+), 35 deletions(-) diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 6e0d9ea324..fdc04193d7 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -896,7 +896,6 @@ "EmailProviderSchema (const)", "EmailServiceConfig (type)", "EmailServiceConfigSchema (const)", - "EmailTemplate (type)", "EmailTemplateDefinition (type)", "EmailTemplateDefinitionCategory (type)", "EmailTemplateDefinitionCategorySchema (const)", @@ -904,7 +903,6 @@ "EmailTemplateDefinitionSchema (const)", "EmailTemplateDefinitionVariable (type)", "EmailTemplateDefinitionVariableSchema (const)", - "EmailTemplateSchema (const)", "EmailVerificationConfig (type)", "EmailVerificationConfigSchema (const)", "EncryptionAlgorithm (type)", @@ -950,8 +948,6 @@ "HttpServerConfigSchema (const)", "ISettingsCapability (interface)", "ISettingsClient (interface)", - "InAppNotification (type)", - "InAppNotificationSchema (const)", "Incident (type)", "IncidentCategory (type)", "IncidentCategorySchema (const)", @@ -1146,8 +1142,6 @@ "PlanSchema (const)", "PresignedUrlConfig (type)", "PresignedUrlConfigSchema (const)", - "PushNotification (type)", - "PushNotificationSchema (const)", "QueueConfig (type)", "QueueConfigInput (type)", "QueueConfigSchema (const)", @@ -1188,8 +1182,6 @@ "RowLevelIsolationStrategyInput (type)", "RowLevelIsolationStrategySchema (const)", "SETTINGS_CHANGE_EVENT (const)", - "SMSTemplate (type)", - "SMSTemplateSchema (const)", "SamplingDecision (type)", "SamplingStrategyType (type)", "Schedule (type)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index cb969b242d..ccc3346a9d 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -5898,12 +5898,6 @@ "system/EmailServiceConfig:persist", "system/EmailServiceConfig:provider", "system/EmailServiceConfig:retries", - "system/EmailTemplate:attachments", - "system/EmailTemplate:body", - "system/EmailTemplate:bodyType", - "system/EmailTemplate:id", - "system/EmailTemplate:subject", - "system/EmailTemplate:variables", "system/EmailTemplateDefinition:_lock", "system/EmailTemplateDefinition:_lockDocsUrl", "system/EmailTemplateDefinition:_lockReason", @@ -6005,12 +5999,6 @@ "system/HttpServerConfig:security", "system/HttpServerConfig:static", "system/HttpServerConfig:trustProxy", - "system/InAppNotification:actionUrl", - "system/InAppNotification:dismissible", - "system/InAppNotification:expiresAt", - "system/InAppNotification:message", - "system/InAppNotification:title", - "system/InAppNotification:type", "system/Incident:affectedDataClassifications", "system/Incident:affectedSystems", "system/Incident:category", @@ -6465,12 +6453,6 @@ "system/PresignedUrlConfig:operation", "system/PresignedUrlConfig:responseContentDisposition", "system/PresignedUrlConfig:responseContentType", - "system/PushNotification:actions", - "system/PushNotification:badge", - "system/PushNotification:body", - "system/PushNotification:data", - "system/PushNotification:icon", - "system/PushNotification:title", "system/QueueConfig:autoScale", "system/QueueConfig:concurrency", "system/QueueConfig:deadLetterQueue", @@ -6531,10 +6513,6 @@ "system/RowLevelIsolationStrategy:database", "system/RowLevelIsolationStrategy:performance", "system/RowLevelIsolationStrategy:strategy", - "system/SMSTemplate:id", - "system/SMSTemplate:maxLength", - "system/SMSTemplate:message", - "system/SMSTemplate:variables", "system/SchemaChange:changeType", "system/SchemaChange:entityName", "system/SchemaChange:entityType", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 04c7371ef7..798a505df7 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -1,5 +1,5 @@ { - "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema \u2014 remove a key ONLY for a deliberate retirement. See #2978.", + "description": "Ratchet manifest of every JSON Schema emitted by scripts/build-schemas.ts. Auto-appended when new schemas are added (commit the change). A listed schema that a build no longer emits fails gen:schema — remove a key ONLY for a deliberate retirement. See #2978.", "schemas": [ "ai/AIModelConfig", "ai/AIUsageRecord", @@ -1256,7 +1256,6 @@ "system/EmailAndPasswordConfig", "system/EmailProvider", "system/EmailServiceConfig", - "system/EmailTemplate", "system/EmailTemplateDefinition", "system/EmailTemplateDefinitionCategory", "system/EmailTemplateDefinitionVariable", @@ -1278,7 +1277,6 @@ "system/HistogramBucketConfig", "system/HttpDestinationConfig", "system/HttpServerConfig", - "system/InAppNotification", "system/Incident", "system/IncidentCategory", "system/IncidentNotificationMatrix", @@ -1370,7 +1368,6 @@ "system/PackagePublishResult", "system/Plan", "system/PresignedUrlConfig", - "system/PushNotification", "system/QueueConfig", "system/QuotaEnforcementResult", "system/RPO", @@ -1385,7 +1382,6 @@ "system/RollbackPlan", "system/RouteHandlerMetadata", "system/RowLevelIsolationStrategy", - "system/SMSTemplate", "system/SamplingDecision", "system/SamplingStrategyType", "system/Schedule",