# Data Model: Edit Combatant **Feature**: 004-edit-combatant **Date**: 2026-03-03 ## Entities ### Combatant (unchanged) | Field | Type | Notes | |-------|------|-------| | id | CombatantId (branded string) | Immutable identity | | name | string | Mutable — this feature updates it | ### Encounter (unchanged structure) | Field | Type | Notes | |-------|------|-------| | combatants | readonly Combatant[] | Edit replaces name in-place by mapping | | activeIndex | number | Preserved during edit | | roundNumber | number | Preserved during edit | ## Events ### CombatantUpdated (new) | Field | Type | Notes | |-------|------|-------| | type | "CombatantUpdated" | Discriminant | | combatantId | CombatantId | Which combatant was renamed | | oldName | string | Name before edit | | newName | string | Name after edit | Added to the `DomainEvent` union type. ## State Transitions ### editCombatant(encounter, id, newName) **Preconditions**: - `newName` is non-empty and not whitespace-only - `id` matches a combatant in `encounter.combatants` **Postconditions**: - The combatant with matching `id` has `name` set to `newName` - `activeIndex` and `roundNumber` unchanged - Combatant list order unchanged - Exactly one `CombatantUpdated` event emitted **Error cases**: - `id` not found → `DomainError { code: "combatant-not-found" }` - `newName` empty/whitespace → `DomainError { code: "invalid-name" }` ## Validation Rules | Rule | Condition | Error Code | |------|-----------|------------| | Name must be non-empty | `newName.trim().length === 0` | `"invalid-name"` | | Combatant must exist | No combatant with matching `id` | `"combatant-not-found"` |