|
| 1 | +--- |
| 2 | +'@objectstack/spec': major |
| 3 | +--- |
| 4 | + |
| 5 | +**BREAKING**: `DataEventType` drops `data.field.changed` — it had no producer (ADR-0049 enforce-or-remove, #4673) |
| 6 | + |
| 7 | +`data.field.changed` was declared in the `DataEventType` enum and emitted by |
| 8 | +nothing. The engine's `publishDataEvent` sends `data.record.{created,updated,deleted}` |
| 9 | +and (since #4639) `data.records.{updated,deleted}`; no other producer exists in |
| 10 | +either repository. A subscriber that switched on `data.field.changed` held a |
| 11 | +branch that could never run — and because the surrounding `switch` still |
| 12 | +compiled, nothing ever reported the gap. That is ADR-0078's silently-inert |
| 13 | +declaration, on the event vocabulary. |
| 14 | + |
| 15 | +It also could not have been implemented against this contract as written: |
| 16 | +`DataEventSchema` is record-shaped (`recordId`, `changes`, `before`, `after`) |
| 17 | +with no `field` / `oldValue` / `newValue` slot, so the member advertised a |
| 18 | +granularity the payload has no room for. |
| 19 | + |
| 20 | +**FROM → TO** |
| 21 | + |
| 22 | +| FROM | TO | |
| 23 | +| :--- | :--- | |
| 24 | +| `type: 'data.field.changed'` | `type: 'data.record.updated'`, reading the per-field detail from the payload's `changes` map (with `before` / `after` for surrounding state) | |
| 25 | + |
| 26 | +**The one-line fix** — delete the dead branch and read `changes` off the update |
| 27 | +event: |
| 28 | + |
| 29 | +```ts |
| 30 | +// BEFORE — never ran; no producer ever sent this event |
| 31 | +if (event.type === 'data.field.changed') { onFieldChange(event); } |
| 32 | + |
| 33 | +// AFTER — the changed fields have always ridden on the record event |
| 34 | +if (event.type === 'data.record.updated') { |
| 35 | + for (const [field, value] of Object.entries(event.changes ?? {})) onFieldChange(field, value); |
| 36 | +} |
| 37 | +``` |
| 38 | + |
| 39 | +Removing that branch changes no observable behaviour — it never executed — so |
| 40 | +this is deleting code that could not run, not rebuilding a capability. Note the |
| 41 | +replacement is one event per write rather than N events on a wide table. |
| 42 | + |
| 43 | +**The retirement kit:** |
| 44 | + |
| 45 | +- **Schema** — the member is gone from `DataEventType` (`api/events.zod.ts`), |
| 46 | + with an in-schema comment recording what was removed and what the live |
| 47 | + mechanism is. Deliberately **no `retiredKey()` tombstone**: a removed enum |
| 48 | + VALUE cannot carry a fix-it prescription the way an authorable object key |
| 49 | + can (the same limit the sharing-rule `full` retirement hit). The enforced |
| 50 | + channels are `tsc`, which fails any consumer still naming the value in a |
| 51 | + `DataEventType` position, and the enum parse, which now rejects the name |
| 52 | + instead of accepting an event that never arrives. |
| 53 | +- **ADR-0087 D3 semantic migration** — `data-field-changed-event-retired` in |
| 54 | + `migrations/registry.ts` (step 17), carrying the reason and acceptance |
| 55 | + criteria. Registered as a **semantic TODO rather than a D2 conversion** |
| 56 | + because this is a runtime EVENT surface: no stack, example or template |
| 57 | + authors an event name, so there is no source for `os migrate meta` to |
| 58 | + rewrite. (Webhooks subscribe through the separate authorable |
| 59 | + `WebhookTriggerType`, whose vocabulary was already trimmed to producers that |
| 60 | + exist, #3196.) |
| 61 | +- **No liveness-ledger entry** — the ledger governs authorable metadata types |
| 62 | + (`object`, `field`, `flow`, …); `DataEvent` is a runtime payload contract and |
| 63 | + has no ledger file. `check:liveness` and `check:empty-state` pass unchanged. |
| 64 | +- **No `authorable-surface.json` movement** — that ratchet tracks authorable |
| 65 | + *keys* (`api/DataEvent:type` and friends), not enum members, so the key list |
| 66 | + is unchanged and gates (a)/(b) correctly stay silent. |
| 67 | +- **Tests** — `api/events.test.ts` pins the narrowed `.options`, asserts the |
| 68 | + retired name no longer parses, and pins the FROM → TO replacement (that |
| 69 | + `data.record.updated` really does carry `changes` / `before` / `after`). |
| 70 | +- **Docs** — `content/docs/references/api/events.mdx` and |
| 71 | + `docs/protocol-upgrade-guide.md` regenerated. |
| 72 | + |
| 73 | +If a genuine per-field change stream is ever wanted, it earns its own honest |
| 74 | +contract — the precedent #4639 set for bulk writes — rather than reclaiming |
| 75 | +this slot. |
0 commit comments