Systematic Type Migration
Overview
When refactoring components to new type-safe implementations, use this systematic workflow to prevent "works in isolation but broken integration" bugs.
Core principle: Integration test FIRST → file-by-file migration → incremental verification → cleanup
The Problem
Recurring issue during major refactoring:
- •Old component definitions remain in the codebase
- •Some files spawn entities with old types
- •Other files query for new types
- •Systems become disconnected (queries find zero entities)
- •User functionality breaks despite all systems being "properly registered"
Example: After introducing type-safe MovementState enum, if setup.rs spawns entities with the old MovementState but planning.rs queries for the new MovementState, queries will silently fail.
The Workflow
Phase 1: Preparation
Step 1: Document the change
- •Create work directory (e.g.,
docs/work/YYYY-MM-DD-type-safe-X) - •Document scope and goals
Step 2: Identify all uses
# Find all references to the type being replaced grep -r "ComponentName" src/ rg "OldType" --type rust
Step 3: Create integration test FIRST
- •Write test that exercises full user flow (not just isolated system behavior)
- •Test should verify end-to-end functionality
- •This test MUST pass before starting AND after completion
Step 4: Run baseline tests
- •Run project test command
- •Run project check command
- •Capture baseline to compare against
Phase 2: Implementation
Step 1: Create new component
- •Implement in canonical location (e.g.,
src/components/movement/states.rs) - •Include all required derives
Step 2: Do NOT delete old component yet
- •Keep both during migration
- •Enables gradual migration without breaking everything
- •Allows atomic commits per file
Phase 3: Migration (Systematic, File-by-File)
For EACH file using the old component:
Step 1: Update imports
// Before use old_module::OldType; // After use new_module::NewType;
Step 2: Update type usage
- •Pattern matching if enum variants changed
- •Entity spawning to use new type
- •Queries to use new type
Step 3: Test after each file
cargo check # Or language-specific quick check
- •Catch type errors immediately
- •Don't accumulate errors across files
Step 4: Commit atomically
git add path/to/file.rs git commit -m "refactor: migrate FileX to new ComponentName"
- •One commit per file or logical group
- •Enables easy rollback if needed
Common file locations to check:
- •Entity spawning files (e.g.,
setup.rs,spawners.rs) - •System logic files (business logic using the component)
- •UI display files (rendering component state)
- •Component definition files
- •Integration test files
Phase 4: Cleanup
Step 1: Delete old component definition
- •Remove from original module
- •Only after ALL references migrated
Step 2: Remove obsolete imports
# Find unused imports cargo clippy -- -W unused_imports
Step 3: Remove obsolete helper code
- •Builder functions
- •Conversion utilities
- •Deprecated APIs
Step 4: Update exports
- •Remove old type from
mod.rspublic API - •Ensure new type properly exported
Phase 5: Verification
Step 1: Compile clean
cargo check --all-targets # Or language-specific equivalent
Step 2: Run all tests
Run project test command
- •All unit tests must pass
- •All integration tests must pass
Step 3: Verify integration test passes
- •The test created in Phase 1 MUST pass
- •This verifies end-to-end functionality
Step 4: Run checks
Run project check command
- •Linting, formatting, type checking
Step 5: Manual testing
- •Test actual user flows
- •Verify UI updates correctly
- •Check edge cases
Phase 6: Documentation
Step 1: Update pattern docs
- •If new pattern emerged, document it
Step 2: Document in retrospective
- •Capture lessons learned in work directory
- •Note any pitfalls encountered
Step 3: Update project docs
- •If pattern is important, reference from CLAUDE.md or README
Key Principles
| Principle | Rationale |
|---|---|
| Integration test FIRST | Prevents "works in parts, broken as whole" |
| Keep both during migration | Enables atomic commits per file |
| File-by-file, not all-at-once | Easier debugging, clear progress |
| Incremental verification | Catch errors immediately (5 min) vs batch (30+ min) |
| Atomic commits | Easy rollback if specific change breaks something |
Prevention: Integration Tests
The best prevention is integration tests that verify the full user flow, not just isolated system behavior.
What Makes a Good Integration Test
Good integration test:
#[test]
fn test_user_can_move_vehicle() {
// Setup: Spawn entities with realistic component combinations
let world = setup_test_world();
let vehicle = spawn_vehicle_with_all_components(&mut world);
// Act: Trigger user action (click → select → move)
click_vehicle(&mut world, vehicle);
issue_move_order(&mut world, target_position);
// Assert: Verify end result, not internal state
run_systems_until_complete(&mut world);
assert!(vehicle_arrived_at_target(&world, vehicle));
}
Bad integration test:
#[test]
fn test_planning_system_queries() {
// Only tests one system in isolation
// Doesn't verify components are actually compatible
}
Integration test should:
- •Spawn entities with ALL required components (like real usage)
- •Exercise multiple systems together (full pipeline)
- •Verify user-visible outcome, not internal state
- •Use realistic component combinations
Common Mistakes
Mistake 1: Skipping Integration Test
Problem: Discover breakage during manual testing (too late) Solution: Write integration test FIRST, watch it pass LAST
Mistake 2: Big-Bang Migration
Problem: Migrate all files at once, giant debug session when it fails
Solution: File-by-file with cargo check after each
Mistake 3: Deleting Old Type Too Early
Problem: Can't compile during migration, hard to debug Solution: Keep both until migration complete
Mistake 4: Batching Commits
Problem: Hard to identify which change broke tests Solution: Atomic commits per file
Mistake 5: Testing Only Units
Problem: Units pass, integration broken (components incompatible) Solution: Integration test MUST exercise full user flow
Example Workflow
# Phase 1: Preparation mkdir -p docs/work/2025-10-23-type-safe-movement-state grep -r "MovementState" src/ > docs/work/2025-10-23-type-safe-movement-state/references.txt # Write integration test: tests/movement_integration.rs # Run project test command to establish baseline # Phase 2: Implementation # Create src/components/movement/states.rs with new MovementState # Keep old src/space/components.rs::MovementState # Phase 3: Migration (file-by-file) # File 1: src/space/systems/setup.rs nvim src/space/systems/setup.rs # Update import, spawning cargo check # Verify git add src/space/systems/setup.rs git commit -m "refactor: migrate setup.rs to new MovementState" # File 2: src/space/systems/planning.rs nvim src/space/systems/planning.rs # Update import, queries cargo check # Verify git add src/space/systems/planning.rs git commit -m "refactor: migrate planning.rs to new MovementState" # ... repeat for each file ... # Phase 4: Cleanup # Delete old MovementState from src/space/components.rs git add src/space/components.rs git commit -m "refactor: remove old MovementState definition" # Phase 5: Verification cargo check --all-targets # Run project test command - integration test MUST pass # Run project check command # Manual testing # Phase 6: Documentation # Write docs/work/2025-10-23-type-safe-movement-state/summary.md
Related Practices
Before using this skill:
- •Read:
${CLAUDE_PLUGIN_ROOT}principles/development.md- Code structure principles - •Read:
${CLAUDE_PLUGIN_ROOT}principles/testing.md- Testing principles
After migration:
- •Use:
${CLAUDE_PLUGIN_ROOT}skills/requesting-code-review/SKILL.md- Request code review
Testing This Skill
See test-scenarios.md for pressure tests validating this workflow prevents integration breakage.