feat(backend): Implement MCP Protocol Handler (Story 5.1)
Implemented JSON-RPC 2.0 protocol handler for MCP communication, enabling AI agents to communicate with ColaFlow using the Model Context Protocol. **Implementation:** - JSON-RPC 2.0 data models (Request, Response, Error, ErrorCode) - MCP protocol models (Initialize, Capabilities, ClientInfo, ServerInfo) - McpProtocolHandler with method routing and error handling - Method handlers: initialize, resources/list, tools/list, tools/call - ASP.NET Core middleware for /mcp endpoint - Service registration and dependency injection setup **Testing:** - 28 unit tests covering protocol parsing, validation, and error handling - Integration tests for initialize handshake and error responses - All tests passing with >80% coverage **Changes:** - Created ColaFlow.Modules.Mcp.Contracts project - Created ColaFlow.Modules.Mcp.Domain project - Created ColaFlow.Modules.Mcp.Application project - Created ColaFlow.Modules.Mcp.Infrastructure project - Created ColaFlow.Modules.Mcp.Tests project - Registered MCP module in ColaFlow.API Program.cs - Added /mcp endpoint via middleware **Acceptance Criteria Met:** ✅ JSON-RPC 2.0 messages correctly parsed ✅ Request validation (jsonrpc: "2.0", method, params, id) ✅ Error responses conform to JSON-RPC 2.0 spec ✅ Invalid requests return proper error codes (-32700, -32600, -32601, -32602) ✅ MCP initialize method implemented ✅ Server capabilities returned (resources, tools, prompts) ✅ Protocol version negotiation works (1.0) ✅ Request routing to method handlers ✅ Unit test coverage > 80% ✅ All tests passing **Story**: docs/stories/sprint_5/story_5_1.md 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
586
docs/stories/sprint_5/story_5_3.md
Normal file
586
docs/stories/sprint_5/story_5_3.md
Normal file
@@ -0,0 +1,586 @@
|
||||
---
|
||||
story_id: story_5_3
|
||||
sprint_id: sprint_5
|
||||
phase: Phase 1 - Foundation
|
||||
status: not_started
|
||||
priority: P0
|
||||
story_points: 5
|
||||
assignee: backend
|
||||
estimated_days: 2
|
||||
created_date: 2025-11-06
|
||||
dependencies: []
|
||||
---
|
||||
|
||||
# Story 5.3: MCP Domain Layer Design
|
||||
|
||||
**Phase**: Phase 1 - Foundation (Week 1-2)
|
||||
**Priority**: P0 CRITICAL
|
||||
**Estimated Effort**: 5 Story Points (2 days)
|
||||
|
||||
## User Story
|
||||
|
||||
**As a** Backend Developer
|
||||
**I want** well-designed domain entities and aggregates for MCP operations
|
||||
**So that** the system has a solid foundation following DDD principles and can be easily extended
|
||||
|
||||
## Business Value
|
||||
|
||||
Clean domain design is critical for long-term maintainability. This Story establishes the domain model for all MCP operations, ensuring business rules are enforced consistently and the system can evolve without accumulating technical debt.
|
||||
|
||||
**Impact**:
|
||||
- Provides solid foundation for all MCP features
|
||||
- Enforces business rules at domain level
|
||||
- Enables rich domain events for audit and notifications
|
||||
- Reduces bugs through domain-driven validation
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### AC1: McpApiKey Aggregate Root (Covered in Story 5.2)
|
||||
- [ ] McpApiKey entity implemented as aggregate root
|
||||
- [ ] Factory method for creation with validation
|
||||
- [ ] Domain methods (Revoke, RecordUsage, UpdatePermissions)
|
||||
- [ ] Domain events (ApiKeyCreated, ApiKeyRevoked)
|
||||
|
||||
### AC2: PendingChange Aggregate Root
|
||||
- [ ] PendingChange entity with status lifecycle
|
||||
- [ ] Factory method for creation from Diff Preview
|
||||
- [ ] Approve() method that triggers execution
|
||||
- [ ] Reject() method with reason logging
|
||||
- [ ] Expire() method for 24-hour timeout
|
||||
- [ ] Domain events (PendingChangeCreated, Approved, Rejected, Expired)
|
||||
|
||||
### AC3: DiffPreview Value Object
|
||||
- [ ] Immutable value object for before/after data
|
||||
- [ ] Field-level change tracking (DiffField collection)
|
||||
- [ ] Support for CREATE, UPDATE, DELETE operations
|
||||
- [ ] JSON serialization for complex objects
|
||||
- [ ] Equality comparison (value semantics)
|
||||
|
||||
### AC4: Domain Events
|
||||
- [ ] ApiKeyCreatedEvent
|
||||
- [ ] ApiKeyRevokedEvent
|
||||
- [ ] PendingChangeCreatedEvent
|
||||
- [ ] PendingChangeApprovedEvent
|
||||
- [ ] PendingChangeRejectedEvent
|
||||
- [ ] PendingChangeExpiredEvent
|
||||
- [ ] All events inherit from `DomainEvent` base class
|
||||
|
||||
### AC5: Business Rule Validation
|
||||
- [ ] PendingChange cannot be approved if expired
|
||||
- [ ] PendingChange cannot be rejected if already approved
|
||||
- [ ] McpApiKey cannot be used if revoked or expired
|
||||
- [ ] DiffPreview must have at least one changed field for UPDATE
|
||||
- [ ] All invariants enforced in domain entities
|
||||
|
||||
### AC6: Testing
|
||||
- [ ] Unit test coverage > 90% for domain layer
|
||||
- [ ] Test all domain methods (Create, Approve, Reject, etc.)
|
||||
- [ ] Test domain events are raised correctly
|
||||
- [ ] Test business rule violations throw exceptions
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Domain Model Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ McpApiKey (Aggregate Root) │
|
||||
│ - TenantId │
|
||||
│ - KeyHash, KeyPrefix │
|
||||
│ - Permissions │
|
||||
│ - Status (Active, Revoked) │
|
||||
│ - ExpiresAt │
|
||||
│ + Create() │
|
||||
│ + Revoke() │
|
||||
│ + RecordUsage() │
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
┌─────────────────────────────────────┐
|
||||
│ PendingChange (Aggregate Root) │
|
||||
│ - TenantId, ApiKeyId │
|
||||
│ - ToolName │
|
||||
│ - Diff (DiffPreview) │
|
||||
│ - Status (Pending, Approved, ...) │
|
||||
│ - ExpiresAt │
|
||||
│ + Create() │
|
||||
│ + Approve() │
|
||||
│ + Reject() │
|
||||
│ + Expire() │
|
||||
└─────────────────────────────────────┘
|
||||
│
|
||||
│ has-a
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ DiffPreview (Value Object) │
|
||||
│ - Operation (CREATE/UPDATE/DELETE) │
|
||||
│ - EntityType, EntityId │
|
||||
│ - BeforeData, AfterData │
|
||||
│ - ChangedFields[] │
|
||||
│ + CalculateDiff() │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### PendingChange Aggregate Root
|
||||
|
||||
```csharp
|
||||
public class PendingChange : AggregateRoot
|
||||
{
|
||||
public Guid TenantId { get; private set; }
|
||||
public Guid ApiKeyId { get; private set; }
|
||||
|
||||
public string ToolName { get; private set; }
|
||||
public DiffPreview Diff { get; private set; }
|
||||
|
||||
public PendingChangeStatus Status { get; private set; }
|
||||
public DateTime ExpiresAt { get; private set; }
|
||||
|
||||
public Guid? ApprovedBy { get; private set; }
|
||||
public DateTime? ApprovedAt { get; private set; }
|
||||
|
||||
public Guid? RejectedBy { get; private set; }
|
||||
public DateTime? RejectedAt { get; private set; }
|
||||
public string? RejectionReason { get; private set; }
|
||||
|
||||
// Factory method
|
||||
public static PendingChange Create(
|
||||
string toolName,
|
||||
DiffPreview diff,
|
||||
Guid tenantId,
|
||||
Guid apiKeyId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(toolName))
|
||||
throw new ArgumentException("Tool name required", nameof(toolName));
|
||||
|
||||
if (diff == null)
|
||||
throw new ArgumentNullException(nameof(diff));
|
||||
|
||||
var pendingChange = new PendingChange
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
TenantId = tenantId,
|
||||
ApiKeyId = apiKeyId,
|
||||
ToolName = toolName,
|
||||
Diff = diff,
|
||||
Status = PendingChangeStatus.PendingApproval,
|
||||
ExpiresAt = DateTime.UtcNow.AddHours(24)
|
||||
};
|
||||
|
||||
pendingChange.AddDomainEvent(new PendingChangeCreatedEvent(
|
||||
pendingChange.Id,
|
||||
toolName,
|
||||
diff.EntityType,
|
||||
diff.Operation
|
||||
));
|
||||
|
||||
return pendingChange;
|
||||
}
|
||||
|
||||
// Domain method: Approve
|
||||
public void Approve(Guid approvedBy)
|
||||
{
|
||||
if (Status != PendingChangeStatus.PendingApproval)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot approve change with status {Status}");
|
||||
|
||||
if (DateTime.UtcNow > ExpiresAt)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot approve expired change");
|
||||
|
||||
Status = PendingChangeStatus.Approved;
|
||||
ApprovedBy = approvedBy;
|
||||
ApprovedAt = DateTime.UtcNow;
|
||||
|
||||
AddDomainEvent(new PendingChangeApprovedEvent(
|
||||
Id,
|
||||
ToolName,
|
||||
Diff,
|
||||
approvedBy
|
||||
));
|
||||
}
|
||||
|
||||
// Domain method: Reject
|
||||
public void Reject(Guid rejectedBy, string reason)
|
||||
{
|
||||
if (Status != PendingChangeStatus.PendingApproval)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot reject change with status {Status}");
|
||||
|
||||
Status = PendingChangeStatus.Rejected;
|
||||
RejectedBy = rejectedBy;
|
||||
RejectedAt = DateTime.UtcNow;
|
||||
RejectionReason = reason;
|
||||
|
||||
AddDomainEvent(new PendingChangeRejectedEvent(
|
||||
Id,
|
||||
ToolName,
|
||||
reason,
|
||||
rejectedBy
|
||||
));
|
||||
}
|
||||
|
||||
// Domain method: Expire
|
||||
public void Expire()
|
||||
{
|
||||
if (Status != PendingChangeStatus.PendingApproval)
|
||||
return; // Already processed
|
||||
|
||||
if (DateTime.UtcNow <= ExpiresAt)
|
||||
throw new InvalidOperationException(
|
||||
"Cannot expire change before expiration time");
|
||||
|
||||
Status = PendingChangeStatus.Expired;
|
||||
|
||||
AddDomainEvent(new PendingChangeExpiredEvent(Id, ToolName));
|
||||
}
|
||||
}
|
||||
|
||||
public enum PendingChangeStatus
|
||||
{
|
||||
PendingApproval = 0,
|
||||
Approved = 1,
|
||||
Rejected = 2,
|
||||
Expired = 3
|
||||
}
|
||||
```
|
||||
|
||||
### DiffPreview Value Object
|
||||
|
||||
```csharp
|
||||
public class DiffPreview : ValueObject
|
||||
{
|
||||
public string Operation { get; private set; } // CREATE, UPDATE, DELETE
|
||||
public string EntityType { get; private set; }
|
||||
public Guid? EntityId { get; private set; }
|
||||
public string? EntityKey { get; private set; } // e.g., "COLA-146"
|
||||
|
||||
public object? BeforeData { get; private set; }
|
||||
public object? AfterData { get; private set; }
|
||||
|
||||
public IReadOnlyList<DiffField> ChangedFields { get; private set; }
|
||||
|
||||
public DiffPreview(
|
||||
string operation,
|
||||
string entityType,
|
||||
Guid? entityId,
|
||||
string? entityKey,
|
||||
object? beforeData,
|
||||
object? afterData,
|
||||
IReadOnlyList<DiffField> changedFields)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(operation))
|
||||
throw new ArgumentException("Operation required", nameof(operation));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(entityType))
|
||||
throw new ArgumentException("EntityType required", nameof(entityType));
|
||||
|
||||
if (operation == "UPDATE" && (changedFields == null || !changedFields.Any()))
|
||||
throw new ArgumentException(
|
||||
"UPDATE operation must have at least one changed field",
|
||||
nameof(changedFields));
|
||||
|
||||
Operation = operation;
|
||||
EntityType = entityType;
|
||||
EntityId = entityId;
|
||||
EntityKey = entityKey;
|
||||
BeforeData = beforeData;
|
||||
AfterData = afterData;
|
||||
ChangedFields = changedFields ?? new List<DiffField>().AsReadOnly();
|
||||
}
|
||||
|
||||
// Value object equality
|
||||
protected override IEnumerable<object?> GetEqualityComponents()
|
||||
{
|
||||
yield return Operation;
|
||||
yield return EntityType;
|
||||
yield return EntityId;
|
||||
yield return EntityKey;
|
||||
yield return BeforeData;
|
||||
yield return AfterData;
|
||||
foreach (var field in ChangedFields)
|
||||
yield return field;
|
||||
}
|
||||
}
|
||||
|
||||
public class DiffField : ValueObject
|
||||
{
|
||||
public string FieldName { get; private set; }
|
||||
public string DisplayName { get; private set; }
|
||||
public object? OldValue { get; private set; }
|
||||
public object? NewValue { get; private set; }
|
||||
public string? DiffHtml { get; private set; }
|
||||
|
||||
public DiffField(
|
||||
string fieldName,
|
||||
string displayName,
|
||||
object? oldValue,
|
||||
object? newValue,
|
||||
string? diffHtml = null)
|
||||
{
|
||||
FieldName = fieldName;
|
||||
DisplayName = displayName;
|
||||
OldValue = oldValue;
|
||||
NewValue = newValue;
|
||||
DiffHtml = diffHtml;
|
||||
}
|
||||
|
||||
protected override IEnumerable<object?> GetEqualityComponents()
|
||||
{
|
||||
yield return FieldName;
|
||||
yield return DisplayName;
|
||||
yield return OldValue;
|
||||
yield return NewValue;
|
||||
yield return DiffHtml;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Domain Events
|
||||
|
||||
```csharp
|
||||
// Base class (should already exist in M1)
|
||||
public abstract record DomainEvent
|
||||
{
|
||||
public Guid EventId { get; init; } = Guid.NewGuid();
|
||||
public DateTime OccurredAt { get; init; } = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
// PendingChange events
|
||||
public record PendingChangeCreatedEvent(
|
||||
Guid PendingChangeId,
|
||||
string ToolName,
|
||||
string EntityType,
|
||||
string Operation
|
||||
) : DomainEvent;
|
||||
|
||||
public record PendingChangeApprovedEvent(
|
||||
Guid PendingChangeId,
|
||||
string ToolName,
|
||||
DiffPreview Diff,
|
||||
Guid ApprovedBy
|
||||
) : DomainEvent;
|
||||
|
||||
public record PendingChangeRejectedEvent(
|
||||
Guid PendingChangeId,
|
||||
string ToolName,
|
||||
string Reason,
|
||||
Guid RejectedBy
|
||||
) : DomainEvent;
|
||||
|
||||
public record PendingChangeExpiredEvent(
|
||||
Guid PendingChangeId,
|
||||
string ToolName
|
||||
) : DomainEvent;
|
||||
|
||||
// ApiKey events (from Story 5.2)
|
||||
public record ApiKeyCreatedEvent(
|
||||
Guid ApiKeyId,
|
||||
string Name
|
||||
) : DomainEvent;
|
||||
|
||||
public record ApiKeyRevokedEvent(
|
||||
Guid ApiKeyId,
|
||||
string Name
|
||||
) : DomainEvent;
|
||||
```
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1: Create Domain Base Classes (2 hours)
|
||||
- [ ] Verify `AggregateRoot` base class exists (from M1)
|
||||
- [ ] Verify `ValueObject` base class exists (from M1)
|
||||
- [ ] Verify `DomainEvent` base class exists (from M1)
|
||||
- [ ] Update if needed for MCP requirements
|
||||
|
||||
**Files to Check/Update**:
|
||||
- `ColaFlow.Core/Domain/AggregateRoot.cs`
|
||||
- `ColaFlow.Core/Domain/ValueObject.cs`
|
||||
- `ColaFlow.Core/Domain/DomainEvent.cs`
|
||||
|
||||
### Task 2: PendingChange Aggregate Root (4 hours)
|
||||
- [ ] Create `PendingChange` entity class
|
||||
- [ ] Implement `Create()` factory method
|
||||
- [ ] Implement `Approve()` domain method
|
||||
- [ ] Implement `Reject()` domain method
|
||||
- [ ] Implement `Expire()` domain method
|
||||
- [ ] Add business rule validation
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Domain/Entities/PendingChange.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Enums/PendingChangeStatus.cs`
|
||||
|
||||
### Task 3: DiffPreview Value Object (3 hours)
|
||||
- [ ] Create `DiffPreview` value object
|
||||
- [ ] Create `DiffField` value object
|
||||
- [ ] Implement equality comparison
|
||||
- [ ] Add validation (operation, changed fields)
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Domain/ValueObjects/DiffPreview.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/ValueObjects/DiffField.cs`
|
||||
|
||||
### Task 4: Domain Events (2 hours)
|
||||
- [ ] Create `PendingChangeCreatedEvent`
|
||||
- [ ] Create `PendingChangeApprovedEvent`
|
||||
- [ ] Create `PendingChangeRejectedEvent`
|
||||
- [ ] Create `PendingChangeExpiredEvent`
|
||||
- [ ] Ensure events inherit from `DomainEvent`
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/PendingChangeCreatedEvent.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/PendingChangeApprovedEvent.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/PendingChangeRejectedEvent.cs`
|
||||
- `ColaFlow.Modules.Mcp.Domain/Events/PendingChangeExpiredEvent.cs`
|
||||
|
||||
### Task 5: Unit Tests - PendingChange (4 hours)
|
||||
- [ ] Test `Create()` factory method
|
||||
- [ ] Test `Approve()` happy path
|
||||
- [ ] Test `Approve()` when already approved (throws exception)
|
||||
- [ ] Test `Approve()` when expired (throws exception)
|
||||
- [ ] Test `Reject()` happy path
|
||||
- [ ] Test `Reject()` when already approved (throws exception)
|
||||
- [ ] Test `Expire()` happy path
|
||||
- [ ] Test domain events are raised
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Tests/Domain/PendingChangeTests.cs`
|
||||
|
||||
### Task 6: Unit Tests - DiffPreview (3 hours)
|
||||
- [ ] Test `DiffPreview` creation
|
||||
- [ ] Test validation (operation required, entity type required)
|
||||
- [ ] Test UPDATE requires changed fields
|
||||
- [ ] Test equality comparison (value semantics)
|
||||
- [ ] Test `DiffField` creation and equality
|
||||
|
||||
**Files to Create**:
|
||||
- `ColaFlow.Modules.Mcp.Tests/Domain/DiffPreviewTests.cs`
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests (Target: > 90% coverage)
|
||||
- All domain entity methods (Create, Approve, Reject, Expire)
|
||||
- All business rule validations
|
||||
- Domain events raised at correct times
|
||||
- Value object equality semantics
|
||||
- Edge cases (null values, invalid states)
|
||||
|
||||
### Test Cases for PendingChange
|
||||
|
||||
```csharp
|
||||
[Fact]
|
||||
public void Create_ValidInput_Success()
|
||||
{
|
||||
// Arrange
|
||||
var diff = CreateValidDiff();
|
||||
|
||||
// Act
|
||||
var pendingChange = PendingChange.Create(
|
||||
"create_issue", diff, tenantId, apiKeyId);
|
||||
|
||||
// Assert
|
||||
Assert.NotEqual(Guid.Empty, pendingChange.Id);
|
||||
Assert.Equal(PendingChangeStatus.PendingApproval, pendingChange.Status);
|
||||
Assert.True(pendingChange.ExpiresAt > DateTime.UtcNow);
|
||||
Assert.Single(pendingChange.DomainEvents); // PendingChangeCreatedEvent
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_PendingChange_Success()
|
||||
{
|
||||
// Arrange
|
||||
var pendingChange = CreatePendingChange();
|
||||
var approverId = Guid.NewGuid();
|
||||
|
||||
// Act
|
||||
pendingChange.Approve(approverId);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(PendingChangeStatus.Approved, pendingChange.Status);
|
||||
Assert.Equal(approverId, pendingChange.ApprovedBy);
|
||||
Assert.NotNull(pendingChange.ApprovedAt);
|
||||
Assert.Contains(pendingChange.DomainEvents,
|
||||
e => e is PendingChangeApprovedEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_AlreadyApproved_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var pendingChange = CreatePendingChange();
|
||||
pendingChange.Approve(Guid.NewGuid());
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => pendingChange.Approve(Guid.NewGuid()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Approve_ExpiredChange_ThrowsException()
|
||||
{
|
||||
// Arrange
|
||||
var pendingChange = CreateExpiredPendingChange();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(
|
||||
() => pendingChange.Approve(Guid.NewGuid()));
|
||||
}
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
**Prerequisites**:
|
||||
- M1 domain base classes (AggregateRoot, ValueObject, DomainEvent)
|
||||
- .NET 9
|
||||
|
||||
**Used By**:
|
||||
- Story 5.9 (Diff Preview Service) - Uses DiffPreview value object
|
||||
- Story 5.10 (PendingChange Management) - Uses PendingChange aggregate
|
||||
- Story 5.11 (Core MCP Tools) - Creates PendingChange entities
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
| Risk | Impact | Probability | Mitigation |
|
||||
|------|--------|-------------|------------|
|
||||
| Business rules incomplete | Medium | Medium | Comprehensive unit tests, domain expert review |
|
||||
| Domain events not raised | Medium | Low | Unit tests verify events, event sourcing pattern |
|
||||
| Value object equality bugs | Low | Low | Thorough testing, use proven patterns |
|
||||
| Over-engineering domain | Medium | Medium | Keep it simple, add complexity only when needed |
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Code compiles without warnings
|
||||
- [ ] All unit tests passing (> 90% coverage)
|
||||
- [ ] Code reviewed and approved
|
||||
- [ ] XML documentation for all public APIs
|
||||
- [ ] All aggregates follow DDD patterns
|
||||
- [ ] All value objects are immutable
|
||||
- [ ] All domain events inherit from DomainEvent
|
||||
- [ ] Business rules enforced in domain entities (not services)
|
||||
- [ ] No anemic domain model (rich behavior in entities)
|
||||
|
||||
## Notes
|
||||
|
||||
### Why This Story Matters
|
||||
- **Clean Architecture**: Solid domain foundation prevents future refactoring
|
||||
- **Business Rules**: Domain entities enforce invariants, reducing bugs
|
||||
- **Domain Events**: Enable loose coupling, audit trail, notifications
|
||||
- **Testability**: Rich domain model is easy to unit test
|
||||
|
||||
### Key Design Principles
|
||||
1. **Aggregates**: Consistency boundaries (McpApiKey, PendingChange)
|
||||
2. **Value Objects**: Immutable, equality by value (DiffPreview, DiffField)
|
||||
3. **Domain Events**: Side effects handled outside aggregate
|
||||
4. **Factory Methods**: Encapsulate creation logic, enforce invariants
|
||||
5. **Encapsulation**: Private setters, public domain methods
|
||||
|
||||
### DDD Patterns Used
|
||||
- **Aggregate Root**: PendingChange, McpApiKey
|
||||
- **Value Object**: DiffPreview, DiffField
|
||||
- **Domain Event**: 6 events for audit and notifications
|
||||
- **Factory Method**: Create() methods with validation
|
||||
- **Invariant Protection**: Business rules in domain entities
|
||||
|
||||
### Reference Materials
|
||||
- Domain-Driven Design (Eric Evans)
|
||||
- Sprint 5 Plan: `docs/plans/sprint_5.md`
|
||||
- Architecture Design: `docs/M2-MCP-SERVER-ARCHITECTURE.md`
|
||||
- M1 Domain Layer: `ColaFlow.Core/Domain/`
|
||||
Reference in New Issue
Block a user