diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml
index 3a20a4d..0dcd89a 100644
--- a/.github/workflows/pr-validation.yml
+++ b/.github/workflows/pr-validation.yml
@@ -46,7 +46,7 @@ jobs:
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v4
with:
- versionSpec: '5.x'
+ versionSpec: '6.x'
- name: Run GitVersion
id: gitversion
diff --git a/src/PatternKit.Examples/Generators/Memento/EditorStateDemo.cs b/src/PatternKit.Examples/Generators/Memento/EditorStateDemo.cs
new file mode 100644
index 0000000..b560234
--- /dev/null
+++ b/src/PatternKit.Examples/Generators/Memento/EditorStateDemo.cs
@@ -0,0 +1,266 @@
+using PatternKit.Generators;
+
+namespace PatternKit.Examples.Generators.Memento;
+
+///
+/// Demonstrates the Memento pattern source generator with a text editor scenario.
+/// Shows snapshot capture, restore, and undo/redo functionality using generated code.
+///
+public static class EditorStateDemo
+{
+ ///
+ /// Immutable editor state using record class with generated memento support.
+ /// The [Memento] attribute generates:
+ /// - EditorStateMemento struct for capturing snapshots
+ /// - EditorStateHistory class for undo/redo management
+ ///
+ [Memento(GenerateCaretaker = true, Capacity = 100, SkipDuplicates = true)]
+ public partial record class EditorState(string Text, int Cursor, int SelectionLength)
+ {
+ public bool HasSelection => SelectionLength > 0;
+
+ public int SelectionStart => Cursor;
+
+ public int SelectionEnd => Cursor + SelectionLength;
+
+ ///
+ /// Creates an initial empty state.
+ ///
+ public static EditorState Empty() => new("", 0, 0);
+
+ ///
+ /// Inserts text at the cursor position (or replaces selection).
+ /// Returns a new state with the text inserted.
+ ///
+ public EditorState Insert(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ return this;
+
+ string newText;
+ int newCursor;
+
+ if (HasSelection)
+ {
+ // Replace selection
+ newText = Text.Remove(SelectionStart, SelectionLength).Insert(SelectionStart, text);
+ newCursor = SelectionStart + text.Length;
+ }
+ else
+ {
+ // Insert at cursor
+ newText = Text.Insert(Cursor, text);
+ newCursor = Cursor + text.Length;
+ }
+
+ return this with { Text = newText, Cursor = newCursor, SelectionLength = 0 };
+ }
+
+ ///
+ /// Moves the cursor to a new position.
+ ///
+ public EditorState MoveCursor(int position)
+ {
+ var newCursor = Math.Clamp(position, 0, Text.Length);
+ return this with { Cursor = newCursor, SelectionLength = 0 };
+ }
+
+ ///
+ /// Selects text from start position with the given length.
+ ///
+ public EditorState Select(int start, int length)
+ {
+ start = Math.Clamp(start, 0, Text.Length);
+ var end = Math.Clamp(start + length, 0, Text.Length);
+ var selLength = end - start;
+ return this with { Cursor = start, SelectionLength = selLength };
+ }
+
+ ///
+ /// Deletes the selection or one character before the cursor.
+ ///
+ public EditorState Backspace()
+ {
+ if (HasSelection)
+ {
+ var newText = Text.Remove(SelectionStart, SelectionLength);
+ return this with { Text = newText, Cursor = SelectionStart, SelectionLength = 0 };
+ }
+
+ if (Cursor == 0)
+ return this;
+
+ var newText2 = Text.Remove(Cursor - 1, 1);
+ return this with { Text = newText2, Cursor = Cursor - 1, SelectionLength = 0 };
+ }
+
+ public override string ToString() => HasSelection
+ ? $"Text='{Text}' Cursor={Cursor} Sel=[{SelectionStart},{SelectionEnd})"
+ : $"Text='{Text}' Cursor={Cursor}";
+ }
+
+ ///
+ /// Text editor using the generated caretaker for undo/redo.
+ ///
+ public sealed class TextEditor
+ {
+ // The generated EditorStateHistory class manages undo/redo
+ private readonly EditorStateHistory _history;
+
+ public TextEditor()
+ {
+ _history = new EditorStateHistory(EditorState.Empty());
+ }
+
+ public EditorState Current => _history.Current;
+
+ public bool CanUndo => _history.CanUndo;
+
+ public bool CanRedo => _history.CanRedo;
+
+ public int HistoryCount => _history.Count;
+
+ ///
+ /// Applies an editing operation and captures it in history.
+ ///
+ public void Apply(Func operation)
+ {
+ var newState = operation(Current);
+ _history.Capture(newState);
+ }
+
+ ///
+ /// Undoes the last operation.
+ ///
+ public bool Undo()
+ {
+ return _history.Undo();
+ }
+
+ ///
+ /// Redoes the last undone operation.
+ ///
+ public bool Redo()
+ {
+ return _history.Redo();
+ }
+
+ ///
+ /// Clears all history and resets to empty state.
+ ///
+ public void Clear()
+ {
+ _history.Clear(EditorState.Empty());
+ }
+ }
+
+ ///
+ /// Runs a demonstration of the text editor with undo/redo.
+ ///
+ public static List Run()
+ {
+ var log = new List();
+ var editor = new TextEditor();
+
+ void LogState(string action)
+ {
+ log.Add($"{action}: {editor.Current}");
+ }
+
+ // Initial state
+ LogState("Initial");
+
+ // Type "Hello"
+ editor.Apply(s => s.Insert("Hello"));
+ LogState("Insert 'Hello'");
+
+ // Type " world"
+ editor.Apply(s => s.Insert(" world"));
+ LogState("Insert ' world'");
+
+ // Move cursor to position 5 (after "Hello")
+ editor.Apply(s => s.MoveCursor(5));
+ LogState("Move cursor to 5");
+
+ // Insert " brave new"
+ editor.Apply(s => s.Insert(" brave new"));
+ LogState("Insert ' brave new'");
+
+ // Select "Hello" (0-5)
+ editor.Apply(s => s.Select(0, 5));
+ LogState("Select 'Hello'");
+
+ // Replace with "Hi"
+ editor.Apply(s => s.Insert("Hi"));
+ LogState("Replace with 'Hi'");
+
+ // Undo (restore "Hello brave new world" with selection)
+ if (editor.Undo())
+ {
+ LogState("Undo");
+ }
+
+ // Undo (restore no selection)
+ if (editor.Undo())
+ {
+ LogState("Undo");
+ }
+
+ // Undo (restore "Hello world")
+ if (editor.Undo())
+ {
+ LogState("Undo");
+ }
+
+ // Redo
+ if (editor.Redo())
+ {
+ LogState("Redo");
+ }
+
+ // Create divergent branch: make a new edit
+ editor.Apply(s => s.MoveCursor(s.Text.Length));
+ LogState("Move to end (divergent)");
+
+ editor.Apply(s => s.Insert("!!!"));
+ LogState("Insert '!!!' (clears redo)");
+
+ // Try to redo (should fail - redo history was truncated)
+ if (!editor.Redo())
+ {
+ log.Add("Redo failed (as expected - forward history was truncated)");
+ }
+
+ log.Add($"Final: {editor.Current}");
+ log.Add($"CanUndo: {editor.CanUndo}, CanRedo: {editor.CanRedo}");
+ log.Add($"History count: {editor.HistoryCount}");
+
+ return log;
+ }
+
+ ///
+ /// Demonstrates manual memento capture/restore without the caretaker.
+ ///
+ public static List RunManualSnapshot()
+ {
+ var log = new List();
+
+ var state1 = new EditorState("Hello", 5, 0);
+ log.Add($"State1: {state1}");
+
+ // Manually capture a memento
+ var memento = EditorStateMemento.Capture(in state1);
+ log.Add($"Captured memento: Version={memento.MementoVersion}");
+
+ // Modify state
+ var state2 = state1.Insert(" world");
+ log.Add($"State2: {state2}");
+
+ // Restore from memento
+ var restored = memento.RestoreNew();
+ log.Add($"Restored: {restored}");
+ log.Add($"Restored equals State1: {restored == state1}");
+
+ return log;
+ }
+}
diff --git a/src/PatternKit.Examples/Generators/Memento/GameStateDemo.cs b/src/PatternKit.Examples/Generators/Memento/GameStateDemo.cs
new file mode 100644
index 0000000..f86f15b
--- /dev/null
+++ b/src/PatternKit.Examples/Generators/Memento/GameStateDemo.cs
@@ -0,0 +1,267 @@
+using PatternKit.Generators;
+
+namespace PatternKit.Examples.Generators.Memento;
+
+///
+/// Demonstrates the Memento pattern with a game state scenario.
+/// Shows how to use generated memento for save/load and undo/redo in games.
+///
+public static class GameStateDemo
+{
+ ///
+ /// Game state tracking player position, inventory, and game progress.
+ /// Uses a mutable class with generated memento support.
+ ///
+ [Memento(GenerateCaretaker = true, Capacity = 50)]
+ public partial class GameState
+ {
+ public int PlayerX { get; set; }
+ public int PlayerY { get; set; }
+ public int Health { get; set; }
+ public int Score { get; set; }
+ public int Level { get; set; }
+
+ // This field is excluded from snapshots (e.g., temporary state)
+ [MementoIgnore]
+ public bool IsPaused { get; set; }
+
+ public GameState()
+ {
+ Health = 100;
+ Score = 0;
+ Level = 1;
+ }
+
+ ///
+ /// Moves the player to a new position.
+ ///
+ public void MovePlayer(int deltaX, int deltaY)
+ {
+ PlayerX += deltaX;
+ PlayerY += deltaY;
+ }
+
+ ///
+ /// Player takes damage.
+ ///
+ public void TakeDamage(int amount)
+ {
+ Health = Math.Max(0, Health - amount);
+ }
+
+ ///
+ /// Player heals.
+ ///
+ public void Heal(int amount)
+ {
+ Health = Math.Min(100, Health + amount);
+ }
+
+ ///
+ /// Player collects points.
+ ///
+ public void AddScore(int points)
+ {
+ Score += points;
+ }
+
+ ///
+ /// Advance to next level.
+ ///
+ public void AdvanceLevel()
+ {
+ Level++;
+ Health = 100; // Full heal on level up
+ }
+
+ public override string ToString() =>
+ $"Level {Level}: Health={Health}, Score={Score}, Pos=({PlayerX},{PlayerY})";
+ }
+
+ ///
+ /// Game session manager using the generated caretaker.
+ ///
+ public sealed class GameSession
+ {
+ private readonly GameState _state;
+ private readonly GameStateHistory _history;
+
+ public GameSession()
+ {
+ _state = new GameState();
+ _history = new GameStateHistory(_state);
+ }
+
+ public GameState State => _state;
+ public bool CanUndo => _history.CanUndo;
+ public bool CanRedo => _history.CanRedo;
+
+ ///
+ /// Performs an action and saves it in history (checkpoint).
+ ///
+ public void PerformAction(Action action, string description)
+ {
+ action(_state);
+ SaveCheckpoint(description);
+ }
+
+ ///
+ /// Creates a checkpoint (snapshot) of the current game state.
+ ///
+ public void SaveCheckpoint(string? tag = null)
+ {
+ _history.Capture(_state);
+ }
+
+ ///
+ /// Undo to previous checkpoint.
+ ///
+ public bool UndoToCheckpoint()
+ {
+ if (!_history.Undo())
+ return false;
+
+ // Copy state directly from history's current state (no intermediate memento needed)
+ _state.PlayerX = _history.Current.PlayerX;
+ _state.PlayerY = _history.Current.PlayerY;
+ _state.Health = _history.Current.Health;
+ _state.Score = _history.Current.Score;
+ _state.Level = _history.Current.Level;
+
+ return true;
+ }
+
+ ///
+ /// Redo to next checkpoint.
+ ///
+ public bool RedoToCheckpoint()
+ {
+ if (!_history.Redo())
+ return false;
+
+ // Copy state directly from history's current state (no intermediate memento needed)
+ _state.PlayerX = _history.Current.PlayerX;
+ _state.PlayerY = _history.Current.PlayerY;
+ _state.Health = _history.Current.Health;
+ _state.Score = _history.Current.Score;
+ _state.Level = _history.Current.Level;
+
+ return true;
+ }
+ }
+
+ ///
+ /// Runs a demonstration of the game state with checkpoints and undo/redo.
+ ///
+ public static List Run()
+ {
+ var log = new List();
+ var session = new GameSession();
+
+ void LogState(string action)
+ {
+ log.Add($"{action}: {session.State}");
+ }
+
+ // Initial checkpoint
+ session.SaveCheckpoint("Game Start");
+ LogState("Game Start");
+
+ // Player moves and collects points
+ session.PerformAction(s =>
+ {
+ s.MovePlayer(5, 3);
+ s.AddScore(100);
+ }, "Move and collect coins");
+ LogState("Move (5,3) +100 points");
+
+ // Player takes damage
+ session.PerformAction(s =>
+ {
+ s.TakeDamage(30);
+ }, "Hit by enemy");
+ LogState("Take 30 damage");
+
+ // Player heals
+ session.PerformAction(s =>
+ {
+ s.Heal(20);
+ s.AddScore(50);
+ }, "Collect health pack");
+ LogState("Heal 20 +50 points");
+
+ // Advance to level 2
+ session.PerformAction(s =>
+ {
+ s.AdvanceLevel();
+ s.MovePlayer(-5, -3); // Reset position
+ }, "Level complete");
+ LogState("Advance to Level 2");
+
+ // Oops, made a mistake - undo
+ if (session.UndoToCheckpoint())
+ {
+ LogState("Undo (back before level 2)");
+ }
+
+ // Undo again
+ if (session.UndoToCheckpoint())
+ {
+ LogState("Undo (back before heal)");
+ }
+
+ // Redo
+ if (session.RedoToCheckpoint())
+ {
+ LogState("Redo (restore heal)");
+ }
+
+ // New action (clears redo history)
+ session.PerformAction(s =>
+ {
+ s.MovePlayer(10, 0);
+ s.AddScore(200);
+ }, "Different path taken");
+ LogState("New action (redo history cleared)");
+
+ // Try redo (should fail)
+ if (!session.RedoToCheckpoint())
+ {
+ log.Add("Redo failed (as expected - took different path)");
+ }
+
+ log.Add($"Final: {session.State}");
+ log.Add($"CanUndo: {session.CanUndo}, CanRedo: {session.CanRedo}");
+
+ return log;
+ }
+
+ ///
+ /// Demonstrates manual save/load functionality using mementos.
+ ///
+ public static List RunSaveLoad()
+ {
+ var log = new List();
+
+ var game = new GameState();
+ game.MovePlayer(10, 20);
+ game.AddScore(500);
+ game.AdvanceLevel();
+ log.Add($"Game state: {game}");
+
+ // Save game (capture memento)
+ var saveFile = GameStateMemento.Capture(in game);
+ log.Add($"Game saved (memento version {saveFile.MementoVersion})");
+
+ // Continue playing...
+ game.TakeDamage(50);
+ game.MovePlayer(5, 5);
+ log.Add($"After playing more: {game}");
+
+ // Load game (restore from memento)
+ saveFile.Restore(game); // In-place restore for mutable class
+ log.Add($"Game loaded: {game}");
+
+ return log;
+ }
+}
diff --git a/src/PatternKit.Examples/Generators/Memento/README.md b/src/PatternKit.Examples/Generators/Memento/README.md
new file mode 100644
index 0000000..55cad52
--- /dev/null
+++ b/src/PatternKit.Examples/Generators/Memento/README.md
@@ -0,0 +1,411 @@
+# Memento Pattern Source Generator
+
+The Memento pattern source generator provides a powerful, compile-time solution for capturing and restoring object state with full undo/redo support. It works seamlessly with **classes, structs, record classes, and record structs**.
+
+## Features
+
+- ✨ **Zero-boilerplate** memento generation
+- 🔄 **Undo/redo** support via optional caretaker
+- 📸 **Immutable snapshots** with deterministic versioning
+- 🎯 **Type-safe** restore operations
+- ⚡ **Compile-time** code generation (no reflection)
+- 🛡️ **Safe by default** with warnings for mutable reference captures
+- 🎨 **Flexible** member selection (include-all or explicit)
+
+## Quick Start
+
+### Basic Memento (Snapshot Only)
+
+```csharp
+using PatternKit.Generators;
+
+[Memento]
+public partial record class EditorState(string Text, int Cursor);
+```
+
+**Generated Code:**
+```csharp
+public readonly partial struct EditorStateMemento
+{
+ public int MementoVersion => 12345678;
+ public string Text { get; }
+ public int Cursor { get; }
+
+ public static EditorStateMemento Capture(in EditorState originator);
+ public EditorState RestoreNew();
+}
+```
+
+**Usage:**
+```csharp
+var state = new EditorState("Hello", 5);
+var memento = EditorStateMemento.Capture(in state);
+
+// Later...
+var restored = memento.RestoreNew();
+```
+
+### With Undo/Redo Caretaker
+
+```csharp
+[Memento(GenerateCaretaker = true, Capacity = 100)]
+public partial record class EditorState(string Text, int Cursor);
+```
+
+**Generated Caretaker:**
+```csharp
+public sealed partial class EditorStateHistory
+{
+ public int Count { get; }
+ public bool CanUndo { get; }
+ public bool CanRedo { get; }
+ public EditorState Current { get; }
+
+ public EditorStateHistory(EditorState initial);
+ public void Capture(EditorState state);
+ public bool Undo();
+ public bool Redo();
+ public void Clear(EditorState initial);
+}
+```
+
+**Usage:**
+```csharp
+var history = new EditorStateHistory(new EditorState("", 0));
+
+history.Capture(new EditorState("Hello", 5));
+history.Capture(new EditorState("Hello World", 11));
+
+if (history.CanUndo)
+{
+ history.Undo(); // Back to "Hello"
+ var current = history.Current;
+}
+
+if (history.CanRedo)
+{
+ history.Redo(); // Forward to "Hello World"
+}
+```
+
+## Supported Types
+
+### Record Class (Immutable-Friendly)
+
+```csharp
+[Memento(GenerateCaretaker = true)]
+public partial record class EditorState(string Text, int Cursor);
+```
+
+- Primary restore method: `RestoreNew()` (returns new instance)
+- Caretaker stores state instances
+- Perfect for immutable design
+
+### Record Struct
+
+```csharp
+[Memento]
+public partial record struct Point(int X, int Y);
+```
+
+- Value semantics with snapshot support
+- Efficient for small state objects
+
+### Class (Mutable)
+
+```csharp
+[Memento(GenerateCaretaker = true)]
+public partial class GameState
+{
+ public int Health { get; set; }
+ public int Score { get; set; }
+}
+```
+
+- Supports both `Restore(originator)` (in-place) and `RestoreNew()`
+- Useful for large, complex state objects
+
+### Struct (Mutable)
+
+```csharp
+[Memento]
+public partial struct Counter
+{
+ public int Value { get; set; }
+}
+```
+
+- Efficient value-type snapshots
+
+## Configuration Options
+
+### Attribute Parameters
+
+```csharp
+[Memento(
+ GenerateCaretaker = true, // Generate undo/redo caretaker
+ Capacity = 100, // Max history entries (0 = unlimited)
+ InclusionMode = MementoInclusionMode.IncludeAll, // or ExplicitOnly
+ SkipDuplicates = true // Skip consecutive equal states
+)]
+public partial record class MyState(...);
+```
+
+### Member Selection
+
+#### Include All (Default)
+
+```csharp
+[Memento]
+public partial class Document
+{
+ public string Text { get; set; } // ✓ Included
+
+ [MementoIgnore]
+ public string TempData { get; set; } // ✗ Excluded
+}
+```
+
+#### Explicit Only
+
+```csharp
+[Memento(InclusionMode = MementoInclusionMode.ExplicitOnly)]
+public partial class Document
+{
+ [MementoInclude]
+ public string Text { get; set; } // ✓ Included
+
+ public string InternalId { get; set; } // ✗ Excluded
+}
+```
+
+### Capture Strategies
+
+```csharp
+[Memento]
+public partial class Document
+{
+ public string Text { get; set; } // Safe (immutable)
+
+ [MementoStrategy(MementoCaptureStrategy.ByReference)]
+ public List Tags { get; set; } // ⚠️ Warning: mutable reference
+}
+```
+
+**Available Strategies:**
+- `ByReference` - Shallow copy (safe for value types and strings)
+- `Clone` - Deep clone via ICloneable or with-expression
+- `DeepCopy` - Generator-emitted deep copy
+- `Custom` - User-provided custom capture logic
+
+## Caretaker Behavior
+
+### Undo/Redo Semantics
+
+```csharp
+var history = new EditorStateHistory(initial);
+
+history.Capture(state1); // [initial, state1] cursor=1
+history.Capture(state2); // [initial, state1, state2] cursor=2
+
+history.Undo(); // [initial, state1, state2] cursor=1
+history.Undo(); // [initial, state1, state2] cursor=0
+
+history.Redo(); // [initial, state1, state2] cursor=1
+
+history.Capture(state3); // [initial, state1, state3] cursor=2 (state2 removed)
+```
+
+### Capacity Management
+
+```csharp
+[Memento(GenerateCaretaker = true, Capacity = 3)]
+public partial record class State(int Value);
+```
+
+When capacity is exceeded, the **oldest** state is evicted (FIFO):
+
+```csharp
+var history = new StateHistory(s0);
+
+history.Capture(s1); // [s0, s1]
+history.Capture(s2); // [s0, s1, s2]
+history.Capture(s3); // [s0, s1, s2, s3] - over capacity!
+ // [s1, s2, s3] - s0 evicted
+```
+
+### Duplicate Suppression
+
+```csharp
+[Memento(GenerateCaretaker = true, SkipDuplicates = true)]
+public partial record class State(int Value);
+```
+
+Consecutive equal states (by value equality) are automatically skipped:
+
+```csharp
+var history = new StateHistory(new State(0));
+
+history.Capture(new State(0)); // Skipped (duplicate)
+history.Capture(new State(1)); // Added
+history.Capture(new State(1)); // Skipped (duplicate)
+// History: [State(0), State(1)]
+```
+
+## Diagnostics
+
+The generator provides comprehensive diagnostics:
+
+| ID | Severity | Description |
+|----|----------|-------------|
+| **PKMEM001** | Error | Type must be `partial` |
+| **PKMEM002** | Warning | Member inaccessible for capture/restore |
+| **PKMEM003** | Warning | Unsafe reference capture (mutable reference) |
+| **PKMEM004** | Error | Clone strategy missing mechanism |
+| **PKMEM005** | Error | Record restore generation failed |
+| **PKMEM006** | Info | Init-only restrictions prevent in-place restore |
+
+## Real-World Examples
+
+### Text Editor with Undo/Redo
+
+```csharp
+[Memento(GenerateCaretaker = true, Capacity = 100, SkipDuplicates = true)]
+public partial record class EditorState(string Text, int Cursor, int SelectionLength)
+{
+ public EditorState Insert(string text) { /* ... */ }
+ public EditorState Backspace() { /* ... */ }
+}
+
+var editor = new EditorStateHistory(EditorState.Empty);
+
+// Edit operations
+editor.Capture(state.Insert("Hello"));
+editor.Capture(state.Insert(" World"));
+
+// Undo/Redo
+editor.Undo(); // Back to "Hello"
+editor.Redo(); // Forward to "Hello World"
+```
+
+### Game Save/Load System
+
+```csharp
+[Memento]
+public partial class GameState
+{
+ public int PlayerX { get; set; }
+ public int PlayerY { get; set; }
+ public int Health { get; set; }
+ public int Score { get; set; }
+}
+
+// Save game
+var saveFile = GameStateMemento.Capture(in gameState);
+File.WriteAllBytes("save.dat", Serialize(saveFile));
+
+// Load game
+var saveFile = Deserialize(File.ReadAllBytes("save.dat"));
+saveFile.Restore(gameState); // In-place restore for mutable class
+```
+
+### Configuration Snapshots
+
+```csharp
+[Memento(InclusionMode = MementoInclusionMode.ExplicitOnly)]
+public partial class AppConfig
+{
+ [MementoInclude]
+ public string ApiEndpoint { get; set; }
+
+ [MementoInclude]
+ public int Timeout { get; set; }
+
+ // Not included in snapshots
+ public string RuntimeToken { get; set; }
+}
+
+// Capture configuration
+var backup = AppConfigMemento.Capture(in config);
+
+// Restore if validation fails
+if (!ValidateConfig(config))
+{
+ config = backup.RestoreNew();
+}
+```
+
+## Best Practices
+
+### 1. Use Records for Immutable State
+
+```csharp
+// ✓ Good: Immutable record
+[Memento(GenerateCaretaker = true)]
+public partial record class State(string Value);
+
+// ✗ Avoid: Mutable class when records would work
+[Memento(GenerateCaretaker = true)]
+public partial class State
+{
+ public string Value { get; set; }
+}
+```
+
+### 2. Be Explicit About Mutable References
+
+```csharp
+[Memento]
+public partial class Document
+{
+ // ✓ Good: Explicitly acknowledge the strategy
+ [MementoStrategy(MementoCaptureStrategy.ByReference)]
+ public List Tags { get; set; }
+}
+```
+
+### 3. Exclude Transient State
+
+```csharp
+[Memento]
+public partial class Editor
+{
+ public string Text { get; set; }
+
+ // ✓ Good: Exclude runtime-only state
+ [MementoIgnore]
+ public bool IsDirty { get; set; }
+}
+```
+
+### 4. Set Appropriate Capacity
+
+```csharp
+// ✓ Good: Reasonable capacity for undo/redo
+[Memento(GenerateCaretaker = true, Capacity = 100)]
+
+// ✗ Avoid: Unlimited capacity for large states
+[Memento(GenerateCaretaker = true, Capacity = 0)] // Can cause memory issues
+```
+
+## Performance Considerations
+
+- **Memento capture**: O(n) where n = number of members
+- **Caretaker undo/redo**: O(1)
+- **Capacity eviction**: O(1) (removes oldest)
+- **Memory**: Each snapshot stores a complete copy of included members
+
+For large objects with frequent snapshots, consider:
+- Using `[MementoIgnore]` to exclude large, reconstructible data
+- Setting a reasonable `Capacity`
+- Using value types (structs/record structs) when appropriate
+
+## See Also
+
+- [EditorStateDemo.cs](./EditorStateDemo.cs) - Full text editor example
+- [GameStateDemo.cs](./GameStateDemo.cs) - Game state with save/load
+- [PatternKit.Behavioral.Memento](../../../Core/Behavioral/Memento/) - Runtime memento implementation
+
+## License
+
+MIT License - see [LICENSE](../../../../../../LICENSE) for details.
diff --git a/src/PatternKit.Generators.Abstractions/MementoAttribute.cs b/src/PatternKit.Generators.Abstractions/MementoAttribute.cs
new file mode 100644
index 0000000..c94dbf5
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/MementoAttribute.cs
@@ -0,0 +1,117 @@
+namespace PatternKit.Generators;
+
+///
+/// Marks a type (class/struct/record class/record struct) for Memento pattern code generation.
+/// Generates an immutable memento struct for capturing and restoring state snapshots,
+/// with optional undo/redo caretaker history management.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class MementoAttribute : Attribute
+{
+ ///
+ /// When true, generates a caretaker class for undo/redo history management.
+ /// Default is false (generates only the memento struct).
+ ///
+ public bool GenerateCaretaker { get; set; }
+
+ ///
+ /// Maximum number of snapshots to retain in the caretaker history.
+ /// When the limit is exceeded, the oldest snapshot is evicted (FIFO).
+ /// Default is 0 (unbounded). Only applies when GenerateCaretaker is true.
+ ///
+ public int Capacity { get; set; }
+
+ ///
+ /// Member selection mode for the memento.
+ /// Default is IncludeAll (all public instance properties/fields with getters).
+ ///
+ public MementoInclusionMode InclusionMode { get; set; } = MementoInclusionMode.IncludeAll;
+
+ ///
+ /// When true, the generated caretaker will skip capturing duplicate states
+ /// (states that are equal according to value equality).
+ /// Default is true. Only applies when GenerateCaretaker is true.
+ ///
+ public bool SkipDuplicates { get; set; } = true;
+}
+
+///
+/// Determines how members are selected for inclusion in the memento.
+///
+public enum MementoInclusionMode
+{
+ ///
+ /// Include all eligible public instance properties and fields with getters,
+ /// except those marked with [MementoIgnore].
+ ///
+ IncludeAll = 0,
+
+ ///
+ /// Include only members explicitly marked with [MementoInclude].
+ ///
+ ExplicitOnly = 1
+}
+
+///
+/// Marks a member to be excluded from the generated memento.
+/// Only applies when InclusionMode is IncludeAll.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class MementoIgnoreAttribute : Attribute
+{
+}
+
+///
+/// Marks a member to be explicitly included in the generated memento.
+/// Only applies when InclusionMode is ExplicitOnly.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class MementoIncludeAttribute : Attribute
+{
+}
+
+///
+/// Specifies the capture strategy for a member in the memento.
+/// Determines how the member value is copied when creating a snapshot.
+///
+[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false, Inherited = false)]
+public sealed class MementoStrategyAttribute : Attribute
+{
+ public MementoCaptureStrategy Strategy { get; }
+
+ public MementoStrategyAttribute(MementoCaptureStrategy strategy)
+ {
+ Strategy = strategy;
+ }
+}
+
+///
+/// Defines how a member's value is captured in the memento snapshot.
+///
+public enum MementoCaptureStrategy
+{
+ ///
+ /// Capture the reference as-is (shallow copy).
+ /// Safe for immutable types and value types.
+ /// WARNING: For mutable reference types, mutations will affect all snapshots.
+ ///
+ ByReference = 0,
+
+ ///
+ /// Clone the value using a known mechanism (ICloneable, record with-expression, etc.).
+ /// Generator emits an error if no suitable clone mechanism is available.
+ ///
+ Clone = 1,
+
+ ///
+ /// Perform a deep copy of the member value.
+ /// Only available when the generator can safely emit deep copy logic.
+ ///
+ DeepCopy = 2,
+
+ ///
+ /// Use a custom capture mechanism provided by the user.
+ /// Requires the user to implement a partial method for custom capture logic.
+ ///
+ Custom = 3
+}
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index b7e870e..90f116b 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -31,3 +31,9 @@ BP002 | PatternKit.Builders | Error | Diagnostics
BP003 | PatternKit.Builders | Error | Diagnostics
BA001 | PatternKit.Builders | Warning | Diagnostics
BA002 | PatternKit.Builders | Warning | Diagnostics
+PKMEM001 | PatternKit.Generators.Memento | Error | Type marked with [Memento] must be partial
+PKMEM002 | PatternKit.Generators.Memento | Warning | Member is inaccessible for memento capture or restore
+PKMEM003 | PatternKit.Generators.Memento | Warning | Unsafe reference capture
+PKMEM004 | PatternKit.Generators.Memento | Error | Clone strategy requested but mechanism missing
+PKMEM005 | PatternKit.Generators.Memento | Error | Record restore generation failed
+PKMEM006 | PatternKit.Generators.Memento | Info | Init-only or readonly restrictions prevent in-place restore
diff --git a/src/PatternKit.Generators/MementoGenerator.cs b/src/PatternKit.Generators/MementoGenerator.cs
new file mode 100644
index 0000000..bb89631
--- /dev/null
+++ b/src/PatternKit.Generators/MementoGenerator.cs
@@ -0,0 +1,692 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Text;
+
+namespace PatternKit.Generators;
+
+///
+/// Source generator for the Memento pattern.
+/// Generates immutable memento structs and optional caretaker classes for undo/redo history.
+///
+[Generator]
+public sealed class MementoGenerator : IIncrementalGenerator
+{
+ // Diagnostic IDs
+ private const string DiagIdTypeNotPartial = "PKMEM001";
+ private const string DiagIdInaccessibleMember = "PKMEM002";
+ private const string DiagIdUnsafeReferenceCapture = "PKMEM003";
+ private const string DiagIdCloneMechanismMissing = "PKMEM004";
+ private const string DiagIdRecordRestoreFailed = "PKMEM005";
+ private const string DiagIdInitOnlyRestriction = "PKMEM006";
+
+ private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
+ id: DiagIdTypeNotPartial,
+ title: "Type marked with [Memento] must be partial",
+ messageFormat: "Type '{0}' is marked with [Memento] but is not declared as partial. Add the 'partial' keyword to the type declaration.",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InaccessibleMemberDescriptor = new(
+ id: DiagIdInaccessibleMember,
+ title: "Member is inaccessible for memento capture or restore",
+ messageFormat: "Member '{0}' cannot be accessed for memento operations. Ensure the member has appropriate accessibility.",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor UnsafeReferenceCaptureDescriptor = new(
+ id: DiagIdUnsafeReferenceCapture,
+ title: "Unsafe reference capture",
+ messageFormat: "Member '{0}' is a mutable reference type captured by reference. Mutations will affect all snapshots. Consider using [MementoStrategy(Clone)] or [MementoStrategy(DeepCopy)].",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor CloneMechanismMissingDescriptor = new(
+ id: DiagIdCloneMechanismMissing,
+ title: "Clone strategy requested but mechanism missing",
+ messageFormat: "Member '{0}' has [MementoStrategy(Clone)] but no suitable clone mechanism is available. Implement ICloneable or provide a custom cloner.",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor RecordRestoreFailedDescriptor = new(
+ id: DiagIdRecordRestoreFailed,
+ title: "Record restore generation failed",
+ messageFormat: "Cannot generate RestoreNew for record type '{0}'. No accessible constructor or with-expression path is viable. Ensure the record has an accessible primary constructor.",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InitOnlyRestrictionDescriptor = new(
+ id: DiagIdInitOnlyRestriction,
+ title: "Init-only or readonly restrictions prevent in-place restore",
+ messageFormat: "Member '{0}' is init-only or readonly, preventing in-place restore. Only RestoreNew() will be generated for this type.",
+ category: "PatternKit.Generators.Memento",
+ defaultSeverity: DiagnosticSeverity.Info,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all type declarations with [Memento] attribute
+ var mementoTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.MementoAttribute",
+ predicate: static (node, _) => node is TypeDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate for each type
+ context.RegisterSourceOutput(mementoTypes, (spc, typeContext) =>
+ {
+ if (typeContext.TargetSymbol is not INamedTypeSymbol typeSymbol)
+ return;
+
+ var attr = typeContext.Attributes.FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.MementoAttribute");
+ if (attr is null)
+ return;
+
+ GenerateMementoForType(spc, typeSymbol, attr, typeContext.TargetNode);
+ });
+ }
+
+ private void GenerateMementoForType(
+ SourceProductionContext context,
+ INamedTypeSymbol typeSymbol,
+ AttributeData attribute,
+ SyntaxNode node)
+ {
+ // Check if type is partial
+ if (!IsPartialType(node))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ TypeNotPartialDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Parse attribute arguments
+ var config = ParseMementoConfig(attribute);
+
+ // Analyze type and members
+ var typeInfo = AnalyzeType(typeSymbol, config, context);
+ if (typeInfo is null)
+ return;
+
+ // Generate memento struct
+ var mementoSource = GenerateMementoStruct(typeInfo, context);
+ if (!string.IsNullOrEmpty(mementoSource))
+ {
+ var fileName = $"{typeSymbol.Name}.Memento.g.cs";
+ context.AddSource(fileName, mementoSource);
+ }
+
+ // Generate caretaker if requested
+ if (config.GenerateCaretaker)
+ {
+ var caretakerSource = GenerateCaretaker(typeInfo, config, context);
+ if (!string.IsNullOrEmpty(caretakerSource))
+ {
+ var fileName = $"{typeSymbol.Name}.History.g.cs";
+ context.AddSource(fileName, caretakerSource);
+ }
+ }
+ }
+
+ private static bool IsPartialType(SyntaxNode node)
+ {
+ return node switch
+ {
+ ClassDeclarationSyntax classDecl => classDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ StructDeclarationSyntax structDecl => structDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ RecordDeclarationSyntax recordDecl => recordDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ _ => false
+ };
+ }
+
+ private MementoConfig ParseMementoConfig(AttributeData attribute)
+ {
+ var config = new MementoConfig();
+
+ foreach (var named in attribute.NamedArguments)
+ {
+ switch (named.Key)
+ {
+ case nameof(MementoAttribute.GenerateCaretaker):
+ config.GenerateCaretaker = (bool)named.Value.Value!;
+ break;
+ case nameof(MementoAttribute.Capacity):
+ config.Capacity = (int)named.Value.Value!;
+ break;
+ case nameof(MementoAttribute.InclusionMode):
+ config.InclusionMode = (int)named.Value.Value!;
+ break;
+ case nameof(MementoAttribute.SkipDuplicates):
+ config.SkipDuplicates = (bool)named.Value.Value!;
+ break;
+ }
+ }
+
+ return config;
+ }
+
+ private TypeInfo? AnalyzeType(
+ INamedTypeSymbol typeSymbol,
+ MementoConfig config,
+ SourceProductionContext context)
+ {
+ var typeInfo = new TypeInfo
+ {
+ TypeSymbol = typeSymbol,
+ TypeName = typeSymbol.Name,
+ Namespace = typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? string.Empty
+ : typeSymbol.ContainingNamespace.ToDisplayString(),
+ IsClass = typeSymbol.TypeKind == TypeKind.Class && !typeSymbol.IsRecord,
+ IsStruct = typeSymbol.TypeKind == TypeKind.Struct && !typeSymbol.IsRecord,
+ IsRecordClass = typeSymbol.TypeKind == TypeKind.Class && typeSymbol.IsRecord,
+ IsRecordStruct = typeSymbol.TypeKind == TypeKind.Struct && typeSymbol.IsRecord,
+ Members = new List()
+ };
+
+ // Collect members based on inclusion mode
+ var members = GetMembersForMemento(typeSymbol, config, context);
+ typeInfo.Members.AddRange(members);
+
+ if (typeInfo.Members.Count == 0)
+ {
+ // No members to capture - this might be intentional, but warn
+ return typeInfo;
+ }
+
+ return typeInfo;
+ }
+
+ private List GetMembersForMemento(
+ INamedTypeSymbol typeSymbol,
+ MementoConfig config,
+ SourceProductionContext context)
+ {
+ var members = new List();
+ var includeAll = config.InclusionMode == 0; // IncludeAll
+
+ // Filter to only public instance properties and fields
+ var candidateMembers = typeSymbol.GetMembers()
+ .Where(m => (m is IPropertySymbol || m is IFieldSymbol) &&
+ !m.IsStatic &&
+ m.DeclaredAccessibility == Accessibility.Public);
+
+ foreach (var member in candidateMembers)
+ {
+ // Check for attributes
+ var hasIgnore = HasAttribute(member, "PatternKit.Generators.MementoIgnoreAttribute");
+ var hasInclude = HasAttribute(member, "PatternKit.Generators.MementoIncludeAttribute");
+ var strategyAttr = GetAttribute(member, "PatternKit.Generators.MementoStrategyAttribute");
+
+ // Determine if this member should be included
+ bool shouldInclude = includeAll ? !hasIgnore : hasInclude;
+ if (!shouldInclude)
+ continue;
+
+ // Ensure member has a getter
+ ITypeSymbol? memberType = null;
+ bool isReadOnly = false;
+ bool isInitOnly = false;
+
+ if (member is IPropertySymbol prop)
+ {
+ if (prop.GetMethod is null || prop.GetMethod.DeclaredAccessibility != Accessibility.Public)
+ continue;
+
+ // Skip properties that cannot be restored:
+ // - computed properties (no setter)
+ // - init-only properties on non-record types
+ if (prop.SetMethod is null || (prop.SetMethod.IsInitOnly && !typeSymbol.IsRecord))
+ continue;
+
+ memberType = prop.Type;
+ isReadOnly = prop.SetMethod is null;
+ isInitOnly = prop.SetMethod?.IsInitOnly ?? false;
+ }
+ else if (member is IFieldSymbol fld)
+ {
+ memberType = fld.Type;
+ isReadOnly = fld.IsReadOnly;
+ }
+
+ if (memberType is null)
+ continue;
+
+ // Determine capture strategy
+ var strategy = DetermineCaptureStrategy(member, memberType, strategyAttr, context);
+
+ members.Add(new MemberInfo
+ {
+ Name = member.Name,
+ Type = memberType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ TypeSymbol = memberType,
+ IsProperty = member is IPropertySymbol,
+ IsField = member is IFieldSymbol,
+ IsReadOnly = isReadOnly,
+ IsInitOnly = isInitOnly,
+ CaptureStrategy = strategy
+ });
+ }
+
+ return members;
+ }
+
+ private int DetermineCaptureStrategy(
+ ISymbol member,
+ ITypeSymbol memberType,
+ AttributeData? strategyAttr,
+ SourceProductionContext context)
+ {
+ // If explicit strategy provided, use it
+ if (strategyAttr is not null)
+ {
+ var ctorArg = strategyAttr.ConstructorArguments.FirstOrDefault();
+ if (ctorArg.Value is int strategyValue)
+ return strategyValue;
+ }
+
+ // Otherwise, infer safe default
+ // Value types and string: ByReference (safe)
+ if (memberType.IsValueType || memberType.SpecialType == SpecialType.System_String)
+ return 0; // ByReference
+
+ // Reference types: warn and default to ByReference
+ context.ReportDiagnostic(Diagnostic.Create(
+ UnsafeReferenceCaptureDescriptor,
+ member.Locations.FirstOrDefault(),
+ member.Name));
+
+ return 0; // ByReference (with warning)
+ }
+
+ private static bool HasAttribute(ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().Any(a =>
+ a.AttributeClass?.ToDisplayString() == attributeName);
+ }
+
+ private static AttributeData? GetAttribute(ISymbol symbol, string attributeName)
+ {
+ return symbol.GetAttributes().FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == attributeName);
+ }
+
+ private string GenerateMementoStruct(TypeInfo typeInfo, SourceProductionContext context)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+
+ // Only add namespace declaration if not in global namespace
+ if (!string.IsNullOrEmpty(typeInfo.Namespace))
+ {
+ sb.AppendLine($"namespace {typeInfo.Namespace};");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine($"public readonly partial struct {typeInfo.TypeName}Memento");
+ sb.AppendLine("{");
+
+ // Version field (use computed hash to avoid conflicts)
+ sb.AppendLine(" /// Memento version for compatibility checking.");
+ sb.AppendLine($" public int MementoVersion => {ComputeVersionHash(typeInfo)};");
+ sb.AppendLine();
+
+ // Member properties
+ foreach (var member in typeInfo.Members)
+ {
+ sb.AppendLine($" public {member.Type} {member.Name} {{ get; }}");
+ }
+ sb.AppendLine();
+
+ // Constructor
+ sb.Append($" private {typeInfo.TypeName}Memento(");
+ sb.Append(string.Join(", ", typeInfo.Members.Select(m => $"{m.Type} {ToCamelCase(m.Name)}")));
+ sb.AppendLine(")");
+ sb.AppendLine(" {");
+ foreach (var member in typeInfo.Members)
+ {
+ sb.AppendLine($" {member.Name} = {ToCamelCase(member.Name)};");
+ }
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Capture method
+ GenerateCaptureMethod(sb, typeInfo);
+ sb.AppendLine();
+
+ // Restore methods
+ GenerateRestoreMethods(sb, typeInfo, context);
+
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private void GenerateCaptureMethod(StringBuilder sb, TypeInfo typeInfo)
+ {
+ sb.AppendLine($" /// Captures the current state of the originator as an immutable memento.");
+ sb.AppendLine($" public static {typeInfo.TypeName}Memento Capture(in {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} originator)");
+ sb.AppendLine(" {");
+ sb.Append($" return new {typeInfo.TypeName}Memento(");
+ sb.Append(string.Join(", ", typeInfo.Members.Select(m => $"originator.{m.Name}")));
+ sb.AppendLine(");");
+ sb.AppendLine(" }");
+ }
+
+ private void GenerateRestoreMethods(StringBuilder sb, TypeInfo typeInfo, SourceProductionContext context)
+ {
+ // Always generate RestoreNew for all types
+ GenerateRestoreNewMethod(sb, typeInfo, context);
+
+ // For mutable types (non-record or record with setters), also generate in-place Restore
+ bool hasMutableMembers = typeInfo.Members.Any(m => !m.IsReadOnly && !m.IsInitOnly);
+ if (!typeInfo.IsRecordClass && !typeInfo.IsRecordStruct && hasMutableMembers)
+ {
+ sb.AppendLine();
+ GenerateInPlaceRestoreMethod(sb, typeInfo);
+ }
+ }
+
+ private void GenerateRestoreNewMethod(StringBuilder sb, TypeInfo typeInfo, SourceProductionContext context)
+ {
+ sb.AppendLine();
+ sb.AppendLine($" /// Restores the memento state by creating a new instance.");
+ sb.AppendLine($" public {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} RestoreNew()");
+ sb.AppendLine(" {");
+
+ if (typeInfo.IsRecordClass || typeInfo.IsRecordStruct)
+ {
+ // For records, try using positional constructor if parameters match members
+ // For now, use simple object initializer which works with records
+ sb.Append($" return new {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
+
+ // Try to use positional constructor
+ if (typeInfo.Members.Count > 0)
+ {
+ // Check if we can use positional parameters (primary constructor)
+ // Verify parameter names and types match the members
+ var primaryCtor = typeInfo.TypeSymbol.Constructors.FirstOrDefault(c =>
+ {
+ if (c.Parameters.Length != typeInfo.Members.Count)
+ return false;
+
+ // Check if parameter names and types match members (case-insensitive for names)
+ for (int i = 0; i < c.Parameters.Length; i++)
+ {
+ var param = c.Parameters[i];
+ var member = typeInfo.Members.FirstOrDefault(m =>
+ string.Equals(m.Name, param.Name, StringComparison.OrdinalIgnoreCase) &&
+ SymbolEqualityComparer.Default.Equals(m.TypeSymbol, param.Type));
+
+ if (member is null)
+ return false;
+ }
+
+ return true;
+ });
+
+ if (primaryCtor is not null)
+ {
+ // Use positional constructor, ordered by parameter order
+ sb.Append("(");
+ var orderedMembers = primaryCtor.Parameters
+ .Select(p => typeInfo.Members.First(m =>
+ string.Equals(m.Name, p.Name, StringComparison.OrdinalIgnoreCase)))
+ .ToList();
+ sb.Append(string.Join(", ", orderedMembers.Select(m => $"this.{m.Name}")));
+ sb.AppendLine(");");
+ }
+ else
+ {
+ // Fall back to object initializer
+ sb.AppendLine("()");
+ sb.AppendLine(" {");
+ foreach (var member in typeInfo.Members)
+ {
+ sb.AppendLine($" {member.Name} = this.{member.Name},");
+ }
+ sb.AppendLine(" };");
+ }
+ }
+ else
+ {
+ sb.AppendLine("();");
+ }
+ }
+ else
+ {
+ // For classes/structs, use object initializer (only include settable members)
+ sb.Append($" return new {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}");
+ var settableMembers = typeInfo.Members.Where(m => !m.IsReadOnly && !m.IsInitOnly).ToList();
+ if (settableMembers.Count > 0)
+ {
+ sb.AppendLine("()");
+ sb.AppendLine(" {");
+ foreach (var member in settableMembers)
+ {
+ sb.AppendLine($" {member.Name} = this.{member.Name},");
+ }
+ sb.AppendLine(" };");
+ }
+ else
+ {
+ sb.AppendLine("();");
+ }
+ }
+
+ sb.AppendLine(" }");
+ }
+
+ private void GenerateInPlaceRestoreMethod(StringBuilder sb, TypeInfo typeInfo)
+ {
+ sb.AppendLine($" /// Restores the memento state to an existing originator instance (in-place).");
+ sb.AppendLine($" public void Restore({typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} originator)");
+ sb.AppendLine(" {");
+
+ // Filter to only settable members
+ var settableMembers = typeInfo.Members.Where(m => !m.IsReadOnly && !m.IsInitOnly);
+ foreach (var member in settableMembers)
+ {
+ sb.AppendLine($" originator.{member.Name} = this.{member.Name};");
+ }
+
+ sb.AppendLine(" }");
+ }
+
+ private string GenerateCaretaker(TypeInfo typeInfo, MementoConfig config, SourceProductionContext context)
+ {
+ var sb = new StringBuilder();
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("// ");
+ sb.AppendLine();
+
+ // Only add namespace declaration if not in global namespace
+ if (!string.IsNullOrEmpty(typeInfo.Namespace))
+ {
+ sb.AppendLine($"namespace {typeInfo.Namespace};");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine($"/// Manages undo/redo history for {typeInfo.TypeName}.");
+ sb.AppendLine($"public sealed partial class {typeInfo.TypeName}History");
+ sb.AppendLine("{");
+
+ // Fields
+ sb.AppendLine($" private readonly System.Collections.Generic.List<{typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}> _states = new();");
+ sb.AppendLine(" private int _currentIndex = -1;");
+ if (config.Capacity > 0)
+ {
+ sb.AppendLine($" private const int MaxCapacity = {config.Capacity};");
+ }
+ sb.AppendLine();
+
+ // Properties
+ sb.AppendLine(" /// Total number of states in history.");
+ sb.AppendLine(" public int Count => _states.Count;");
+ sb.AppendLine();
+ sb.AppendLine(" /// True if undo is possible.");
+ sb.AppendLine(" public bool CanUndo => _currentIndex > 0;");
+ sb.AppendLine();
+ sb.AppendLine(" /// True if redo is possible.");
+ sb.AppendLine(" public bool CanRedo => _currentIndex >= 0 && _currentIndex < _states.Count - 1;");
+ sb.AppendLine();
+ sb.AppendLine(" /// Current state (or default if empty).");
+ sb.AppendLine($" public {typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} Current");
+ sb.AppendLine(" {");
+ sb.AppendLine(" get => _currentIndex >= 0 ? _states[_currentIndex] : default!;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Constructor
+ sb.AppendLine($" public {typeInfo.TypeName}History({typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} initial)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _states.Add(initial);");
+ sb.AppendLine(" _currentIndex = 0;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Capture method
+ sb.AppendLine($" /// Captures a new state, truncating forward history if not at the end.");
+ sb.AppendLine($" public void Capture({typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} state)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" // Truncate forward history if we're not at the end");
+ sb.AppendLine(" if (_currentIndex < _states.Count - 1)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _states.RemoveRange(_currentIndex + 1, _states.Count - _currentIndex - 1);");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ if (config.SkipDuplicates)
+ {
+ sb.AppendLine(" // Skip duplicates");
+ sb.AppendLine(" if (_currentIndex >= 0 && System.Collections.Generic.EqualityComparer<" + typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + ">.Default.Equals(_states[_currentIndex], state))");
+ sb.AppendLine(" return;");
+ sb.AppendLine();
+ }
+
+ sb.AppendLine(" _states.Add(state);");
+ sb.AppendLine(" _currentIndex++;");
+ sb.AppendLine();
+
+ if (config.Capacity > 0)
+ {
+ sb.AppendLine(" // Evict oldest if over capacity");
+ sb.AppendLine(" if (_states.Count > MaxCapacity)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _states.RemoveAt(0);");
+ sb.AppendLine(" _currentIndex--;");
+ sb.AppendLine(" }");
+ }
+
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Undo method
+ sb.AppendLine(" /// Moves back to the previous state.");
+ sb.AppendLine(" public bool Undo()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (!CanUndo) return false;");
+ sb.AppendLine(" _currentIndex--;");
+ sb.AppendLine(" return true;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Redo method
+ sb.AppendLine(" /// Moves forward to the next state.");
+ sb.AppendLine(" public bool Redo()");
+ sb.AppendLine(" {");
+ sb.AppendLine(" if (!CanRedo) return false;");
+ sb.AppendLine(" _currentIndex++;");
+ sb.AppendLine(" return true;");
+ sb.AppendLine(" }");
+ sb.AppendLine();
+
+ // Clear method
+ sb.AppendLine(" /// Clears all history and resets to initial state.");
+ sb.AppendLine($" public void Clear({typeInfo.TypeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)} initial)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" _states.Clear();");
+ sb.AppendLine(" _states.Add(initial);");
+ sb.AppendLine(" _currentIndex = 0;");
+ sb.AppendLine(" }");
+
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private static int ComputeVersionHash(TypeInfo typeInfo)
+ {
+ // Simple deterministic hash based on member names and types
+ // Using FNV-1a hash algorithm for compatibility with netstandard2.0
+ unchecked
+ {
+ const int FnvPrime = 16777619;
+ int hash = (int)2166136261;
+
+ foreach (var member in typeInfo.Members.OrderBy(m => m.Name))
+ {
+ foreach (char c in member.Name)
+ {
+ hash = (hash ^ c) * FnvPrime;
+ }
+ foreach (char c in member.Type)
+ {
+ hash = (hash ^ c) * FnvPrime;
+ }
+ }
+
+ return hash;
+ }
+ }
+
+ private static string ToCamelCase(string name)
+ {
+ if (string.IsNullOrEmpty(name) || name.Length == 1 || char.IsLower(name[0]))
+ return name;
+ return char.ToLowerInvariant(name[0]) + name.Substring(1);
+ }
+
+ // Helper classes
+ private class MementoConfig
+ {
+ public bool GenerateCaretaker { get; set; }
+ public int Capacity { get; set; }
+ public int InclusionMode { get; set; }
+ public bool SkipDuplicates { get; set; } = true;
+ }
+
+ private class TypeInfo
+ {
+ public INamedTypeSymbol TypeSymbol { get; set; } = null!;
+ public string TypeName { get; set; } = "";
+ public string Namespace { get; set; } = "";
+ public bool IsClass { get; set; }
+ public bool IsStruct { get; set; }
+ public bool IsRecordClass { get; set; }
+ public bool IsRecordStruct { get; set; }
+ public List Members { get; set; } = new();
+ }
+
+ private class MemberInfo
+ {
+ public string Name { get; set; } = "";
+ public string Type { get; set; } = "";
+ public ITypeSymbol TypeSymbol { get; set; } = null!;
+ public bool IsProperty { get; set; }
+ public bool IsField { get; set; }
+ public bool IsReadOnly { get; set; }
+ public bool IsInitOnly { get; set; }
+ public int CaptureStrategy { get; set; }
+ }
+}
diff --git a/test/PatternKit.Generators.Tests/MementoGeneratorTests.cs b/test/PatternKit.Generators.Tests/MementoGeneratorTests.cs
new file mode 100644
index 0000000..52d6f19
--- /dev/null
+++ b/test/PatternKit.Generators.Tests/MementoGeneratorTests.cs
@@ -0,0 +1,388 @@
+using Microsoft.CodeAnalysis;
+using PatternKit.Common;
+
+namespace PatternKit.Generators.Tests;
+
+public class MementoGeneratorTests
+{
+ [Fact]
+ public void GenerateMementoForClass()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial class Document
+ {
+ public string Text { get; set; } = "";
+ public int Version { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateMementoForClass));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Memento struct is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Document.Memento.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateMementoForRecordClass()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial record class EditorState(string Text, int Cursor);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateMementoForRecordClass));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Memento struct is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("EditorState.Memento.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateMementoForRecordStruct()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial record struct Point(int X, int Y);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateMementoForRecordStruct));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Memento struct is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Point.Memento.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenerateMementoForStruct()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial struct Counter
+ {
+ public int Value { get; set; }
+ public string Name { get; set; }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateMementoForStruct));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Memento struct is generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("Counter.Memento.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void ErrorWhenNotPartial()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public class Document
+ {
+ public string Text { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ErrorWhenNotPartial));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // PKMEM001 diagnostic is reported
+ var diags = result.Results.SelectMany(r => r.Diagnostics);
+ Assert.Contains(diags, d => d.Id == "PKMEM001");
+ }
+
+ [Fact]
+ public void GenerateCaretakerWhenRequested()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento(GenerateCaretaker = true, Capacity = 100)]
+ public partial record class EditorState(string Text, int Cursor);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateCaretakerWhenRequested));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Memento and caretaker are generated
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("EditorState.Memento.g.cs", names);
+ Assert.Contains("EditorState.History.g.cs", names);
+
+ // Compilation succeeds
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void MemberExclusionWithIgnore()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial class Document
+ {
+ public string Text { get; set; } = "";
+
+ [MementoIgnore]
+ public string InternalId { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(MemberExclusionWithIgnore));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var mementoSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("Memento.g.cs"));
+
+ Assert.NotEqual(default, mementoSourceResult);
+ var mementoSource = mementoSourceResult.SourceText.ToString();
+
+ // Memento includes Text but not InternalId
+ Assert.Contains("string Text", mementoSource); // Type might be "string" or "global::System.String"
+ Assert.DoesNotContain("InternalId", mementoSource);
+ }
+
+ [Fact]
+ public void ExplicitInclusionMode()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento(InclusionMode = MementoInclusionMode.ExplicitOnly)]
+ public partial class Document
+ {
+ [MementoInclude]
+ public string Text { get; set; } = "";
+
+ public string InternalData { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(ExplicitInclusionMode));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var mementoSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("Memento.g.cs"));
+
+ Assert.NotEqual(default, mementoSourceResult);
+ var mementoSource = mementoSourceResult.SourceText.ToString();
+
+ // Memento includes Text but not InternalData
+ Assert.Contains("string Text", mementoSource); // Type might be "string" or "global::System.String"
+ Assert.DoesNotContain("InternalData", mementoSource);
+ }
+
+ [Fact]
+ public void WarningForMutableReferenceCapture()
+ {
+ const string source = """
+ using PatternKit.Generators;
+ using System.Collections.Generic;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial class Document
+ {
+ public string Text { get; set; } = "";
+ public List Tags { get; set; } = new();
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(WarningForMutableReferenceCapture));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ // PKMEM003 warning is reported for List
+ var diags = result.Results.SelectMany(r => r.Diagnostics);
+ Assert.Contains(diags, d => d.Id == "PKMEM003" && d.GetMessage().Contains("Tags"));
+ }
+
+ [Fact]
+ public void GeneratedMementoHasCaptureAndRestore()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial record class EditorState(string Text, int Cursor);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GeneratedMementoHasCaptureAndRestore));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var mementoSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("Memento.g.cs"));
+
+ Assert.NotEqual(default, mementoSourceResult);
+ var mementoSource = mementoSourceResult.SourceText.ToString();
+
+ // Verify Capture and RestoreNew methods exist
+ Assert.Contains("public static EditorStateMemento Capture", mementoSource);
+ Assert.Contains("public global::TestNamespace.EditorState RestoreNew()", mementoSource);
+ }
+
+ [Fact]
+ public void GeneratedCaretakerHasUndoRedo()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento(GenerateCaretaker = true)]
+ public partial record class EditorState(string Text, int Cursor);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GeneratedCaretakerHasUndoRedo));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var caretakerSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("History.g.cs"));
+
+ Assert.NotEqual(default, caretakerSourceResult);
+ var caretakerSource = caretakerSourceResult.SourceText.ToString();
+
+ // Verify caretaker has undo/redo functionality
+ Assert.Contains("public bool Undo()", caretakerSource);
+ Assert.Contains("public bool Redo()", caretakerSource);
+ Assert.Contains("public void Capture", caretakerSource);
+ Assert.Contains("public bool CanUndo", caretakerSource);
+ Assert.Contains("public bool CanRedo", caretakerSource);
+ }
+
+ [Fact]
+ public void GeneratedMementoIncludesVersion()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento]
+ public partial class Document
+ {
+ public string Text { get; set; } = "";
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GeneratedMementoIncludesVersion));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var mementoSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("Memento.g.cs"));
+
+ Assert.NotEqual(default, mementoSourceResult);
+ var mementoSource = mementoSourceResult.SourceText.ToString();
+
+ // Verify MementoVersion property exists
+ Assert.Contains("public int MementoVersion", mementoSource);
+ }
+
+ [Fact]
+ public void CaretakerRespectsCapacity()
+ {
+ const string source = """
+ using PatternKit.Generators;
+
+ namespace TestNamespace;
+
+ [Memento(GenerateCaretaker = true, Capacity = 50)]
+ public partial record class EditorState(string Text, int Cursor);
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(CaretakerRespectsCapacity));
+ var gen = new MementoGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out _);
+
+ var caretakerSourceResult = result.Results
+ .SelectMany(r => r.GeneratedSources)
+ .FirstOrDefault(gs => gs.HintName.Contains("History.g.cs"));
+
+ Assert.NotEqual(default, caretakerSourceResult);
+ var caretakerSource = caretakerSourceResult.SourceText.ToString();
+
+ // Verify capacity setting (using regex for flexibility)
+ Assert.Matches(@"private\s+const\s+int\s+MaxCapacity\s*=\s*50", caretakerSource);
+ Assert.Matches(@"if\s*\(\s*_states\.Count\s*>\s*MaxCapacity\s*\)", caretakerSource);
+ }
+}