Entity rehydration (reconstructing typed domain objects from untyped JSON) currently lives in persistence adapters, duplicating validation logic that the domain already owns. This caused a bug where adding level to PlayerCharacter required updating both the domain type and a separate rehydration function in the persistence layer — the persistence function was missed, silently dropping levels on reload. Moving rehydration to the domain ensures adding a field to a type and its rehydration function is the only change needed.
Acceptance Criteria
Domain exports rehydratePlayerCharacter(raw: unknown): PlayerCharacter | null that validates all fields (id, name, ac, maxHp, color, icon, level) and returns a typed instance or null
Domain exports rehydrateCombatant(raw: unknown): Combatant | null with equivalent validation for combatant fields
player-character-storage.ts delegates to rehydratePlayerCharacter instead of reimplementing field checks
encounter-storage.ts delegates to rehydrateCombatant instead of reimplementing field checks
validateImportBundle and rehydrateEncounter are unchanged — they validate bundle envelope / encounter shape in the adapter and delegate entity validation to domain rehydration functions
Adding a new optional field to PlayerCharacter or Combatant requires updating only the domain type and its rehydration function — persistence adapters stay in sync automatically
Exhaustive corrupt/missing-field test cases live in domain tests; persistence tests only verify localStorage round-trip and delegation
## Summary
Entity rehydration (reconstructing typed domain objects from untyped JSON) currently lives in persistence adapters, duplicating validation logic that the domain already owns. This caused a bug where adding `level` to `PlayerCharacter` required updating both the domain type and a separate rehydration function in the persistence layer — the persistence function was missed, silently dropping levels on reload. Moving rehydration to the domain ensures adding a field to a type and its rehydration function is the only change needed.
## Acceptance Criteria
- [x] Domain exports `rehydratePlayerCharacter(raw: unknown): PlayerCharacter | null` that validates all fields (id, name, ac, maxHp, color, icon, level) and returns a typed instance or null
- [x] Domain exports `rehydrateCombatant(raw: unknown): Combatant | null` with equivalent validation for combatant fields
- [x] `player-character-storage.ts` delegates to `rehydratePlayerCharacter` instead of reimplementing field checks
- [x] `encounter-storage.ts` delegates to `rehydrateCombatant` instead of reimplementing field checks
- [x] `validateImportBundle` and `rehydrateEncounter` are unchanged — they validate bundle envelope / encounter shape in the adapter and delegate entity validation to domain rehydration functions
- [x] Adding a new optional field to `PlayerCharacter` or `Combatant` requires updating only the domain type and its rehydration function — persistence adapters stay in sync automatically
- [x] Exhaustive corrupt/missing-field test cases live in domain tests; persistence tests only verify localStorage round-trip and delegation
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Entity rehydration (reconstructing typed domain objects from untyped JSON) currently lives in persistence adapters, duplicating validation logic that the domain already owns. This caused a bug where adding
leveltoPlayerCharacterrequired updating both the domain type and a separate rehydration function in the persistence layer — the persistence function was missed, silently dropping levels on reload. Moving rehydration to the domain ensures adding a field to a type and its rehydration function is the only change needed.Acceptance Criteria
rehydratePlayerCharacter(raw: unknown): PlayerCharacter | nullthat validates all fields (id, name, ac, maxHp, color, icon, level) and returns a typed instance or nullrehydrateCombatant(raw: unknown): Combatant | nullwith equivalent validation for combatant fieldsplayer-character-storage.tsdelegates torehydratePlayerCharacterinstead of reimplementing field checksencounter-storage.tsdelegates torehydrateCombatantinstead of reimplementing field checksvalidateImportBundleandrehydrateEncounterare unchanged — they validate bundle envelope / encounter shape in the adapter and delegate entity validation to domain rehydration functionsPlayerCharacterorCombatantrequires updating only the domain type and its rehydration function — persistence adapters stay in sync automatically