# Data Model: Add Combatant **Feature**: 002-add-combatant **Date**: 2026-03-03 ## Entities ### Combatant (existing, unchanged) | Field | Type | Constraints | |-------|------|-------------| | id | CombatantId (branded string) | Unique, required | | name | string | Non-empty after trimming, required | ### Encounter (existing, unchanged) | Field | Type | Constraints | |-------|------|-------------| | combatants | readonly Combatant[] | Ordered list, may be empty | | activeIndex | number | 0 <= activeIndex < combatants.length (or 0 if empty) | | roundNumber | number | Positive integer >= 1, only increases | ## Domain Events ### CombatantAdded (new) | Field | Type | Description | |-------|------|-------------| | type | "CombatantAdded" (literal) | Discriminant for the DomainEvent union | | combatantId | CombatantId | Id of the newly added combatant | | name | string | Name of the newly added combatant | | position | number | Zero-based index where the combatant was placed | ## State Transitions ### AddCombatant **Input**: Encounter + CombatantId + name (string) **Preconditions**: - Name must be non-empty after trimming **Transition**: - New combatant `{ id, name: trimmedName }` appended to end of combatants list - activeIndex unchanged - roundNumber unchanged **Postconditions**: - combatants.length increased by 1 - New combatant is at index `combatants.length - 1` - All existing combatants preserve their order and index positions - INV-2 satisfied (activeIndex still valid for the now-larger list) **Events emitted**: Exactly one `CombatantAdded` **Error cases**: - Empty or whitespace-only name → DomainError `{ code: "invalid-name" }` ## Function Signatures ### Domain Layer ``` addCombatant(encounter, id, name) → { encounter, events } | DomainError ``` ### Application Layer ``` addCombatantUseCase(store, id, name) → DomainEvent[] | DomainError ``` ## Validation Rules | Rule | Layer | Error Code | |------|-------|------------| | Name non-empty after trim | Domain | invalid-name |