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
77 changes: 77 additions & 0 deletions .changeset/converge-activation-event-schema.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
---
"@objectstack/spec": major
---

feat(spec)!: 双源 C5 收敛 — `ActivationEventSchema` 归 `./kernel` 结构化形状,`./studio` re-export (#4653)

`ActivationEventSchema` 这个名字过去在两个入口解析到**两份不同的声明**,插件作者拿到哪套校验取决于他从哪个子路径 import(#4411 陷阱):

| 入口 | 声明 | 作者写的样子 |
|:--|:--|:--|
| `@objectstack/spec/kernel` | `z.object({ type: z.enum([...]), pattern: z.string() })` | `{ type: 'onCommand', pattern: 'my.cmd' }` |
| `@objectstack/spec/studio` | `z.string()` | `'onCommand:my.cmd'` |

两侧都在作者面上(kernel 侧嵌在 `DynamicLoadRequest.activationEvents`,studio 侧嵌在 `StudioPluginManifest.activationEvents`,后者正是 `defineStudioPlugin` 的入参),所以没有"死侧"可删。v17 统一到**结构化形状**:`./studio` 现在 re-export `./kernel` 的那一份声明,平台只剩一套激活词表。

**为什么是结构化的那一侧赢。** 字符串那一侧更眼熟(照搬 VS Code),但它什么都不校验:`z.string()` 接受 `''`、`'banana'`,以及真正要命的 `'onMetadatType:flow'` —— 这个文件文档里列的词表(`*`、`onMetadataType:`、`onCommand:`、`onView:`)只活在散文里,拼错永远静默通过。结构化形状用 enum 在**创作时**就把触发器类型钉死,这才是声明它的意义。

## FROM → TO

`activationEvents` 的每一项从字符串变成对象。冒号前的段成为 `type`,冒号后的段成为 `pattern`:

```ts
// FROM (v16 及以前,@objectstack/spec/studio)
defineStudioPlugin({
id: 'objectstack.flow-designer',
name: 'Flow Designer',
activationEvents: ['onMetadataType:flow'],
});

// TO (v17+)
defineStudioPlugin({
id: 'objectstack.flow-designer',
name: 'Flow Designer',
activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }],
});
```

逐条对照:

| FROM | TO |
|:--|:--|
| `'*'` | `{ type: 'onStartup', pattern: '*' }` |
| `'onMetadataType:flow'` | `{ type: 'onMetadataType', pattern: 'flow' }` |
| `'onCommand:myPlugin.doSomething'` | `{ type: 'onCommand', pattern: 'myPlugin.doSomething' }` |
| `'onView:myPlugin.myPanel'` | `{ type: 'onView', pattern: 'myPlugin.myPanel' }` |

`StudioPluginManifest.activationEvents` 的默认值随之从 `['*']` 变为 `[{ type: 'onStartup', pattern: '*' }]`。`'*'` 没有拿到独立的 `type`:它一直就是"立即激活",而 kernel 侧的 `onStartup` 本来就是这个意思,再加一个枚举值只会造出两个同义词。

## 词表 = 两侧并集,没有能力被静默拿掉

enum 取**两侧 v17 前词表的并集**,共 9 个值:

| 值 | 来源 |
|:--|:--|
| `onCommand` | kernel enum + studio 文档 `onCommand:myPlugin.doSomething` |
| `onRoute` | kernel enum |
| `onObject` | kernel enum |
| `onEvent` | kernel enum |
| `onService` | kernel enum |
| `onSchedule` | kernel enum |
| `onStartup` | kernel enum;同时是 studio `'*'` 的落点 |
| `onMetadataType` | studio 文档/测试 `onMetadataType:object` —— kernel 原本没有 |
| `onView` | studio 文档/测试 `onView:myPlugin.myPanel` —— kernel 原本没有 |

**未采纳**:cloud-v1 未发布的 marketplace runtime 里的 `priority`、`onInstall`、`onWebhook`。四仓无人读它们,而新增一个 declared-but-unenforced 的键正是 ADR-0049 在清的债 —— 等真有执行点再单独提。

## 迁移是手工的,但失败是响亮的

**没有随附 ADR-0087 conversion,因为写不出能跑到的那一个。** conversion 层(`applyConversions`)接在 `normalizeStackInput` 上,只走 stack 树;而 `StudioPluginManifestSchema` 和 `DynamicLoadRequestSchema` 都是**根 schema**,没有任何父 schema 嵌入它们(前者由 `defineStudioPlugin` 直接 parse,后者是运行时请求载荷),都不在 stack 里。伪造一个永远不会命中的 conversion 只会制造"已自动迁移"的假象。

手工迁移步骤:按上表把每个字符串改写成 `{ type, pattern }`。**漏改会在 parse 处响亮失败** —— `StudioPluginManifestSchema` 是 `strictObject`,字符串遇到对象 schema 直接抛错,不存在静默吞掉或强制转换。

## 其它影响

- `@objectstack/spec/studio` 现在**额外导出** `ActivationEvent` 类型(此前只有 schema),与 `./kernel` 指向同一份声明。
- `ActivationEventSchema` 从 `dual-source-exports.baseline.json` 移除,基线 22 → 21。
- 零可作者化 key 消失、零 tombstone:kernel 的 `ActivationEvent:type` / `:pattern` 原样存活,`studio/ActivationEvent` 侧新增 2 个 key(字符串没有 key,对象有),属 `gen:schema` 允许的**新增**。
2 changes: 1 addition & 1 deletion content/docs/plugins/development.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -384,7 +384,7 @@ export const manifest = defineStudioPlugin({
name: 'Flow Designer',
version: '2.0.0',
description: 'Visual flow builder for automation workflows',
activationEvents: ['onMetadataType:flow'],
activationEvents: [{ type: 'onMetadataType', pattern: 'flow' }],

contributes: {
metadataViewers: [{
Expand Down
22 changes: 18 additions & 4 deletions content/docs/references/kernel/plugin-runtime.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,13 +38,27 @@ This protocol enables:
## TypeScript Usage

```typescript
import { DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
import type { DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';
import { ActivationEventSchema, DynamicLoadRequestSchema, DynamicPluginOperationSchema, DynamicPluginResultSchema, DynamicUnloadRequestSchema, PluginSourceSchema } from '@objectstack/spec/kernel';
import type { ActivationEvent, DynamicLoadRequest, DynamicPluginOperation, DynamicPluginResult, DynamicUnloadRequest, PluginSource } from '@objectstack/spec/kernel';

// Validate data
const result = DynamicLoadRequestSchema.parse(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 |


---

## DynamicLoadRequest
Expand All@@ -57,7 +71,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'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately |
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | optional | Lazy activation triggers; if omitted plugin starts immediately |
| **config** | `Record<string, any>` | optional | Runtime configuration overrides |
| **priority** | `integer` | ✅ | Loading priority (lower is higher) |
| **sandbox** | `boolean` | ✅ | Run in an isolated sandbox |
Expand Down
19 changes: 2 additions & 17 deletions content/docs/references/kernel/plugin.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,27 +16,12 @@ These are the specialized plugin types common between Manifest (Package) and Plu
## TypeScript Usage

```typescript
import { ActivationEventSchema, PluginSchema } from '@objectstack/spec/kernel';
import type { ActivationEvent } from '@objectstack/spec/kernel';
import { PluginSchema } from '@objectstack/spec/kernel';

// Validate data
const result = ActivationEventSchema.parse(data);
const result = PluginSchema.parse(data);
```

---

## ActivationEvent

Lazy activation trigger for a dynamic plugin

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup'>` | ✅ | Trigger type for lazy activation |
| **pattern** | `string` | ✅ | Match pattern for the activation trigger |


---

## Plugin
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/studio/meta.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
"action",
"flow-builder",
"object-designer",
"plugin"
"plugin",
"plugin-runtime"
]
}
33 changes: 33 additions & 0 deletions content/docs/references/studio/plugin-runtime.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
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 |


---

7 changes: 2 additions & 5 deletions content/docs/references/studio/plugin.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -100,7 +100,7 @@ modes: ['preview', 'design', 'data'],
## TypeScript Usage

```typescript
import { ActionContributionSchema, ActivationEventSchema, CommandContributionSchema, MetadataIconContributionSchema, MetadataViewerContributionSchema, PanelContributionSchema, PanelLocationSchema, SidebarGroupContributionSchema, StudioPluginContributionsSchema, StudioPluginManifestSchema, ViewModeSchema } from '@objectstack/spec/studio';
import { ActionContributionSchema, CommandContributionSchema, MetadataIconContributionSchema, MetadataViewerContributionSchema, PanelContributionSchema, PanelLocationSchema, SidebarGroupContributionSchema, StudioPluginContributionsSchema, StudioPluginManifestSchema, ViewModeSchema } from '@objectstack/spec/studio';
import type { ActionContribution, CommandContribution, MetadataIconContribution, MetadataViewerContribution, PanelContribution, SidebarGroupContribution, StudioPluginContributions, StudioPluginManifest, ViewMode } from '@objectstack/spec/studio';

// Validate data
Expand All@@ -122,9 +122,6 @@ const result = ActionContributionSchema.parse(data);
| **metadataTypes** | `string[]` | ✅ | Applicable metadata types |


---


---

## CommandContribution
Expand DownExpand Up@@ -237,7 +234,7 @@ 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** | `string[]` | ✅ | |
| **activationEvents** | `{ type: Enum<'onCommand' \| 'onRoute' \| 'onObject' \| 'onEvent' \| 'onService' \| 'onSchedule' \| 'onStartup' \| 'onMetadataType' \| 'onView'>; pattern: string }[]` | ✅ | |


---
Expand Down
2 changes: 1 addition & 1 deletion packages/spec/PLUGIN_STANDARDS.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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: `onCommand`, `onRoute`, `onObject`, `onEvent`, `onService`, `onSchedule`, `onStartup`
- **`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)
- **`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

Expand Down
1 change: 1 addition & 0 deletions packages/spec/api-surface.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -4045,6 +4045,7 @@
"ActionContribution (type)",
"ActionContributionSchema (const)",
"ActionLocationSchema (const)",
"ActivationEvent (type)",
"ActivationEventSchema (const)",
"BUILT_IN_NODE_DESCRIPTORS (const)",
"CommandContribution (type)",
Expand Down
4 changes: 3 additions & 1 deletion packages/spec/authorable-surface.json
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
{
"description": "Ratchet of every AUTHORABLE key in the spec \u2014 what a metadata author may write, which for this platform IS the third-party API. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 \u00a75.",
"description": "Ratchet of every AUTHORABLE key in the spec what a metadata author may write, which for this platform IS the third-party API. Auto-updated on additions (commit the change). A key that disappears without a tombstone fails gen:schema, because these schemas are not .strict() and Zod would silently strip it. \"[RETIRED]\" marks a tombstoned key that still rejects with an upgrade prescription. See #3855, ADR-0059 §5.",
"keys": [
"ai/AIModelConfig:maxTokens",
"ai/AIModelConfig:model",
Expand DownExpand Up@@ -5422,6 +5422,8 @@
"studio/ActionContribution:label",
"studio/ActionContribution:location",
"studio/ActionContribution:metadataTypes",
"studio/ActivationEvent:pattern",
"studio/ActivationEvent:type",
"studio/CommandContribution:icon",
"studio/CommandContribution:id",
"studio/CommandContribution:label",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/docs-import-surface.baseline.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,7 +88,6 @@
"shared/MutationEventEnum — no type export",
"shared/SortDirectionEnum — no type export",
"studio/ActionLocation — no type export",
"studio/ActivationEvent — no type export",
"studio/PanelLocation — no type export",
"system/AddFieldOperation — no type export",
"system/CreateObjectOperation — no type export",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/dual-source-exports.baseline.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,7 +2,6 @@
"_comment": "Accepted cross-entry DUAL-SOURCE exports of @objectstack/spec (#4446): names that two or more public entry points export for DIFFERENT declarations, so which type a consumer gets depends on the import path — the #4411 trap. Shrink-only ratchet, judged by symbol identity (a re-export of one declaration from many entries is fine and not listed). A NEW name here fails check:dual-source-exports: converge on one declaration and re-export it, or rename one side — growing this list needs maintainer sign-off and shows up as this file in the diff. An entry that stops being dual-source fails until its line is deleted. Regenerate with: tsx scripts/check-dual-source-exports.ts --update (after pnpm build).",
"entries": [
"ActionLocationSchema — [./studio (const)] ≠ [./ui (const)]",
"ActivationEventSchema — [./kernel (const)] ≠ [./studio (const)]",
"ConflictResolution — [./automation (type)] ≠ [./integration (type)] ≠ [./ui (type)]",
"ConflictResolutionSchema — [./automation (const)] ≠ [./integration (const)] ≠ [./ui (const)]",
"DataSyncConfig — [./automation (type)] ≠ [./integration (type)]",
Expand Down
20 changes: 20 additions & 0 deletions packages/spec/src/kernel/plugin-runtime.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -88,12 +88,32 @@ describe('Plugin Runtime Management Protocol', () => {
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();
}
});
});

describe('DynamicLoadRequestSchema', () => {
Expand Down
Loading
Loading