refactor(spec)!: remove DataEventType 'data.field.changed' — no producer (#4673) - #4685
Conversation
…cer (#4673) `data.field.changed` was declared in `DataEventType` and emitted by nothing. The engine's `publishDataEvent` sends `data.record.{created,updated,deleted}` and (since #4639) `data.records.{updated,deleted}`; no other producer exists in either repository. A subscriber switching on it held a branch that could never run, and the surrounding `switch` still compiled — ADR-0078's silently-inert declaration, on the event vocabulary. It could not have been implemented against this contract as written either: `DataEventSchema` is record-shaped (`recordId`, `changes`, `before`, `after`) with no `field` / `oldValue` / `newValue` slot, so the member advertised a granularity the payload has no room for. FROM `type: 'data.field.changed'` TO `type: 'data.record.updated'`, reading the per-field detail off the payload's `changes` map (with `before` / `after`). Nothing is lost — that detail has always ridden on the record event, as one event per write rather than N on a wide table. Registered as an ADR-0087 D3 semantic migration (`data-field-changed-event-retired`) rather than a D2 conversion: this is a runtime EVENT surface, so there is no authorable source for `os migrate meta` to rewrite. Deliberately no `retiredKey()` tombstone — a removed enum VALUE cannot carry a fix-it prescription the way an authorable object key can (the same limit the sharing-rule `full` retirement hit). The enforced channels are tsc and the enum parse. ADR-0049 enforce-or-remove, route 3. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LYnZrTwXbrctB8E8HpJAPT
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
…ire-data-field-changed # Conflicts: # docs/protocol-upgrade-guide.md # packages/spec/src/migrations/registry.ts
📓 Docs Drift CheckThis PR changes 1 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
已合入
|
| 冲突文件 | 解决方式 |
|---|---|
packages/spec/src/migrations/registry.ts |
双方都在 step 17 的 rationale 末尾追加段落。两段都保留,按 main 的段落在前、本次移除的段落在后,conversionIds 与 semantic[] 数组本身自动合并无冲突。 |
docs/protocol-upgrade-guide.md |
生成物,不手工解冲突:取 main 版本后重新跑 gen:upgrade-guide。 |
check:generated 在合并后精确指认出只有 docs/protocol-upgrade-guide.md 一项过期(spec-changes.json 与 content/docs/references/** 自动合并后即为最新),重新生成该项后门禁全绿:
check:generated PASS check:spec-changes PASS
check:liveness PASS check:upgrade-guide PASS
check:empty-state PASS check:skill-examples PASS
check:authorable-surface PASS check:strictness-ledger PASS
check:api-surface PASS
合并后重跑 spec 测试:Test Files 292 passed (292) / Tests 7330 passed (7330),typecheck exit 0。
相对 main 的净 diff 仍严格限定在 7 个文件、+196/−5,未触碰 content/docs/releases/。
与 #4677 的潜在文本冲突提示依然有效(它尚未合入
main),见 PR 描述最后一节。
Generated by Claude Code
Fixes #4673
按维护者裁决走 ADR-0049 enforce-or-remove 的路线 3(移除)。
为什么移除
data.field.changed声明在DataEventType里,但全仓没有任何生产者:engine 的publishDataEvent只发data.record.{created,updated,deleted},以及(自 #4639 起)data.records.{updated,deleted}。订阅方switch到这个值上的分支永远不会执行,而switch本身照样编译通过——这正是 ADR-0078 说的"静默失效的声明",只不过发生在事件词表上。更关键的是,按现有契约它根本无法被实现:
DataEventSchema是 record 形状的(recordId/changes/before/after),没有field/oldValue/newValue槽位可以承载逐字段语义。这个 enum 成员承诺了一个 payload 装不下的粒度。FROM → TO
type: 'data.field.changed'type: 'data.record.updated',逐字段明细从 payload 的changes读(before/after给出前后状态)一行修复——删掉死分支,改从 update 事件读
changes:信息没有丢失:逐字段明细本来就在
data.record.updated上,而且是一次写入一个事件,而不是宽表上 N 个事件。删掉那个分支不改变任何可观测行为——它从来没跑过。退役套件:哪些面适用、哪些不适用
这是本次实现前先做的判断。
data.field.changed是运行时事件类型的 enum 成员,不是作者在 stack 定义里书写的可授权(authorable)元数据属性,所以.claude/skills/spec-property-retirement的很多机件并不适用。逐条核对结果:适用:
DataEventType删除成员,并在 schema 注释里写明移除了什么、活的机制是什么data-field-changed-event-retired到migrations/registry.ts的 step 17,并扩写该 step 的rationaleapi/events.test.ts:收窄后的.options、退役名不再 parse、以及 FROM → TO 的正向 pinspec-changes.json、docs/protocol-upgrade-guide.md、content/docs/references/api/events.mdx@objectstack/specmajor,含 FROM → TO 表与一行修复不适用(逐条说明理由,避免下次重新审一遍):
retiredKey()墓碑 —— 一个被删除的 enum 值无法承载 fix-it 提示,这和 step 17 里 sharing-rulefull退役时撞上的是同一个限制(该 step 的 rationale 已经写明这一点)。可强制执行的通道是 tsc(任何仍把该值写在DataEventType位置上的消费者编译失败)和 enum parse(现在直接拒绝,而不是接受一个永不到达的事件)。os migrate meta改写。按enhanced-api-error-field-errors-renamed和data-driver-find-stream-retired的先例,登记为 semantic TODO 而非 conversion。(webhook 的订阅走的是另一个可授权 enumWebhookTriggerType,其词表早在 Webhook triggersundeleteandapiare declared but never fire #3196 就已经收敛到"真实存在的生产者"。)object/field/flow…),liveness/下没有 event 条目;DataEvent是运行时 payload 契约。check:liveness、check:empty-state均原样通过。authorable-surface.json/ gate (a)(b) —— 该棘轮追踪的是可授权键(api/DataEvent:type这种),不是 enum 成员,所以键列表不变、两个 gate 正确地保持沉默(check:authorable-surfacePASS 已验证)。api-surface.json—— 快照记录导出的name (kind),对 enum 成员级收窄是盲的;DataEventType (type)条目不变(check:api-surfacePASS 已验证)。*.form.ts没有对应输入,lint 是 ledger 驱动的,examples/与skills/全仓 grep 无命中。content/docs/references/api/realtime.mdx—— 里面的field.changed属于另一个 enumRealtimeEventType(不带data.前缀),归 Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197,本 PR 不动。验证
全部在本地实跑,命令与结果如下:
重新生成后的 gate 全绿:
测试与类型检查:
#4677(分支
claude/bulk-write-missing-events-sm1i4b)向同一个文件packages/spec/src/api/events.zod.ts新增BulkDataEventType/BulkDataEventSchema。本分支从origin/main切出(尚不含 #4677),diff 严格限定在data.field.changed的移除上,但两者大概率会在该文件产生文本冲突。后合并的一方请注意:解决冲突后必须重新跑 spec 生成物门禁,否则
spec-changes.json/protocol-upgrade-guide.md/content/docs/references/**会过期:pnpm --filter @objectstack/spec build pnpm --filter @objectstack/spec check:generated # 再按它证明过期的项精确重新生成两者语义上不冲突:#4677 新增一套批量写事件契约,本 PR 移除一个无生产者的成员,方向一致(都在让事件词表只保留真实存在的生产者)。
刻意没做
content/docs/releases/(code PR 禁止,发布说明在 release 时集中从 changeset + ADR-0087 registries 编译)。content/docs/references/下的自动生成文件——全部走重新生成。RealtimeEventType的field.changed(属 Audit: several event/subscription/connector enums are schema-only (declared, no runtime consumer) #3197,不是本 issue 的范围)。Generated by Claude Code