Skip to content

Commit 10bd0c4

Browse files
committed
feat(spec)!: 移除 ./automation 的孤儿 EventSchema —— C6 双源清账,信封唯一真源在 ./kernel (#4658)
`EventSchema` 曾由 `./automation` 与 `./kernel` 各自导出一个声明,且两侧键集完全 不相交:automation 侧是 XState 式信号声明 `{ type, schema? }`,kernel 侧是事件总线 信封 `{ id?, name, payload, metadata }`。收敛等于在合同里写假话(把信号声明说成信 封),按维护者裁决(#4658,路线 A)删除 automation 侧孤儿:`StateMachineSchema` 从 不引用它(状态机事件类型是 `on:` 的记录键),objectstack / cloud / objectui 三仓 import 语句级零消费者,且以 #4650 门禁同款真 Zod 图 BFS 实测其从 24 个元数据根不可达 (同一次 BFS 中 `StateMachineSchema` 本身可达 —— 删除是外科式的)。 - state-machine.zod.ts:删除孤儿声明,留 #4658 指路注释 - dual-source-exports.baseline.json:删 `EventSchema` 行(13 → 12) - authorable-surface.json:`automation/Event:type` / `:schema` 随整 def 出账 (#4650 门禁判定:def no longer emitted,输出见 PR) - json-schema.manifest.json:删 `automation/Event` 键(#2978 蓄意移除通道) - api-surface.json:`./automation` 仅一行删除;docs-import-surface.baseline.json 经生成器收缩一行;references/automation/events-core.mdx 随生成器消失 - 回归 pin(state-machine.test.ts):compiler-API 断言 `./automation` 无 `EventSchema`、`./kernel` 为全包唯一声明(package.json exports 全入口枚举), 防空转守卫;S1(重加导出)/ S2(禁止的 re-export 收敛路线)sabotage 均红 - 严格性台账:automation 87 → 86 sites,state-machine 行 7 → 6(实跑判定) - changeset:@objectstack/spec major(移除已发布导出名,TS2305),零元数据迁移 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9
1 parent 072806a commit 10bd0c4

11 files changed

Lines changed: 203 additions & 47 deletions
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
BREAKING(spec): `@objectstack/spec/automation` 不再导出 `EventSchema` —— 该名字在本包内曾指向**两个键集完全不相交的声明**,automation 侧是孤儿,已删除;`EventSchema` 现在全包唯一地指 `@objectstack/spec/kernel` 的事件总线信封 (#4658, #4535 C6)
6+
7+
`EventSchema` 过去被两个入口导出,但**不是同一个声明**,拿到哪个只取决于 import 路径 —— #4411 陷阱。且两侧不是宽窄之差,是**两个概念**,键集零重叠:
8+
9+
| 入口 | 声明位置 | 形状 | 概念 |
10+
|:--|:--|:--|:--|
11+
| `@objectstack/spec/automation`(**本次删除**) | `automation/state-machine.zod.ts` | `{ type, schema? }` | XState 式**信号声明**(「这台状态机接受哪些事件」) |
12+
| `@objectstack/spec/kernel`(**不变,唯一真源**) | `kernel/events/core.zod.ts` | `{ id?, name, payload, metadata }` | 事件总线**信封**(一条已发出的事件实例) |
13+
14+
automation 侧是**孤儿声明**:`StateMachineSchema` 从不引用它(状态机的事件类型是 `on:`**记录键**,纯字符串),`packages/spec` 内外零消费者(objectstack / cloud / objectui 三仓 import 语句级实测均为零)。收敛会把「状态机信号声明」写成「事件总线信封」—— 在合同里写假话 —— 故按维护者裁决(#4658,路线 A)删除孤儿而非收敛。
15+
16+
## FROM → TO
17+
18+
```ts
19+
// FROM —— 编译期起将以 TS2305 失败(实测三仓零命中,预期无人受影响)
20+
import { EventSchema } from '@objectstack/spec/automation';
21+
```
22+
23+
- 若你想要的是**事件信封**(校验一条已发出的事件):
24+
25+
```ts
26+
// TO
27+
import { EventSchema } from '@objectstack/spec/kernel';
28+
```
29+
30+
- 若你想要的是**事件类型的声明**(名字、版本、payload 的 JSON Schema):
31+
32+
```ts
33+
// TO —— kernel 侧本来就有的「事件定义」概念
34+
import { EventTypeDefinitionSchema } from '@objectstack/spec/kernel';
35+
```
36+
37+
- 若你想给**状态机**声明它接受的事件:该表面从来不存在 —— 事件类型写在状态节点 `on:` 的记录键上(`on: { APPROVE: 'approved' }`),被删的 schema 从未接入 `StateMachineSchema`,没有替代物也不需要替代物。
38+
39+
## 定级理由(逐条自证,未照抄前例)
40+
41+
**major**,因为这是一次**已发布导出名的移除**:外部 `import { EventSchema } from '@objectstack/spec/automation'` 会以 TS2305 编译失败(与 C14 同形)。
42+
43+
同时它是**零元数据迁移**:
44+
45+
- `automation/Event` def 从 `BUILTIN_METADATA_TYPE_SCHEMAS` 元数据根(24 型)**不可达** —— 以 #4650 门禁同款真 Zod 图 BFS 对合并基线实测复核(输出见 PR;同一次 BFS 里 `StateMachineSchema` 本身可达,证明删除是外科式的)。没有任何元数据文档曾被它解析,`authorable-surface.json` 里对应两行(`automation/Event:type` / `automation/Event:schema`)是过度收集的产物,随整 def 出账(#4650 门禁在本 PR 打印的判定是「def no longer emitted」自证路径),**无 tombstone、无 ADR-0087 conversion / migration**
46+
- 已存 `sys_metadata` 数据、运行时校验行为全部不受影响;`kernel/events/core.zod.ts``EventSchema` 一字未动。
47+
- JSON Schema 产物中 `automation/Event` 停止发布(`json-schema.manifest.json` 同步删键,#2978 蓄意移除通道)。
48+
49+
## 基线 13 → 12
50+
51+
`dual-source-exports.baseline.json` 删掉 `EventSchema — [./automation (const)] ≠ [./kernel (const)]` 一行,其余 12 行一字未动。

content/docs/references/automation/events-core.mdx

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

content/docs/references/automation/meta.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"approval",
2020
"---More---",
2121
"builtin-node-config",
22-
"events-core",
2322
"flow-function",
2423
"io-node-config",
2524
"retry-policy",

docs/audits/2026-07-unknown-key-strictness-ledger.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,14 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts).
477477
| `validation.zod.ts` | 6 | authorable | **strict as of #4001 batch 3b** — a `z.lazy()` discriminated union, so the one-call conversion does not apply: each of the six variants builds its own `strictObject` from a shared `BASE_VALIDATION_SHAPE`. Closing the base alone would have rejected correctly but suggested from the SHARED keys only, so a typo of a variant's own key (`transtions``transitions`) would get no rename. Site count 1 → 6 because the six variants are now object sites in their own right. The ADR-0010 envelope lives in the shared shape, so all six inherit it |
478478
| `field-value.zod.ts` / `seed.zod.ts` | 1+1 | mixed (p) | `seed` is strict (registered-types batch) |
479479

480-
### `automation/`87 sites
480+
### `automation/`86 sites
481481

482482
| File | Sites | Class | Note |
483483
|---|---|---|---|
484484
| `flow.zod.ts` | 11 | authorable | **strict as of #4001** (4 schemas; `FlowVersionHistorySchema` is runtime — stays tolerant) |
485485
| `sync.zod.ts` / `etl.zod.ts` | 12+10 | authorable (p) | authored pipelines — **candidates** |
486486
| `execution.zod.ts` | 13 | wire | run-state envelopes — never strict. +5 at #4354 (the run-summary family: step metrics / skip reason / per-node / per-gate / the summary itself) — engine-emitted telemetry read by the Console and by operator queries, nobody authors them, so the `wire` verdict covers them unchanged |
487-
| `state-machine.zod.ts` | 7 | authorable (p) | |
487+
| `state-machine.zod.ts` | 6 | authorable (p) | **−1 at #4658**: the orphan `EventSchema` (`{ type, schema }`, an XState-style signal declaration nothing referenced — `StateMachineSchema` names event types as `on:` record keys) was deleted rather than converged with `kernel/events/core.zod.ts`'s envelope `EventSchema`, whose key set it did not intersect (#4535 C6). The remaining 6 sites and their verdict are unchanged |
488488
| `control-flow.zod.ts` | 5 | authorable (p) | validated structurally by `validateControlFlow`. **−1 at #4661**: `RetryPolicySchema` moved out to `shared/retry-policy.zod.ts``./automation` and `./system` published the same name for two different declarations (#4411), so the retry policy converged onto one. The site still exists and is still non-strict and authorable; it is simply no longer in a directory this ledger sections. ⚠️ That is a coverage gap worth knowing about: this audit sections `ui/` / `data/` / `automation/` / `security/` / `studio/` only, so a `shared/` shape is unaudited by construction. The tolerance is deliberate here — the `retryDelayMs``backoffMs` rename is tombstoned via `retiredKey()` precisely because a non-strict parent would otherwise swallow the old spelling |
489489
| `bpmn-interop.zod.ts` | 5 | wire (p) | interop import shapes |
490490
| `approval.zod.ts` | 4 | authorable | **strict as of #4001 step 3** — all four authoring schemas (node config / approver / escalation / decision-output). The published JSON schema carries `additionalProperties: false` into the Studio form AND `registerFlow()` config validation (#4027/#4040), so an unknown key in an approval node's `config` is rejected at registration too — verified: `z.toJSONSchema` on the strict lazySchema does not throw (#3746 hazard checked) |

packages/spec/api-surface.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2166,7 +2166,6 @@
21662166
"ETLTransformationSchema (const)",
21672167
"ETLTransformationType (type)",
21682168
"ETLTransformationTypeSchema (const)",
2169-
"EventSchema (const)",
21702169
"ExecutionError (type)",
21712170
"ExecutionErrorParsed (type)",
21722171
"ExecutionErrorSchema (const)",

packages/spec/authorable-surface.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2271,8 +2271,6 @@
22712271
"automation/ETLTransformation:continueOnError",
22722272
"automation/ETLTransformation:name",
22732273
"automation/ETLTransformation:type",
2274-
"automation/Event:schema",
2275-
"automation/Event:type",
22762274
"automation/ExecutionError:code",
22772275
"automation/ExecutionError:context",
22782276
"automation/ExecutionError:executionId",

packages/spec/docs-import-surface.baseline.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
"api/MetadataValidateRequest — no type export",
2020
"api/SubscriptionEvent — no type export",
2121
"automation/ApproverType — no type export",
22-
"automation/Event — no type export",
2322
"automation/FlowNodeAction — no type export",
2423
"automation/FlowVariable — no type export",
2524
"automation/GuardRef — no type export",

packages/spec/dual-source-exports.baseline.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
"EnvironmentArtifact — [./cloud (type)] ≠ [./system (type)]",
1010
"EnvironmentArtifactInput — [./cloud (type)] ≠ [./system (type)]",
1111
"EnvironmentArtifactSchema — [./cloud (const)] ≠ [./system (const)]",
12-
"EventSchema — [./automation (const)] ≠ [./kernel (const)]",
1312
"PackageDependency — [./cloud (type)] ≠ [./kernel (type)]",
1413
"PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]",
1514
"TenantPlan — [./cloud (type)] ≠ [./system (type)]",

packages/spec/json-schema.manifest.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -532,7 +532,6 @@
532532
"automation/ETLSyncMode",
533533
"automation/ETLTransformation",
534534
"automation/ETLTransformationType",
535-
"automation/Event",
536535
"automation/ExecutionError",
537536
"automation/ExecutionErrorSeverity",
538537
"automation/ExecutionLog",

packages/spec/src/automation/state-machine.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,3 +122,146 @@ describe('StateMachineSchema', () => {
122122
expect(() => StateMachineSchema.parse(machine)).toThrow();
123123
});
124124
});
125+
126+
// ─── [#4658] `EventSchema` is gone from ./automation — dual-source C6 ────────
127+
//
128+
// `./automation` and `./kernel` both exported an `EventSchema`, for two
129+
// declarations whose key sets did not even intersect:
130+
//
131+
// automation/state-machine.zod.ts (removed) → `{ type, schema }` — an
132+
// XState-style signal DECLARATION ("which events does this machine
133+
// accept"). An orphan: `StateMachineSchema` names event types as the
134+
// record keys of `on:`, and no repo imported it (objectstack / cloud /
135+
// objectui, import-statement-level scan).
136+
// kernel/events/core.zod.ts → `{ id?, name, payload, metadata }` — an
137+
// event-bus ENVELOPE (an emitted event instance).
138+
//
139+
// Converging them would have declared a signal definition to be an envelope —
140+
// a false statement in the contract — so the orphan was deleted instead
141+
// (maintainer ruling on #4658; ledger #4535 C6). The kernel-side analogue of a
142+
// signal *declaration* already exists: `EventTypeDefinitionSchema`, same file.
143+
//
144+
// #4642 established that a compile-time conditional-type pin in this package
145+
// is a no-op (tsconfig excludes `**/*.test.ts`; vitest never enables
146+
// `typecheck`), so the load-bearing pin is the compiler-API test below, with
147+
// anti-vacuity guards; sabotage-verified in the PR (re-adding the export
148+
// turns it red).
149+
describe('[#4658] `EventSchema` is not exported from ./automation', () => {
150+
it('resolves the export surface: no entry but ./kernel declares `EventSchema`', async () => {
151+
const ts = (await import('typescript')).default;
152+
const { resolve, relative, dirname } = await import('node:path');
153+
const { fileURLToPath } = await import('node:url');
154+
const { readFileSync } = await import('node:fs');
155+
156+
const specDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
157+
// Every public entry point, read from package.json's exports map so a
158+
// future entry cannot silently escape the uniqueness pin below.
159+
const pkg = JSON.parse(readFileSync(resolve(specDir, 'package.json'), 'utf8')) as {
160+
exports: Record<string, unknown>;
161+
};
162+
const entries: Record<string, string> = {};
163+
for (const sub of Object.keys(pkg.exports)) {
164+
if (sub === '.') entries[sub] = resolve(specDir, 'src/index.ts');
165+
else if (/^\.\/[a-z-]+$/.test(sub)) entries[sub] = resolve(specDir, `src/${sub.slice(2)}/index.ts`);
166+
// './openapi.json' / './package.json' are not TypeScript entry points.
167+
}
168+
// Anti-vacuity: the enumeration must have found the real surface.
169+
expect(Object.keys(entries)).toContain('./automation');
170+
expect(Object.keys(entries)).toContain('./kernel');
171+
expect(Object.keys(entries).length).toBeGreaterThan(10);
172+
173+
const program = ts.createProgram(Object.values(entries), {
174+
module: ts.ModuleKind.ESNext,
175+
moduleResolution: ts.ModuleResolutionKind.Bundler,
176+
skipLibCheck: true,
177+
noEmit: true,
178+
});
179+
const checker = program.getTypeChecker();
180+
const unalias = (s: import('typescript').Symbol) =>
181+
s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s;
182+
183+
const exportsOf = (sub: string) => {
184+
const sf = program.getSourceFile(entries[sub]);
185+
const moduleSym = sf && checker.getSymbolAtLocation(sf);
186+
// Without this guard a resolution failure would make every assertion
187+
// below pass vacuously — the exact way a gate goes dormant (#4642).
188+
expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy();
189+
return checker.getExportsOfModule(moduleSym!);
190+
};
191+
192+
const originOf = (sym: import('typescript').Symbol, label: string) => {
193+
const decl = unalias(sym).declarations?.[0];
194+
expect(decl, `${label} must have a declaration`).toBeTruthy();
195+
const declFile = decl!.getSourceFile();
196+
return `${relative(specDir, declFile.fileName)}:${
197+
declFile.getLineAndCharacterOfPosition(decl!.getStart()).line + 1
198+
}`;
199+
};
200+
201+
// 1. The removed side: `./automation` still has a non-trivial surface —
202+
// so the `not.toContain` cannot pass by resolving nothing — and no
203+
// longer names `EventSchema`, while its surviving neighbours stand.
204+
const automationExports = exportsOf('./automation');
205+
expect(automationExports.length, './automation must export a non-trivial surface').toBeGreaterThan(50);
206+
const automationNames = automationExports.map((e) => e.getName());
207+
expect(automationNames).not.toContain('EventSchema');
208+
expect(automationNames).toContain('StateMachineSchema');
209+
expect(automationNames).toContain('TransitionSchema');
210+
211+
// 2. The surviving side: `./kernel` still exports the envelope const and
212+
// its inferred type, declared in kernel/events/core.zod.ts.
213+
const kernelExports = exportsOf('./kernel');
214+
const kernelEventSchema = kernelExports.find((e) => e.getName() === 'EventSchema');
215+
expect(kernelEventSchema, './kernel must export `EventSchema`').toBeTruthy();
216+
const kernelOrigin = originOf(kernelEventSchema!, './kernel EventSchema');
217+
expect(kernelOrigin).toMatch(/^src\/kernel\/events\/core\.zod\.ts:\d+$/);
218+
expect(kernelExports.map((e) => e.getName())).toContain('Event');
219+
220+
// 3. Uniqueness — the dual-source pin proper: across EVERY public entry,
221+
// an export named `EventSchema` must resolve to that ONE declaration.
222+
const holders: string[] = [];
223+
for (const sub of Object.keys(entries)) {
224+
for (const sym of exportsOf(sub).filter((e) => e.getName() === 'EventSchema')) {
225+
holders.push(sub);
226+
expect(
227+
originOf(sym, `${sub} EventSchema`),
228+
`${sub} must resolve \`EventSchema\` to the kernel declaration`,
229+
).toBe(kernelOrigin);
230+
}
231+
}
232+
expect(holders).toContain('./kernel');
233+
expect(holders).not.toContain('./automation');
234+
});
235+
236+
it('keeps the runtime namespaces consistent with the compiler view', async () => {
237+
const automation = await import('./index');
238+
const kernel = await import('../kernel/index');
239+
expect('EventSchema' in automation).toBe(false);
240+
expect('EventSchema' in kernel).toBe(true);
241+
242+
// What the name now unambiguously means: an emitted event INSTANCE.
243+
expect(() =>
244+
kernel.EventSchema.parse({
245+
name: 'user.created',
246+
payload: { id: 'u1' },
247+
metadata: { source: 'test', timestamp: '2026-08-03T00:00:00.000Z' },
248+
}),
249+
).not.toThrow();
250+
251+
// The removed side's shape — a signal DECLARATION — is not what the
252+
// surviving schema accepts: the two concepts were never converged.
253+
expect(() => kernel.EventSchema.parse({ type: 'APPROVE' })).toThrow();
254+
});
255+
256+
it('still authors state-machine event types as `on:` record keys — the surface the orphan never was', () => {
257+
const machine = {
258+
id: 'c6_pin',
259+
initial: 'draft',
260+
states: {
261+
draft: { on: { APPROVE: 'approved' } },
262+
approved: { type: 'final' },
263+
},
264+
};
265+
expect(() => StateMachineSchema.parse(machine)).not.toThrow();
266+
});
267+
});

0 commit comments

Comments
 (0)