From 10bd0c4e421e5ad0bcccbf185ed107eb80ca4323 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 01:24:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(spec)!:=20=E7=A7=BB=E9=99=A4=20`./automati?= =?UTF-8?q?on`=20=E7=9A=84=E5=AD=A4=E5=84=BF=20`EventSchema`=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20C6=20=E5=8F=8C=E6=BA=90=E6=B8=85=E8=B4=A6,=E4=BF=A1?= =?UTF-8?q?=E5=B0=81=E5=94=AF=E4=B8=80=E7=9C=9F=E6=BA=90=E5=9C=A8=20`./ker?= =?UTF-8?q?nel`=20(#4658)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 Claude-Session: https://claude.ai/code/session_0176qgxgCXTJCUv4YFLtusP9 --- .changeset/event-schema-dual-source-c6.md | 51 +++++++ .../references/automation/events-core.mdx | 30 ---- content/docs/references/automation/meta.json | 1 - .../2026-07-unknown-key-strictness-ledger.md | 4 +- packages/spec/api-surface.json | 1 - packages/spec/authorable-surface.json | 2 - .../spec/docs-import-surface.baseline.json | 1 - .../spec/dual-source-exports.baseline.json | 1 - packages/spec/json-schema.manifest.json | 1 - .../spec/src/automation/state-machine.test.ts | 143 ++++++++++++++++++ .../spec/src/automation/state-machine.zod.ts | 15 +- 11 files changed, 203 insertions(+), 47 deletions(-) create mode 100644 .changeset/event-schema-dual-source-c6.md delete mode 100644 content/docs/references/automation/events-core.mdx diff --git a/.changeset/event-schema-dual-source-c6.md b/.changeset/event-schema-dual-source-c6.md new file mode 100644 index 0000000000..82b067abc2 --- /dev/null +++ b/.changeset/event-schema-dual-source-c6.md @@ -0,0 +1,51 @@ +--- +"@objectstack/spec": major +--- + +BREAKING(spec): `@objectstack/spec/automation` 不再导出 `EventSchema` —— 该名字在本包内曾指向**两个键集完全不相交的声明**,automation 侧是孤儿,已删除;`EventSchema` 现在全包唯一地指 `@objectstack/spec/kernel` 的事件总线信封 (#4658, #4535 C6) + +`EventSchema` 过去被两个入口导出,但**不是同一个声明**,拿到哪个只取决于 import 路径 —— #4411 陷阱。且两侧不是宽窄之差,是**两个概念**,键集零重叠: + +| 入口 | 声明位置 | 形状 | 概念 | +|:--|:--|:--|:--| +| `@objectstack/spec/automation`(**本次删除**) | `automation/state-machine.zod.ts` | `{ type, schema? }` | XState 式**信号声明**(「这台状态机接受哪些事件」) | +| `@objectstack/spec/kernel`(**不变,唯一真源**) | `kernel/events/core.zod.ts` | `{ id?, name, payload, metadata }` | 事件总线**信封**(一条已发出的事件实例) | + +automation 侧是**孤儿声明**:`StateMachineSchema` 从不引用它(状态机的事件类型是 `on:` 的**记录键**,纯字符串),`packages/spec` 内外零消费者(objectstack / cloud / objectui 三仓 import 语句级实测均为零)。收敛会把「状态机信号声明」写成「事件总线信封」—— 在合同里写假话 —— 故按维护者裁决(#4658,路线 A)删除孤儿而非收敛。 + +## FROM → TO + +```ts +// FROM —— 编译期起将以 TS2305 失败(实测三仓零命中,预期无人受影响) +import { EventSchema } from '@objectstack/spec/automation'; +``` + +- 若你想要的是**事件信封**(校验一条已发出的事件): + + ```ts + // TO + import { EventSchema } from '@objectstack/spec/kernel'; + ``` + +- 若你想要的是**事件类型的声明**(名字、版本、payload 的 JSON Schema): + + ```ts + // TO —— kernel 侧本来就有的「事件定义」概念 + import { EventTypeDefinitionSchema } from '@objectstack/spec/kernel'; + ``` + +- 若你想给**状态机**声明它接受的事件:该表面从来不存在 —— 事件类型写在状态节点 `on:` 的记录键上(`on: { APPROVE: 'approved' }`),被删的 schema 从未接入 `StateMachineSchema`,没有替代物也不需要替代物。 + +## 定级理由(逐条自证,未照抄前例) + +定 **major**,因为这是一次**已发布导出名的移除**:外部 `import { EventSchema } from '@objectstack/spec/automation'` 会以 TS2305 编译失败(与 C14 同形)。 + +同时它是**零元数据迁移**: + +- `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**。 +- 已存 `sys_metadata` 数据、运行时校验行为全部不受影响;`kernel/events/core.zod.ts` 的 `EventSchema` 一字未动。 +- JSON Schema 产物中 `automation/Event` 停止发布(`json-schema.manifest.json` 同步删键,#2978 蓄意移除通道)。 + +## 基线 13 → 12 + +`dual-source-exports.baseline.json` 删掉 `EventSchema — [./automation (const)] ≠ [./kernel (const)]` 一行,其余 12 行一字未动。 diff --git a/content/docs/references/automation/events-core.mdx b/content/docs/references/automation/events-core.mdx deleted file mode 100644 index f21af721c7..0000000000 --- a/content/docs/references/automation/events-core.mdx +++ /dev/null @@ -1,30 +0,0 @@ ---- -title: Events Core -description: Events Core 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 { EventSchema } from '@objectstack/spec/automation'; - -// Validate data -const result = EventSchema.parse(data); -``` - ---- - -## Event - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **type** | `string` | ✅ | Event Type (e.g. "APPROVE", "REJECT", "Submit") | -| **schema** | `Record` | optional | Expected event payload structure | - - ---- - diff --git a/content/docs/references/automation/meta.json b/content/docs/references/automation/meta.json index 2745091a7f..1ec491015b 100644 --- a/content/docs/references/automation/meta.json +++ b/content/docs/references/automation/meta.json @@ -19,7 +19,6 @@ "approval", "---More---", "builtin-node-config", - "events-core", "flow-function", "io-node-config", "retry-policy", diff --git a/docs/audits/2026-07-unknown-key-strictness-ledger.md b/docs/audits/2026-07-unknown-key-strictness-ledger.md index 2345c9428f..0b8a772253 100644 --- a/docs/audits/2026-07-unknown-key-strictness-ledger.md +++ b/docs/audits/2026-07-unknown-key-strictness-ledger.md @@ -477,14 +477,14 @@ tightening (the #4001 "sharing-rule lesson": candidates, not verdicts). | `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 | | `field-value.zod.ts` / `seed.zod.ts` | 1+1 | mixed (p) | `seed` is strict (registered-types batch) | -### `automation/` — 87 sites +### `automation/` — 86 sites | File | Sites | Class | Note | |---|---|---|---| | `flow.zod.ts` | 11 | authorable | **strict as of #4001** (4 schemas; `FlowVersionHistorySchema` is runtime — stays tolerant) | | `sync.zod.ts` / `etl.zod.ts` | 12+10 | authorable (p) | authored pipelines — **candidates** | | `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 | -| `state-machine.zod.ts` | 7 | authorable (p) | | +| `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 | | `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 | | `bpmn-interop.zod.ts` | 5 | wire (p) | interop import shapes | | `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) | diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index 9be351fd8c..0df9f27c84 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2166,7 +2166,6 @@ "ETLTransformationSchema (const)", "ETLTransformationType (type)", "ETLTransformationTypeSchema (const)", - "EventSchema (const)", "ExecutionError (type)", "ExecutionErrorParsed (type)", "ExecutionErrorSchema (const)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 9a7b56b826..44a722918a 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -2271,8 +2271,6 @@ "automation/ETLTransformation:continueOnError", "automation/ETLTransformation:name", "automation/ETLTransformation:type", - "automation/Event:schema", - "automation/Event:type", "automation/ExecutionError:code", "automation/ExecutionError:context", "automation/ExecutionError:executionId", diff --git a/packages/spec/docs-import-surface.baseline.json b/packages/spec/docs-import-surface.baseline.json index f7165d3249..336f91189c 100644 --- a/packages/spec/docs-import-surface.baseline.json +++ b/packages/spec/docs-import-surface.baseline.json @@ -19,7 +19,6 @@ "api/MetadataValidateRequest — no type export", "api/SubscriptionEvent — no type export", "automation/ApproverType — no type export", - "automation/Event — no type export", "automation/FlowNodeAction — no type export", "automation/FlowVariable — no type export", "automation/GuardRef — no type export", diff --git a/packages/spec/dual-source-exports.baseline.json b/packages/spec/dual-source-exports.baseline.json index d99af4811d..0deda44bb0 100644 --- a/packages/spec/dual-source-exports.baseline.json +++ b/packages/spec/dual-source-exports.baseline.json @@ -9,7 +9,6 @@ "EnvironmentArtifact — [./cloud (type)] ≠ [./system (type)]", "EnvironmentArtifactInput — [./cloud (type)] ≠ [./system (type)]", "EnvironmentArtifactSchema — [./cloud (const)] ≠ [./system (const)]", - "EventSchema — [./automation (const)] ≠ [./kernel (const)]", "PackageDependency — [./cloud (type)] ≠ [./kernel (type)]", "PackageDependencySchema — [./cloud (const)] ≠ [./kernel (const)]", "TenantPlan — [./cloud (type)] ≠ [./system (type)]", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 17704dcfb2..fed09a6893 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -532,7 +532,6 @@ "automation/ETLSyncMode", "automation/ETLTransformation", "automation/ETLTransformationType", - "automation/Event", "automation/ExecutionError", "automation/ExecutionErrorSeverity", "automation/ExecutionLog", diff --git a/packages/spec/src/automation/state-machine.test.ts b/packages/spec/src/automation/state-machine.test.ts index d9517985d7..a6838c7fe4 100644 --- a/packages/spec/src/automation/state-machine.test.ts +++ b/packages/spec/src/automation/state-machine.test.ts @@ -122,3 +122,146 @@ describe('StateMachineSchema', () => { expect(() => StateMachineSchema.parse(machine)).toThrow(); }); }); + +// ─── [#4658] `EventSchema` is gone from ./automation — dual-source C6 ──────── +// +// `./automation` and `./kernel` both exported an `EventSchema`, for two +// declarations whose key sets did not even intersect: +// +// automation/state-machine.zod.ts (removed) → `{ type, schema }` — an +// XState-style signal DECLARATION ("which events does this machine +// accept"). An orphan: `StateMachineSchema` names event types as the +// record keys of `on:`, and no repo imported it (objectstack / cloud / +// objectui, import-statement-level scan). +// kernel/events/core.zod.ts → `{ id?, name, payload, metadata }` — an +// event-bus ENVELOPE (an emitted event instance). +// +// Converging them would have declared a signal definition to be an envelope — +// a false statement in the contract — so the orphan was deleted instead +// (maintainer ruling on #4658; ledger #4535 C6). The kernel-side analogue of a +// signal *declaration* already exists: `EventTypeDefinitionSchema`, same file. +// +// #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 compiler-API test below, with +// anti-vacuity guards; sabotage-verified in the PR (re-adding the export +// turns it red). +describe('[#4658] `EventSchema` is not exported from ./automation', () => { + it('resolves the export surface: no entry but ./kernel declares `EventSchema`', async () => { + const ts = (await import('typescript')).default; + const { resolve, relative, 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 uniqueness pin 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. + expect(Object.keys(entries)).toContain('./automation'); + expect(Object.keys(entries)).toContain('./kernel'); + 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 unalias = (s: import('typescript').Symbol) => + s.getFlags() & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(s) : s; + + 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 assertion + // below pass vacuously — the exact way a gate goes dormant (#4642). + expect(moduleSym, `${sub} module symbol must resolve`).toBeTruthy(); + return checker.getExportsOfModule(moduleSym!); + }; + + const originOf = (sym: import('typescript').Symbol, label: string) => { + const decl = unalias(sym).declarations?.[0]; + expect(decl, `${label} must have a declaration`).toBeTruthy(); + const declFile = decl!.getSourceFile(); + return `${relative(specDir, declFile.fileName)}:${ + declFile.getLineAndCharacterOfPosition(decl!.getStart()).line + 1 + }`; + }; + + // 1. The removed side: `./automation` still has a non-trivial surface — + // so the `not.toContain` cannot pass by resolving nothing — and no + // longer names `EventSchema`, while its surviving neighbours stand. + const automationExports = exportsOf('./automation'); + expect(automationExports.length, './automation must export a non-trivial surface').toBeGreaterThan(50); + const automationNames = automationExports.map((e) => e.getName()); + expect(automationNames).not.toContain('EventSchema'); + expect(automationNames).toContain('StateMachineSchema'); + expect(automationNames).toContain('TransitionSchema'); + + // 2. The surviving side: `./kernel` still exports the envelope const and + // its inferred type, declared in kernel/events/core.zod.ts. + const kernelExports = exportsOf('./kernel'); + const kernelEventSchema = kernelExports.find((e) => e.getName() === 'EventSchema'); + expect(kernelEventSchema, './kernel must export `EventSchema`').toBeTruthy(); + const kernelOrigin = originOf(kernelEventSchema!, './kernel EventSchema'); + expect(kernelOrigin).toMatch(/^src\/kernel\/events\/core\.zod\.ts:\d+$/); + expect(kernelExports.map((e) => e.getName())).toContain('Event'); + + // 3. Uniqueness — the dual-source pin proper: across EVERY public entry, + // an export named `EventSchema` must resolve to that ONE declaration. + const holders: string[] = []; + for (const sub of Object.keys(entries)) { + for (const sym of exportsOf(sub).filter((e) => e.getName() === 'EventSchema')) { + holders.push(sub); + expect( + originOf(sym, `${sub} EventSchema`), + `${sub} must resolve \`EventSchema\` to the kernel declaration`, + ).toBe(kernelOrigin); + } + } + expect(holders).toContain('./kernel'); + expect(holders).not.toContain('./automation'); + }); + + it('keeps the runtime namespaces consistent with the compiler view', async () => { + const automation = await import('./index'); + const kernel = await import('../kernel/index'); + expect('EventSchema' in automation).toBe(false); + expect('EventSchema' in kernel).toBe(true); + + // What the name now unambiguously means: an emitted event INSTANCE. + expect(() => + kernel.EventSchema.parse({ + name: 'user.created', + payload: { id: 'u1' }, + metadata: { source: 'test', timestamp: '2026-08-03T00:00:00.000Z' }, + }), + ).not.toThrow(); + + // The removed side's shape — a signal DECLARATION — is not what the + // surviving schema accepts: the two concepts were never converged. + expect(() => kernel.EventSchema.parse({ type: 'APPROVE' })).toThrow(); + }); + + it('still authors state-machine event types as `on:` record keys — the surface the orphan never was', () => { + const machine = { + id: 'c6_pin', + initial: 'draft', + states: { + draft: { on: { APPROVE: 'approved' } }, + approved: { type: 'final' }, + }, + }; + expect(() => StateMachineSchema.parse(machine)).not.toThrow(); + }); +}); diff --git a/packages/spec/src/automation/state-machine.zod.ts b/packages/spec/src/automation/state-machine.zod.ts index 857305cdd7..5210a0c601 100644 --- a/packages/spec/src/automation/state-machine.zod.ts +++ b/packages/spec/src/automation/state-machine.zod.ts @@ -49,14 +49,13 @@ export const TransitionSchema = lazySchema(() => z.object({ description: z.string().optional().describe('Human readable description of this rule'), })); -/** - * Event Definition (Signals) - */ -export const EventSchema = lazySchema(() => z.object({ - type: z.string().describe('Event Type (e.g. "APPROVE", "REJECT", "Submit")'), - // Payload validation schema could go here if we want deep validation - schema: z.record(z.string(), z.unknown()).optional().describe('Expected event payload structure'), -})); +// `EventSchema` (XState-style signal declaration `{ type, schema }`) was removed +// here in #4658 (dual-source ledger #4535 C6): nothing in this file — or any +// repo — ever referenced it. Event types on a state machine are the RECORD KEYS +// of `on:` (plain strings), not declared signal objects. The platform's one +// `EventSchema` is the event-bus envelope in `kernel/events/core.zod.ts`; the +// kernel-side analogue of a signal *declaration* is `EventTypeDefinitionSchema` +// in the same file. export type ActionRef = z.infer; export type Transition = z.infer;