Skip to content

Commit b7d88fc

Browse files
committed
refactor(spec)!: retire activationEvents (both keys) and the ActivationEventSchema vocabulary (#4657, ADR-0049)
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9
1 parent ffab803 commit b7d88fc

19 files changed

Lines changed: 456 additions & 365 deletions
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
refactor(spec)!: remove `activationEvents` (both keys) and the `ActivationEventSchema` vocabulary — lazy activation that no runtime ever implemented (#4657, ADR-0049)
6+
7+
`activationEvents` promised lazy plugin activation ("plugins remain dormant
8+
until an activation event fires") on two authorable surfaces —
9+
`DynamicLoadRequest.activationEvents` (`@objectstack/spec/kernel`) and
10+
`StudioPluginManifest.activationEvents` (`@objectstack/spec/studio`, the
11+
`defineStudioPlugin` input) — and **no runtime in objectstack / cloud /
12+
cloud-v1 / objectui ever read either key** (four-repo bare-name scan in #4657,
13+
re-verified at implementation time). Every plugin has always activated
14+
immediately on load/registration; cloud-v1's own ROADMAP recorded lazy
15+
activation as ❌ unimplemented (planned v0.4.0). That is ADR-0049's
16+
declared ≠ enforced shape in the semantically-lying direction: an author
17+
writing `activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }]`
18+
expected deferral and got eager activation with a clean parse.
19+
20+
#4653 had just converged the two `ActivationEventSchema` declarations onto one
21+
structured `{ type, pattern }` form inside this same unreleased major; with the
22+
enforce-or-remove ruling landing on **remove**, that converged vocabulary
23+
retires before ever shipping. Composed across the two changes, a v16 author
24+
simply deletes the key in whichever form they carried.
25+
26+
Migration (FROM → TO):
27+
28+
- `activationEvents` in a `defineStudioPlugin` input / `StudioPluginManifest`
29+
value — v16 string form (`['*']`, `['onMetadataType:flow']`) or v17-rc
30+
structured form (`[{ type: 'onStartup', pattern: '*' }]`) alike →
31+
**delete the key**. There is no replacement value: eager activation is the
32+
only behaviour there has ever been, and `activate()` still runs at
33+
registration time. The strict manifest parse rejects the key (and its former
34+
VS Code-flavoured aliases `activation` / `events` / `onActivate`) with this
35+
prescription.
36+
- `activationEvents` in a `DynamicLoadRequest` value → **delete the key**.
37+
Tombstoned, not silently stripped — `DynamicLoadRequestSchema` is not
38+
`.strict()`, so a `retiredKey()` tombstone makes authoring it a `tsc` error
39+
and a parse error carrying the prescription.
40+
- `import { ActivationEventSchema, ActivationEvent } from '@objectstack/spec/kernel'`
41+
(or `/studio`) → **no replacement export** (TS2305 after upgrade). Nothing
42+
consumed the vocabulary; an exported schema with no consumer is read as a
43+
capability by whoever finds it (#3950), so the orphaned def goes with the
44+
keys.
45+
- Lazy activation is a **new capability**: if it is ever built it returns via
46+
the enforce route of ADR-0049 through a new ADR — executor first, vocabulary
47+
second — not by re-declaring inert keys.
48+
49+
Self-check (#4535 §5): TS2305 — yes, two removed exports on two entries;
50+
metadata migration — none possible or needed (`StudioPluginManifest` is TS
51+
configuration parsed by `defineStudioPlugin`, a root schema never stored in
52+
`sys_metadata`; `DynamicLoadRequest` is a runtime request shape with no
53+
caller — no stored row exists for a D2 conversion to rewrite, so the change is
54+
one ADR-0087 D3 semantic record, `plugin-activation-events-retired`); shape
55+
change — two keys removed, zero behaviour change (eager activation before and
56+
after, byte-identical).
57+
58+
The retirement kit: `retiredKey()` tombstone on the non-strict kernel schema;
59+
strict-parse `guidance` prescriptions on the studio manifest (including the
60+
three former aliases); ADR-0087 D3 semantic migration; baselines
61+
(`authorable-surface.json` — one `[RETIRED]` line, five lines dropped
62+
deliberately with the defs; `json-schema.manifest.json``kernel/ActivationEvent`
63+
and `studio/ActivationEvent` def removals; `api-surface.json`) regenerated
64+
deliberately; compiler-API export pin (`activation-events-retirement.test.ts`,
65+
zero holders across every public entry) — sabotage-verified.

content/docs/plugins/development.mdx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,6 @@ export const manifest = defineStudioPlugin({
384384
name: 'Flow Designer',
385385
version: '2.0.0',
386386
description: 'Visual flow builder for automation workflows',
387-
activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }],
388387

389388
contributes: {
390389
metadataViewers: [{
@@ -434,16 +433,13 @@ export const manifest = defineStudioPlugin({
434433
});
435434
```
436435

437-
### Activation Events
436+
### Activation
438437

439-
Control when your plugin loads with activation events:
440-
441-
| Pattern | Trigger |
442-
|:---|:---|
443-
| `*` | Activate immediately (eager, default) |
444-
| `onMetadataType:object` | When metadata type "object" is loaded |
445-
| `onCommand:myPlugin.doSomething` | When command is invoked |
446-
| `onView:myPlugin.myPanel` | When panel is opened |
438+
Every Studio plugin loads and activates immediately on registration — `activate()`
439+
runs at registration time, unconditionally. The former `activationEvents` manifest
440+
key was removed in v17 (#4657, ADR-0049): it declared lazy activation that no
441+
Studio host ever implemented, so a manifest that still carries it now fails the
442+
parse with the upgrade prescription. Delete the key.
447443

448444
### View Modes
449445

content/docs/references/kernel/plugin-runtime.mdx

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,12 @@ Inspired by:
1919

2020
- Kubernetes Operator pattern (reconciliation loop)
2121

22-
- VS Code Extension Host (activation events)
23-
2422
This protocol enables:
2523

2624
- Runtime load/unload of plugins without kernel restart
2725

2826
- Plugin discovery from registries and local filesystem
2927

30-
- Activation events (load plugin only when needed)
31-
3228
- Safe unload with dependency awareness
3329

3430
<Callout type="info">
@@ -38,27 +34,13 @@ This protocol enables:
3834
## TypeScript Usage
3935

4036
```typescript
41-
import { ActivationEventSchema, DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
42-
import type { ActivationEvent, DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
37+
import { DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
38+
import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
4339

4440
// Validate data
45-
const result = ActivationEventSchema.parse(data);
41+
const result = DynamicLoadRequestSchema.parse(data);
4642
```
4743

48-
---
49-
50-
## ActivationEvent
51-
52-
Lazy activation trigger for a dynamic plugin
53-
54-
### Properties
55-
56-
| Property | Type | Required | Description |
57-
| :--- | :--- | :--- | :--- |
58-
| **type** | `Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>` || Trigger type for lazy activation |
59-
| **pattern** | `string` || Match pattern for the activation trigger |
60-
61-
6244
---
6345

6446
## DynamicLoadRequest
@@ -71,7 +53,7 @@ Request to dynamically load a plugin at runtime
7153
| :--- | :--- | :--- | :--- |
7254
| **pluginId** | `string` || Unique plugin identifier |
7355
| **source** | `{ type: Enum<'npm' \| 'local' \| 'url' \| 'registry' \| 'git'>; location: string; version?: string; integrity?: string }` || Plugin source location for dynamic resolution |
74-
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately |
56+
| **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. |
7557
| **config** | `Record<string, any>` | optional | Runtime configuration overrides |
7658
| **priority** | `integer` || Loading priority (lower is higher) |
7759
| **sandbox** | `boolean` || Run in an isolated sandbox |

content/docs/references/studio/meta.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
"pages": [
44
"flow-builder",
55
"object-designer",
6-
"plugin",
7-
"plugin-runtime"
6+
"plugin"
87
]
98
}

content/docs/references/studio/plugin-runtime.mdx

Lines changed: 0 additions & 33 deletions
This file was deleted.

content/docs/references/studio/plugin.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ const result = ActionContributionSchema.parse(data);
245245
| **description** | `string` | optional | Plugin description |
246246
| **author** | `string` | optional | Author |
247247
| **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 }[]; … }` || |
248-
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` || |
249248

250249

251250
---

docs/protocol-upgrade-guide.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ The object capability block closes out the same ADR-0049 pass: `enable.trash` an
180180

181181
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.
182182

183+
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.
184+
183185
### Mechanical (applied for you)
184186

185187
| Conversion | Surface | Change | Load window |
@@ -274,6 +276,9 @@ The same enforce-or-remove pass retires the `RestServerConfig.openApi31` block (
274276
- **`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`)
275277
- 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.
276278
- 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).
279+
- **`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)
280+
- 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.
281+
- 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.
277282

278283
---
279284

0 commit comments

Comments
 (0)