Event Payload Change Policy
Policy for evolving mutation payloads while preserving replay and projection correctness.
Decision summary
- Delta-only payloads are not the default.
- Every mutation event defines one authoritative change representation.
- Authoritative mutation data should be
after-oriented for deterministic replay. - Redundant fields (
before,delta,added,removed) are informational and must pass consistency checks. - Full-replace payloads are valid only when replace semantics are explicit.
Mutation classes
Every mutation event must declare one class.
field_patch
Sparse updates of selected fields.
- authoritative:
*_afterfields orfieldspatch map - informational:
*_before, reason/source metadata - required checks: non-empty patch, unknown-key rejection, optional before/current parity
set_replace
Full logical set/list replacement.
- authoritative: normalized
*_after - informational:
added,removed, optional*_before - required checks: normalized
after; added/removed must match computed diff
operation
Intent-focused operations with resulting state.
- authoritative: operation identity + resulting
aftervalues when state mutates - informational:
before,delta, source/roll metadata - required checks: arithmetic/invariant consistency; explicit no-op policy
full_replace
Explicit full subdocument/record replacement.
- authoritative: full scoped object
- informational: optional reason/source metadata
- required checks: replace scope explicit; projectors assign full scope unconditionally
Cross-cutting authority rules
- One event type uses one authoritative representation.
- Projectors/adapters mutate from authoritative fields only.
- Informational fields are never required for correctness.
- Replay compatibility may decode legacy shapes, but deciders emit canonical shape.
Inventory posture
Current high-mutation domains (campaign, character, participant, action, sys.daggerheart) already map to these classes. New event types must adopt class + authority semantics during review.
Detailed inventories belong in generated catalogs and implementation tickets, not in this policy page.
Rollout sequence
- Classify event type (
field_patch,set_replace,operation,full_replace). - Define authoritative fields in payload docs and validators.
- Add consistency checks for informational redundancy.
- Ensure deciders emit canonical shape.
- Ensure adapters read authoritative fields only.
- Keep compatibility decoders for legacy historical replay when needed.
Replay-safety rules for new fields
Adding a field to an existing payload must not break replay of older events.
- New optional fields must use
omitempty. Without it, serialized events contain explicit zero/null values that create ambiguity during replay. - Pointer fields (
*int,*string, etc.) must always includeomitempty. TheTestPayloadPointerFieldsRequireOmitemptyarchitecture test enforces this at CI time. - Never remove or rename a field in a payload struct. Old events in the journal still carry the old shape.
- Never add a required (non-pointer, non-omitempty) field to an existing payload. Old events will unmarshal with Go zero values, causing silent state divergence.
Adoption checklist
- class selected and documented
- authoritative fields identified
- redundancy consistency checks added
- decider emission canonicalized
- adapter consumption uses authoritative fields only
- replay compatibility note captured where legacy payloads exist