2.5 KiB
Research: Add Combatant
Feature: 002-add-combatant Date: 2026-03-03
Research Summary
No NEEDS CLARIFICATION items existed in the technical context. The feature is straightforward and follows established patterns. Research focused on confirming existing patterns and the one key design decision.
Decision 1: CombatantId Generation Strategy
Decision: CombatantId is passed into the domain function as an argument, not generated internally.
Rationale: The domain layer must remain pure and deterministic (Constitution Principle I). Generating IDs internally would require either randomness (UUID) or side effects (counter with mutable state), both of which violate purity. By accepting the id as input, addCombatant(encounter, id, name) is a pure function: same inputs always produce the same output.
Alternatives considered:
- Generate UUID inside domain: Violates deterministic core principle. Tests would be non-deterministic.
- Pass an id-generator function: Adds unnecessary complexity. The application layer can generate the id and pass it in.
Who generates the id: The application layer (use case) or adapter layer (hook) generates the CombatantId before calling the domain function. This matches how createEncounter already works — callers construct Combatant objects with pre-assigned ids.
Decision 2: Function Signature Pattern
Decision: Follow the advanceTurn pattern — standalone pure function returning a success result or DomainError.
Rationale: Consistency with the existing codebase. advanceTurn returns AdvanceTurnSuccess | DomainError, so addCombatant will return AddCombatantSuccess | DomainError with the same shape: { encounter, events }.
Alternatives considered:
- Method on an Encounter class: Project uses plain interfaces and free functions, not classes.
- Mutating the encounter in place: Violates immutability convention (all fields are
readonly).
Decision 3: Name Validation Approach
Decision: Trim whitespace, then reject empty strings. The domain function validates the name.
Rationale: Name validation is a domain rule (what constitutes a valid combatant name), so it belongs in the domain layer. Trimming before checking prevents whitespace-only names from slipping through.
Alternatives considered:
- Validate in application layer: Would allow invalid data to reach domain if called from a different adapter. Domain should protect its own invariants.
- Accept any string: Would allow empty-name combatants, violating spec FR-004.