T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)

This commit is contained in:
Lukas
2026-03-03 12:54:29 +01:00
parent ddb2b317d3
commit 7dd4abb12a
27 changed files with 2655 additions and 35 deletions

View File

@@ -0,0 +1,160 @@
# Feature Specification: Advance Turn
**Feature Branch**: `001-advance-turn`
**Created**: 2026-03-03
**Status**: Draft
**Input**: Walking-skeleton domain feature — deterministic turn advancement
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Advance Turn (Priority: P1)
A game master running an encounter advances the turn to the next
combatant in initiative order. When the last combatant in the round
finishes, the round number increments and play wraps to the first
combatant.
**Why this priority**: This is the irreducible core of an initiative
tracker. Without turn advancement, no other feature has meaning.
**Independent Test**: Can be fully tested as a pure state transition
with no I/O, persistence, or UI. Given an Encounter value and an
AdvanceTurn action, assert the resulting Encounter value and emitted
domain events.
**Acceptance Scenarios**:
1. **Given** an encounter with combatants [A, B, C], activeIndex 0,
roundNumber 1,
**When** AdvanceTurn,
**Then** activeIndex is 1, roundNumber is 1,
and a TurnAdvanced event is emitted with
previousCombatantId A, newCombatantId B, roundNumber 1.
2. **Given** an encounter with combatants [A, B, C], activeIndex 1,
roundNumber 1,
**When** AdvanceTurn,
**Then** activeIndex is 2, roundNumber is 1,
and a TurnAdvanced event is emitted with
previousCombatantId B, newCombatantId C, roundNumber 1.
3. **Given** an encounter with combatants [A, B, C], activeIndex 2,
roundNumber 1,
**When** AdvanceTurn,
**Then** activeIndex is 0, roundNumber is 2,
and events are emitted in order: TurnAdvanced
(previousCombatantId C, newCombatantId A, roundNumber 2) then
RoundAdvanced (newRoundNumber 2).
4. **Given** an encounter with combatants [A, B, C], activeIndex 2,
roundNumber 5,
**When** AdvanceTurn,
**Then** activeIndex is 0, roundNumber is 6,
and events are emitted in order: TurnAdvanced then RoundAdvanced
(verifies round increment is not hardcoded to 2).
5. **Given** an encounter with a single combatant [A], activeIndex 0,
roundNumber 1,
**When** AdvanceTurn,
**Then** activeIndex is 0, roundNumber is 2,
and events are emitted in order: TurnAdvanced
(previousCombatantId A, newCombatantId A, roundNumber 2) then
RoundAdvanced (newRoundNumber 2).
6. **Given** an encounter with combatants [A, B], activeIndex 0,
roundNumber 1,
**When** AdvanceTurn is applied twice in sequence,
**Then** after the first: activeIndex 1, roundNumber 1;
after the second: activeIndex 0, roundNumber 2.
7. **Given** an encounter with an empty combatant list,
**When** AdvanceTurn,
**Then** the operation MUST fail with an invalid-encounter error.
No events are emitted. State is unchanged.
8. **Given** an encounter with combatants [A, B, C], activeIndex 0,
roundNumber 1,
**When** AdvanceTurn is applied three times,
**Then** the encounter completes a full round cycle:
activeIndex returns to 0 and roundNumber is 2.
---
### Edge Cases
- Empty combatant list: AdvanceTurn MUST reject with an error.
- Single combatant: every advance wraps and increments the round.
- Large round numbers: no overflow or special-case behavior; round
increments uniformly.
## Domain Model *(mandatory)*
### Key Entities
- **Combatant**: An identified participant in the encounter. For this
feature, a combatant is an opaque identity (e.g., a name or id).
The MVP baseline does not include HP, conditions, or stats.
- **Encounter**: The aggregate root. Contains an ordered list of
combatants (pre-sorted by initiative), an activeIndex pointing to
the current combatant, and a roundNumber (positive integer,
starting at 1).
### Domain Events
- **TurnAdvanced**: Emitted on every successful AdvanceTurn.
Carries: previousCombatantId, newCombatantId, roundNumber.
- **RoundAdvanced**: Emitted when activeIndex wraps past the last
combatant. Carries: newRoundNumber.
When a round boundary is crossed, both TurnAdvanced and
RoundAdvanced MUST be emitted in that order (TurnAdvanced first).
This emission order is part of the observable domain contract and
MUST be verified by tests.
### Invariants
- **INV-1**: An encounter MUST have at least one combatant.
Operations on an empty encounter MUST fail.
- **INV-2**: activeIndex MUST always satisfy
0 <= activeIndex < len(combatants).
- **INV-3**: roundNumber MUST be a positive integer (>= 1) and MUST
only increase (never decrease or reset).
- **INV-4**: AdvanceTurn MUST be a pure function of the current
encounter state. Given identical input, output MUST be identical.
- **INV-5**: Every successful AdvanceTurn MUST emit at least one
domain event (TurnAdvanced). No silent state changes.
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The domain MUST expose an AdvanceTurn operation that
accepts an Encounter and returns the next Encounter state plus
emitted domain events.
- **FR-002**: AdvanceTurn MUST increment activeIndex by 1, wrapping
to 0 when past the last combatant.
- **FR-003**: When activeIndex wraps to 0, roundNumber MUST
increment by 1.
- **FR-004**: AdvanceTurn on an empty encounter MUST return an error
without modifying state or emitting events.
- **FR-005**: Domain events MUST be returned as values from the
operation, not dispatched via side effects.
### Out of Scope (MVP baseline does not include)
- Initiative rolling or combatant ordering logic
- Hit points, damage, conditions, or status effects
- Adding or removing combatants mid-encounter
- Persistence, serialization, or storage
- UI, CLI, or any adapter layer
- Agent behavior or suggestions
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 8 acceptance scenarios pass as deterministic,
pure-function tests with no I/O dependencies.
- **SC-002**: Invariants INV-1 through INV-5 are verified by tests.
- **SC-003**: The domain module has zero imports from application,
adapter, or agent layers (layer boundary compliance).