Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/retire-activation-events.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 6 additions & 10 deletions content/docs/plugins/development.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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: [{
Expand Down Expand Up @@ -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

Expand Down
26 changes: 4 additions & 22 deletions content/docs/references/kernel/plugin-runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<Callout type="info">
Expand All @@ -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
Expand All @@ -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<string, any>` | optional | Runtime configuration overrides |
| **priority** | `integer` | ✅ | Loading priority (lower is higher) |
| **sandbox** | `boolean` | ✅ | Run in an isolated sandbox |
Expand Down
3 changes: 1 addition & 2 deletions content/docs/references/studio/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"pages": [
"flow-builder",
"object-designer",
"plugin",
"plugin-runtime"
"plugin"
]
}
33 changes: 0 additions & 33 deletions content/docs/references/studio/plugin-runtime.mdx

This file was deleted.

1 change: 0 additions & 1 deletion content/docs/references/studio/plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }[]` | ✅ | |


---
Expand Down
5 changes: 5 additions & 0 deletions docs/protocol-upgrade-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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.

---

Expand Down
Loading
Loading