# Day 15-16 Implementation Roadmap

**Date**: 2025-11-05 至 2025-11-06
**Focus**: Epic/Story/Task Hierarchy Implementation
**Module**: Issue Management Module (Enhancement)
**Architecture Decision**: ADR-035

---

## Overview

Day 15-16 will implement parent-child hierarchy support in the existing Issue Management Module, enabling Epic → Story → Task relationships without breaking existing functionality.

**Key Principles**:
- Enhance, don't replace
- Backward compatible
- Zero breaking changes
- Multi-tenant secure by default
- Performance first

---

## Day 15: Database & Domain Layer (6-8 hours)

### Morning Session (3-4 hours): Database Design

#### Task 1.1: Create Database Migration (1-1.5h)
**Owner**: Backend Developer

**Steps**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Migrations/
// 20251105_AddIssueHierarchy.cs

public partial class AddIssueHierarchy : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        // Add ParentIssueId column
        migrationBuilder.AddColumn<Guid?>(
            name: "parent_issue_id",
            table: "issues",
            type: "uuid",
            nullable: true);

        // Add foreign key constraint
        migrationBuilder.AddForeignKey(
            name: "fk_issues_parent",
            table: "issues",
            column: "parent_issue_id",
            principalTable: "issues",
            principalColumn: "id",
            onDelete: ReferentialAction.SetNull);  // Prevent cascade delete

        // Add index for performance
        migrationBuilder.CreateIndex(
            name: "ix_issues_parent_issue_id",
            table: "issues",
            column: "parent_issue_id",
            filter: "parent_issue_id IS NOT NULL");  // Partial index
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropForeignKey(
            name: "fk_issues_parent",
            table: "issues");

        migrationBuilder.DropIndex(
            name: "ix_issues_parent_issue_id",
            table: "issues");

        migrationBuilder.DropColumn(
            name: "parent_issue_id",
            table: "issues");
    }
}
```

**Checklist**:
- [ ] Create migration file
- [ ] Test migration: `dotnet ef migrations add AddIssueHierarchy`
- [ ] Apply to dev DB: `dotnet ef database update`
- [ ] Verify column added: `\d+ issues` in psql
- [ ] Verify index created
- [ ] Test backward compatibility (existing queries still work)

**Acceptance Criteria**:
- Migration runs without errors
- `parent_issue_id` column is nullable (existing data unaffected)
- Foreign key prevents invalid parent references
- Index improves query performance

---

#### Task 1.2: Update EF Core Configuration (0.5-1h)
**Owner**: Backend Developer

**Steps**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Configurations/IssueConfiguration.cs

public class IssueConfiguration : IEntityTypeConfiguration<Issue>
{
    public void Configure(EntityTypeBuilder<Issue> builder)
    {
        // Existing configuration...

        // NEW: Hierarchy configuration
        builder.Property(i => i.ParentIssueId)
            .HasColumnName("parent_issue_id")
            .IsRequired(false);

        builder.HasOne(i => i.ParentIssue)
            .WithMany(i => i.ChildIssues)
            .HasForeignKey(i => i.ParentIssueId)
            .OnDelete(DeleteBehavior.SetNull)  // When parent deleted, set child's parent to NULL
            .IsRequired(false);

        builder.HasMany(i => i.ChildIssues)
            .WithOne(i => i.ParentIssue)
            .HasForeignKey(i => i.ParentIssueId)
            .OnDelete(DeleteBehavior.SetNull);
    }
}
```

**Checklist**:
- [ ] Update `IssueConfiguration.cs`
- [ ] Verify EF Core generates correct SQL
- [ ] Test lazy loading disabled (no N+1 queries)
- [ ] Test eager loading works: `.Include(i => i.ChildIssues)`

**Acceptance Criteria**:
- EF Core configuration matches database schema
- Navigation properties work correctly
- Delete behavior is SetNull (not Cascade)

---

### Afternoon Session (3-4 hours): Domain Logic

#### Task 1.3: Update Issue Entity (2-3h)
**Owner**: Backend Developer

**Steps**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Domain/Entities/Issue.cs

public class Issue : TenantEntity, IAggregateRoot
{
    // Existing properties...
    public IssueType Type { get; private set; }
    public string Title { get; private set; }
    public IssueStatus Status { get; private set; }

    // NEW: Hierarchy support
    public Guid? ParentIssueId { get; private set; }
    public virtual Issue? ParentIssue { get; private set; }
    public virtual ICollection<Issue> ChildIssues { get; private set; } = new List<Issue>();

    // NEW: Hierarchy methods
    public Result SetParent(Issue parent)
    {
        // Validation 1: Same tenant
        if (parent.TenantId != this.TenantId)
            return Result.Failure("Cannot link issues across tenants");

        // Validation 2: Valid hierarchy rules
        if (!IsValidHierarchy(parent))
            return Result.Failure($"{parent.Type} cannot be parent of {this.Type}");

        // Validation 3: Prevent circular dependency
        if (WouldCreateCircularDependency(parent))
            return Result.Failure("Circular dependency detected");

        // Validation 4: Depth limit
        if (parent.GetDepth() >= 2)  // Max 3 levels (0, 1, 2)
            return Result.Failure("Maximum hierarchy depth exceeded (3 levels)");

        // Set parent
        ParentIssueId = parent.Id;
        ParentIssue = parent;

        // Raise domain event
        AddDomainEvent(new IssueHierarchyChangedEvent(
            issueId: this.Id,
            newParentId: parent.Id,
            oldParentId: null
        ));

        return Result.Success();
    }

    public Result RemoveParent()
    {
        if (!ParentIssueId.HasValue)
            return Result.Failure("Issue does not have a parent");

        var oldParentId = ParentIssueId.Value;

        ParentIssueId = null;
        ParentIssue = null;

        // Raise domain event
        AddDomainEvent(new IssueHierarchyChangedEvent(
            issueId: this.Id,
            newParentId: null,
            oldParentId: oldParentId
        ));

        return Result.Success();
    }

    // NEW: Validation rules
    private bool IsValidHierarchy(Issue parent)
    {
        return (parent.Type, this.Type) switch
        {
            (IssueType.Epic, IssueType.Story) => true,
            (IssueType.Story, IssueType.Task) => true,
            (IssueType.Story, IssueType.Bug) => true,
            _ => false
        };
    }

    private bool WouldCreateCircularDependency(Issue proposedParent)
    {
        var current = proposedParent;
        var visitedIds = new HashSet<Guid> { this.Id };
        int depth = 0;
        int maxDepth = 10;  // Safety limit

        while (current != null && depth < maxDepth)
        {
            if (visitedIds.Contains(current.Id))
                return true;  // Circular dependency detected

            visitedIds.Add(current.Id);
            current = current.ParentIssue;
            depth++;
        }

        return false;
    }

    public int GetDepth()
    {
        int depth = 0;
        var current = this.ParentIssue;

        while (current != null && depth < 10)
        {
            depth++;
            current = current.ParentIssue;
        }

        return depth;
    }

    public List<Guid> GetAncestorIds()
    {
        var ancestors = new List<Guid>();
        var current = this.ParentIssue;

        while (current != null && ancestors.Count < 10)
        {
            ancestors.Add(current.Id);
            current = current.ParentIssue;
        }

        return ancestors;
    }
}
```

**Checklist**:
- [ ] Add hierarchy properties (ParentIssueId, ParentIssue, ChildIssues)
- [ ] Implement `SetParent` method with 4 validations
- [ ] Implement `RemoveParent` method
- [ ] Add `IsValidHierarchy` validation logic
- [ ] Add `WouldCreateCircularDependency` check
- [ ] Add `GetDepth` method
- [ ] Add `GetAncestorIds` method
- [ ] Ensure domain events are raised

**Acceptance Criteria**:
- All hierarchy rules enforced (Epic → Story → Task)
- Circular dependency prevented
- Max depth 3 levels enforced
- Tenant isolation maintained
- Domain events raised on hierarchy changes

---

#### Task 1.4: Create Domain Events (0.5-1h)
**Owner**: Backend Developer

**Steps**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Domain/Events/IssueHierarchyChangedEvent.cs

public class IssueHierarchyChangedEvent : DomainEvent
{
    public Guid IssueId { get; }
    public Guid? NewParentId { get; }
    public Guid? OldParentId { get; }

    public IssueHierarchyChangedEvent(
        Guid issueId,
        Guid? newParentId,
        Guid? oldParentId)
    {
        IssueId = issueId;
        NewParentId = newParentId;
        OldParentId = oldParentId;
    }
}
```

**Checklist**:
- [ ] Create `IssueHierarchyChangedEvent.cs`
- [ ] Add event handler (if needed for future use)
- [ ] Test event is raised when parent set/removed

**Acceptance Criteria**:
- Event is raised on hierarchy changes
- Event contains correct issueId, newParentId, oldParentId

---

#### Task 1.5: Unit Tests for Domain Logic (1-1.5h)
**Owner**: Backend Developer

**Steps**:
```csharp
// File: colaflow-api/tests/Modules/IssueManagement.Domain.Tests/IssueHierarchyTests.cs

public class IssueHierarchyTests
{
    [Fact]
    public void SetParent_ValidEpicToStory_ShouldSucceed()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var story = CreateIssue(IssueType.Story);

        // Act
        var result = story.SetParent(epic);

        // Assert
        result.IsSuccess.Should().BeTrue();
        story.ParentIssueId.Should().Be(epic.Id);
    }

    [Fact]
    public void SetParent_InvalidTaskToEpic_ShouldFail()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var task = CreateIssue(IssueType.Task);

        // Act
        var result = task.SetParent(epic);

        // Assert
        result.IsSuccess.Should().BeFalse();
        result.Error.Should().Contain("cannot be parent");
    }

    [Fact]
    public void SetParent_CircularDependency_ShouldFail()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var story = CreateIssue(IssueType.Story);
        story.SetParent(epic);

        // Act - Try to set epic's parent as story (circular)
        var result = epic.SetParent(story);

        // Assert
        result.IsSuccess.Should().BeFalse();
        result.Error.Should().Contain("Circular dependency");
    }

    [Fact]
    public void SetParent_CrossTenant_ShouldFail()
    {
        // Arrange
        var epicTenant1 = CreateIssue(IssueType.Epic, tenantId: Guid.NewGuid());
        var storyTenant2 = CreateIssue(IssueType.Story, tenantId: Guid.NewGuid());

        // Act
        var result = storyTenant2.SetParent(epicTenant1);

        // Assert
        result.IsSuccess.Should().BeFalse();
        result.Error.Should().Contain("across tenants");
    }

    [Fact]
    public void SetParent_MaxDepthExceeded_ShouldFail()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var story1 = CreateIssue(IssueType.Story);
        var story2 = CreateIssue(IssueType.Story);
        var task = CreateIssue(IssueType.Task);

        story1.SetParent(epic);     // Depth 1
        story2.SetParent(story1);   // Depth 2
        task.SetParent(story2);     // Depth 3

        // Act - Try to add one more level (would be depth 4)
        var extraTask = CreateIssue(IssueType.Task);
        var result = extraTask.SetParent(task);

        // Assert
        result.IsSuccess.Should().BeFalse();
        result.Error.Should().Contain("Maximum hierarchy depth");
    }

    [Fact]
    public void GetDepth_ThreeLevels_ShouldReturnTwo()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);     // Depth 0
        var story = CreateIssue(IssueType.Story);
        var task = CreateIssue(IssueType.Task);

        story.SetParent(epic);   // Depth 1
        task.SetParent(story);   // Depth 2

        // Act
        var depth = task.GetDepth();

        // Assert
        depth.Should().Be(2);
    }

    [Fact]
    public void RemoveParent_HasParent_ShouldSucceed()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var story = CreateIssue(IssueType.Story);
        story.SetParent(epic);

        // Act
        var result = story.RemoveParent();

        // Assert
        result.IsSuccess.Should().BeTrue();
        story.ParentIssueId.Should().BeNull();
    }

    [Fact]
    public void SetParent_RaisesHierarchyChangedEvent()
    {
        // Arrange
        var epic = CreateIssue(IssueType.Epic);
        var story = CreateIssue(IssueType.Story);

        // Act
        story.SetParent(epic);

        // Assert
        var domainEvent = story.DomainEvents
            .OfType<IssueHierarchyChangedEvent>()
            .FirstOrDefault();

        domainEvent.Should().NotBeNull();
        domainEvent.IssueId.Should().Be(story.Id);
        domainEvent.NewParentId.Should().Be(epic.Id);
    }

    private Issue CreateIssue(
        IssueType type,
        Guid? tenantId = null)
    {
        return Issue.Create(
            tenantId: tenantId ?? Guid.NewGuid(),
            projectId: Guid.NewGuid(),
            type: type,
            title: $"Test {type}",
            description: "Test description",
            priority: IssuePriority.Medium,
            createdBy: Guid.NewGuid()
        );
    }
}
```

**Checklist**:
- [ ] Test valid hierarchy: Epic → Story, Story → Task
- [ ] Test invalid hierarchy: Task → Story, Epic → Task
- [ ] Test circular dependency prevention
- [ ] Test cross-tenant rejection
- [ ] Test max depth enforcement (3 levels)
- [ ] Test `GetDepth` method
- [ ] Test `RemoveParent` method
- [ ] Test domain events raised
- [ ] All 10+ tests passing

**Acceptance Criteria**:
- 100% code coverage for hierarchy logic
- All edge cases tested
- Tests run in < 1 second

---

## Day 16: Application & API Layer (6-8 hours)

### Morning Session (3-4 hours): Commands & Queries

#### Task 2.1: Create Commands (1.5-2h)
**Owner**: Backend Developer

**Files to Create**:

**1. AddChildIssueCommand.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Commands/AddChildIssueCommand.cs

public record AddChildIssueCommand(
    Guid ParentIssueId,
    Guid ChildIssueId
) : IRequest<Result<IssueDto>>;

public class AddChildIssueCommandHandler : IRequestHandler<AddChildIssueCommand, Result<IssueDto>>
{
    private readonly IIssueRepository _issueRepository;
    private readonly ITenantContextAccessor _tenantContext;
    private readonly IMapper _mapper;

    public AddChildIssueCommandHandler(
        IIssueRepository issueRepository,
        ITenantContextAccessor tenantContext,
        IMapper mapper)
    {
        _issueRepository = issueRepository;
        _tenantContext = tenantContext;
        _mapper = mapper;
    }

    public async Task<Result<IssueDto>> Handle(
        AddChildIssueCommand request,
        CancellationToken cancellationToken)
    {
        var tenantId = _tenantContext.GetTenantId();

        // Get parent issue (with tenant filter)
        var parent = await _issueRepository.GetByIdAsync(
            request.ParentIssueId,
            cancellationToken);

        if (parent == null)
            return Result<IssueDto>.Failure("Parent issue not found");

        // Get child issue (with tenant filter)
        var child = await _issueRepository.GetByIdAsync(
            request.ChildIssueId,
            cancellationToken);

        if (child == null)
            return Result<IssueDto>.Failure("Child issue not found");

        // Set parent (domain logic validates)
        var result = child.SetParent(parent);

        if (!result.IsSuccess)
            return Result<IssueDto>.Failure(result.Error);

        // Save changes
        await _issueRepository.UpdateAsync(child, cancellationToken);
        await _issueRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

        // Return updated child issue
        var dto = _mapper.Map<IssueDto>(child);
        return Result<IssueDto>.Success(dto);
    }
}
```

**2. RemoveChildIssueCommand.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Commands/RemoveChildIssueCommand.cs

public record RemoveChildIssueCommand(
    Guid IssueId
) : IRequest<Result>;

public class RemoveChildIssueCommandHandler : IRequestHandler<RemoveChildIssueCommand, Result>
{
    private readonly IIssueRepository _issueRepository;
    private readonly ITenantContextAccessor _tenantContext;

    public RemoveChildIssueCommandHandler(
        IIssueRepository issueRepository,
        ITenantContextAccessor tenantContext)
    {
        _issueRepository = issueRepository;
        _tenantContext = tenantContext;
    }

    public async Task<Result> Handle(
        RemoveChildIssueCommand request,
        CancellationToken cancellationToken)
    {
        var tenantId = _tenantContext.GetTenantId();

        // Get issue (with tenant filter)
        var issue = await _issueRepository.GetByIdAsync(
            request.IssueId,
            cancellationToken);

        if (issue == null)
            return Result.Failure("Issue not found");

        // Remove parent
        var result = issue.RemoveParent();

        if (!result.IsSuccess)
            return result;

        // Save changes
        await _issueRepository.UpdateAsync(issue, cancellationToken);
        await _issueRepository.UnitOfWork.SaveChangesAsync(cancellationToken);

        return Result.Success();
    }
}
```

**Checklist**:
- [ ] Create `AddChildIssueCommand.cs` + handler
- [ ] Create `RemoveChildIssueCommand.cs` + handler
- [ ] Add tenant context validation
- [ ] Add authorization checks (future: role-based)
- [ ] Add FluentValidation rules
- [ ] Test command handlers (unit tests)

**Acceptance Criteria**:
- Commands follow CQRS pattern
- Tenant isolation enforced in handlers
- Domain validation errors propagated correctly
- Unit tests passing

---

#### Task 2.2: Create Queries (1-1.5h)
**Owner**: Backend Developer

**Files to Create**:

**1. GetIssueHierarchyQuery.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Queries/GetIssueHierarchyQuery.cs

public record GetIssueHierarchyQuery(
    Guid IssueId
) : IRequest<Result<IssueHierarchyDto>>;

public class GetIssueHierarchyQueryHandler : IRequestHandler<GetIssueHierarchyQuery, Result<IssueHierarchyDto>>
{
    private readonly IIssueRepository _issueRepository;
    private readonly ITenantContextAccessor _tenantContext;
    private readonly IMapper _mapper;

    public GetIssueHierarchyQueryHandler(
        IIssueRepository issueRepository,
        ITenantContextAccessor tenantContext,
        IMapper mapper)
    {
        _issueRepository = issueRepository;
        _tenantContext = tenantContext;
        _mapper = mapper;
    }

    public async Task<Result<IssueHierarchyDto>> Handle(
        GetIssueHierarchyQuery request,
        CancellationToken cancellationToken)
    {
        var tenantId = _tenantContext.GetTenantId();

        // Get issue with full hierarchy (using CTE for performance)
        var hierarchy = await _issueRepository.GetHierarchyAsync(
            request.IssueId,
            cancellationToken);

        if (hierarchy == null)
            return Result<IssueHierarchyDto>.Failure("Issue not found");

        // Map to DTO
        var dto = _mapper.Map<IssueHierarchyDto>(hierarchy);
        return Result<IssueHierarchyDto>.Success(dto);
    }
}

// DTO
public class IssueHierarchyDto
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IssueType Type { get; set; }
    public IssueStatus Status { get; set; }
    public int Depth { get; set; }
    public List<IssueHierarchyDto> Children { get; set; } = new();
}
```

**2. GetChildIssuesQuery.cs**
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Application/Queries/GetChildIssuesQuery.cs

public record GetChildIssuesQuery(
    Guid ParentIssueId
) : IRequest<Result<List<IssueDto>>>;

public class GetChildIssuesQueryHandler : IRequestHandler<GetChildIssuesQuery, Result<List<IssueDto>>>
{
    private readonly IIssueRepository _issueRepository;
    private readonly ITenantContextAccessor _tenantContext;
    private readonly IMapper _mapper;

    public GetChildIssuesQueryHandler(
        IIssueRepository issueRepository,
        ITenantContextAccessor tenantContext,
        IMapper mapper)
    {
        _issueRepository = issueRepository;
        _tenantContext = tenantContext;
        _mapper = mapper;
    }

    public async Task<Result<List<IssueDto>>> Handle(
        GetChildIssuesQuery request,
        CancellationToken cancellationToken)
    {
        var tenantId = _tenantContext.GetTenantId();

        // Get children (single level)
        var children = await _issueRepository.GetChildrenAsync(
            request.ParentIssueId,
            cancellationToken);

        // Map to DTOs
        var dtos = _mapper.Map<List<IssueDto>>(children);
        return Result<List<IssueDto>>.Success(dtos);
    }
}
```

**Checklist**:
- [ ] Create `GetIssueHierarchyQuery.cs` + handler
- [ ] Create `GetChildIssuesQuery.cs` + handler
- [ ] Implement repository methods (GetHierarchyAsync, GetChildrenAsync)
- [ ] Use PostgreSQL CTE for recursive queries
- [ ] Add query performance tests (< 50ms for 100+ issues)
- [ ] Test tenant isolation in queries

**Acceptance Criteria**:
- Queries return correct hierarchy data
- Performance < 50ms for 100+ issues
- Tenant isolation enforced
- No N+1 query problems

---

### Afternoon Session (3-4 hours): API Endpoints

#### Task 2.3: Add API Endpoints (1.5-2h)
**Owner**: Backend Developer

**File to Update**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Api/Controllers/IssuesController.cs

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class IssuesController : ControllerBase
{
    private readonly IMediator _mediator;

    public IssuesController(IMediator mediator)
    {
        _mediator = mediator;
    }

    // Existing endpoints...

    /// <summary>
    /// Add a child issue to a parent issue
    /// </summary>
    [HttpPost("{parentId}/add-child")]
    [ProducesResponseType(typeof(IssueDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> AddChildIssue(
        [FromRoute] Guid parentId,
        [FromBody] AddChildIssueRequest request)
    {
        var command = new AddChildIssueCommand(parentId, request.ChildIssueId);
        var result = await _mediator.Send(command);

        if (!result.IsSuccess)
            return BadRequest(result.Error);

        return Ok(result.Value);
    }

    /// <summary>
    /// Remove parent from an issue
    /// </summary>
    [HttpDelete("{issueId}/remove-parent")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> RemoveParent(
        [FromRoute] Guid issueId)
    {
        var command = new RemoveChildIssueCommand(issueId);
        var result = await _mediator.Send(command);

        if (!result.IsSuccess)
            return BadRequest(result.Error);

        return NoContent();
    }

    /// <summary>
    /// Get issue hierarchy (full tree)
    /// </summary>
    [HttpGet("{issueId}/hierarchy")]
    [ProducesResponseType(typeof(IssueHierarchyDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetHierarchy(
        [FromRoute] Guid issueId)
    {
        var query = new GetIssueHierarchyQuery(issueId);
        var result = await _mediator.Send(query);

        if (!result.IsSuccess)
            return NotFound(result.Error);

        return Ok(result.Value);
    }

    /// <summary>
    /// Get direct children of an issue
    /// </summary>
    [HttpGet("{issueId}/children")]
    [ProducesResponseType(typeof(List<IssueDto>), StatusCodes.Status200OK)]
    public async Task<IActionResult> GetChildren(
        [FromRoute] Guid issueId)
    {
        var query = new GetChildIssuesQuery(issueId);
        var result = await _mediator.Send(query);

        return Ok(result.Value);
    }
}

// Request DTO
public class AddChildIssueRequest
{
    public Guid ChildIssueId { get; set; }
}
```

**Checklist**:
- [ ] Add `POST /api/issues/{parentId}/add-child` endpoint
- [ ] Add `DELETE /api/issues/{issueId}/remove-parent` endpoint
- [ ] Add `GET /api/issues/{issueId}/hierarchy` endpoint
- [ ] Add `GET /api/issues/{issueId}/children` endpoint
- [ ] Add Swagger documentation (XML comments)
- [ ] Add authorization attributes
- [ ] Test endpoints with Postman/cURL

**Acceptance Criteria**:
- All 4 new endpoints working
- Swagger documentation complete
- Authorization enforced
- Proper HTTP status codes (200, 400, 404)

---

#### Task 2.4: Implement Repository Methods (1-1.5h)
**Owner**: Backend Developer

**File to Update**:
```csharp
// File: colaflow-api/src/Modules/IssueManagement/Infrastructure/Persistence/Repositories/IssueRepository.cs

public class IssueRepository : IIssueRepository
{
    private readonly IssueManagementDbContext _context;

    public IssueRepository(IssueManagementDbContext context)
    {
        _context = context;
    }

    // Existing methods...

    public async Task<IssueHierarchy?> GetHierarchyAsync(
        Guid issueId,
        CancellationToken cancellationToken = default)
    {
        // Use PostgreSQL CTE for recursive query
        var sql = @"
            WITH RECURSIVE hierarchy AS (
                -- Base case: Root issue
                SELECT
                    id,
                    tenant_id,
                    parent_issue_id,
                    title,
                    type,
                    status,
                    priority,
                    0 AS depth
                FROM issues
                WHERE id = @issueId
                  AND tenant_id = @tenantId

                UNION ALL

                -- Recursive case: Children
                SELECT
                    i.id,
                    i.tenant_id,
                    i.parent_issue_id,
                    i.title,
                    i.type,
                    i.status,
                    i.priority,
                    h.depth + 1
                FROM issues i
                INNER JOIN hierarchy h ON i.parent_issue_id = h.id
                WHERE i.tenant_id = @tenantId
                  AND h.depth < 3  -- Max depth limit
            )
            SELECT * FROM hierarchy
            ORDER BY depth, title;
        ";

        var tenantId = _context.TenantId;  // From DbContext

        var issues = await _context.Issues
            .FromSqlRaw(sql, new { issueId, tenantId })
            .ToListAsync(cancellationToken);

        if (!issues.Any())
            return null;

        // Build hierarchy tree
        return BuildHierarchyTree(issues);
    }

    public async Task<List<Issue>> GetChildrenAsync(
        Guid parentIssueId,
        CancellationToken cancellationToken = default)
    {
        return await _context.Issues
            .Where(i => i.ParentIssueId == parentIssueId)
            .OrderBy(i => i.CreatedAt)
            .ToListAsync(cancellationToken);
    }

    private IssueHierarchy BuildHierarchyTree(List<Issue> flatList)
    {
        var lookup = flatList.ToDictionary(i => i.Id);
        var root = flatList.First(i => i.Depth == 0);

        foreach (var issue in flatList.Where(i => i.ParentIssueId.HasValue))
        {
            if (lookup.TryGetValue(issue.ParentIssueId.Value, out var parent))
            {
                parent.ChildIssues.Add(issue);
            }
        }

        return new IssueHierarchy
        {
            RootIssue = root,
            AllIssues = flatList,
            TotalCount = flatList.Count
        };
    }
}

// Interface
public interface IIssueRepository : IRepository<Issue>
{
    // Existing methods...
    Task<IssueHierarchy?> GetHierarchyAsync(Guid issueId, CancellationToken cancellationToken = default);
    Task<List<Issue>> GetChildrenAsync(Guid parentIssueId, CancellationToken cancellationToken = default);
}

// Helper class
public class IssueHierarchy
{
    public Issue RootIssue { get; set; }
    public List<Issue> AllIssues { get; set; }
    public int TotalCount { get; set; }
}
```

**Checklist**:
- [ ] Implement `GetHierarchyAsync` using PostgreSQL CTE
- [ ] Implement `GetChildrenAsync` method
- [ ] Add `BuildHierarchyTree` helper method
- [ ] Test query performance (< 50ms)
- [ ] Test tenant isolation in raw SQL
- [ ] Test with 100+ issues in hierarchy

**Acceptance Criteria**:
- CTE query returns correct hierarchy
- Performance < 50ms for 100+ issues
- Tenant filter applied correctly
- Max depth limit enforced (3 levels)

---

#### Task 2.5: Integration Tests (1.5-2h)
**Owner**: Backend Developer + QA

**File to Create**:
```csharp
// File: colaflow-api/tests/Modules/IssueManagement.IntegrationTests/IssueHierarchyIntegrationTests.cs

public class IssueHierarchyIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly IServiceScope _scope;

    public IssueHierarchyIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.CreateClient();
        _scope = factory.Services.CreateScope();
    }

    [Fact]
    public async Task AddChildIssue_ValidHierarchy_ShouldSucceed()
    {
        // Arrange
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");
        var story = await CreateIssueAsync(IssueType.Story, "Story 1");

        var request = new AddChildIssueRequest { ChildIssueId = story.Id };

        // Act
        var response = await _client.PostAsJsonAsync(
            $"/api/issues/{epic.Id}/add-child",
            request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var updatedStory = await response.Content.ReadFromJsonAsync<IssueDto>();
        updatedStory.ParentIssueId.Should().Be(epic.Id);
    }

    [Fact]
    public async Task AddChildIssue_InvalidHierarchy_ShouldReturn400()
    {
        // Arrange
        var task = await CreateIssueAsync(IssueType.Task, "Task 1");
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");

        var request = new AddChildIssueRequest { ChildIssueId = epic.Id };

        // Act - Try to set Task as parent of Epic (invalid)
        var response = await _client.PostAsJsonAsync(
            $"/api/issues/{task.Id}/add-child",
            request);

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    [Fact]
    public async Task AddChildIssue_CircularDependency_ShouldReturn400()
    {
        // Arrange
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");
        var story = await CreateIssueAsync(IssueType.Story, "Story 1");

        await _client.PostAsJsonAsync(
            $"/api/issues/{epic.Id}/add-child",
            new { ChildIssueId = story.Id });

        // Act - Try to set Epic's parent as Story (circular)
        var response = await _client.PostAsJsonAsync(
            $"/api/issues/{story.Id}/add-child",
            new { ChildIssueId = epic.Id });

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
    }

    [Fact]
    public async Task AddChildIssue_CrossTenant_ShouldReturn404()
    {
        // Arrange
        var epicTenant1 = await CreateIssueAsync(IssueType.Epic, "Epic T1", tenantId: Guid.NewGuid());

        // Switch to tenant 2
        SetTenantContext(Guid.NewGuid());

        var storyTenant2 = await CreateIssueAsync(IssueType.Story, "Story T2");

        // Act - Try to link across tenants
        var response = await _client.PostAsJsonAsync(
            $"/api/issues/{epicTenant1.Id}/add-child",
            new { ChildIssueId = storyTenant2.Id });

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NotFound);  // Parent not found in tenant 2
    }

    [Fact]
    public async Task GetHierarchy_ThreeLevels_ShouldReturnFullTree()
    {
        // Arrange
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");
        var story = await CreateIssueAsync(IssueType.Story, "Story 1");
        var task = await CreateIssueAsync(IssueType.Task, "Task 1");

        await AddChildAsync(epic.Id, story.Id);
        await AddChildAsync(story.Id, task.Id);

        // Act
        var response = await _client.GetAsync($"/api/issues/{epic.Id}/hierarchy");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var hierarchy = await response.Content.ReadFromJsonAsync<IssueHierarchyDto>();
        hierarchy.Id.Should().Be(epic.Id);
        hierarchy.Children.Should().HaveCount(1);
        hierarchy.Children[0].Id.Should().Be(story.Id);
        hierarchy.Children[0].Children.Should().HaveCount(1);
        hierarchy.Children[0].Children[0].Id.Should().Be(task.Id);
    }

    [Fact]
    public async Task GetChildren_ParentWithTwoChildren_ShouldReturnBoth()
    {
        // Arrange
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");
        var story1 = await CreateIssueAsync(IssueType.Story, "Story 1");
        var story2 = await CreateIssueAsync(IssueType.Story, "Story 2");

        await AddChildAsync(epic.Id, story1.Id);
        await AddChildAsync(epic.Id, story2.Id);

        // Act
        var response = await _client.GetAsync($"/api/issues/{epic.Id}/children");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);

        var children = await response.Content.ReadFromJsonAsync<List<IssueDto>>();
        children.Should().HaveCount(2);
        children.Should().Contain(c => c.Id == story1.Id);
        children.Should().Contain(c => c.Id == story2.Id);
    }

    [Fact]
    public async Task RemoveParent_HasParent_ShouldSucceed()
    {
        // Arrange
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");
        var story = await CreateIssueAsync(IssueType.Story, "Story 1");
        await AddChildAsync(epic.Id, story.Id);

        // Act
        var response = await _client.DeleteAsync($"/api/issues/{story.Id}/remove-parent");

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.NoContent);

        // Verify parent removed
        var children = await GetChildrenAsync(epic.Id);
        children.Should().BeEmpty();
    }

    [Fact]
    public async Task HierarchyQuery_Performance_ShouldBeLessThan50ms()
    {
        // Arrange - Create large hierarchy (100+ issues)
        var epic = await CreateIssueAsync(IssueType.Epic, "Epic 1");

        for (int i = 0; i < 10; i++)
        {
            var story = await CreateIssueAsync(IssueType.Story, $"Story {i}");
            await AddChildAsync(epic.Id, story.Id);

            for (int j = 0; j < 10; j++)
            {
                var task = await CreateIssueAsync(IssueType.Task, $"Task {i}-{j}");
                await AddChildAsync(story.Id, task.Id);
            }
        }

        // Act
        var stopwatch = Stopwatch.StartNew();
        var response = await _client.GetAsync($"/api/issues/{epic.Id}/hierarchy");
        stopwatch.Stop();

        // Assert
        response.StatusCode.Should().Be(HttpStatusCode.OK);
        stopwatch.ElapsedMilliseconds.Should().BeLessThan(50);  // Performance requirement
    }

    // Helper methods...
    private async Task<IssueDto> CreateIssueAsync(IssueType type, string title, Guid? tenantId = null) { /* ... */ }
    private async Task AddChildAsync(Guid parentId, Guid childId) { /* ... */ }
    private async Task<List<IssueDto>> GetChildrenAsync(Guid parentId) { /* ... */ }
    private void SetTenantContext(Guid tenantId) { /* ... */ }
}
```

**Checklist**:
- [ ] Test valid hierarchy (Epic → Story → Task)
- [ ] Test invalid hierarchy (Task → Epic)
- [ ] Test circular dependency prevention
- [ ] Test cross-tenant isolation
- [ ] Test GetHierarchy endpoint
- [ ] Test GetChildren endpoint
- [ ] Test RemoveParent endpoint
- [ ] Test performance (< 50ms for 100+ issues)
- [ ] All 10+ integration tests passing

**Acceptance Criteria**:
- All integration tests pass (10+/10+)
- Performance tests pass (< 50ms)
- Multi-tenant isolation verified
- No N+1 query issues

---

## Day 16 Evening: Frontend Integration (Optional, 2-3h)

### Task 3.1: Update Kanban Board (1-1.5h)
**Owner**: Frontend Developer

**Files to Update**:
```typescript
// File: colaflow-frontend/src/components/Kanban/IssueCard.tsx

interface IssueCardProps {
  issue: Issue;
  onUpdate: (issue: Issue) => void;
}

export const IssueCard: React.FC<IssueCardProps> = ({ issue, onUpdate }) => {
  // Existing code...

  // NEW: Show parent issue breadcrumb
  const renderParentBreadcrumb = () => {
    if (!issue.parentIssue) return null;

    return (
      <div className="text-xs text-gray-500 mb-2">
        <Link to={`/issues/${issue.parentIssue.id}`}>
          <IconType type={issue.parentIssue.type} />
          {issue.parentIssue.title}
        </Link>
      </div>
    );
  };

  // NEW: Show child count
  const renderChildCount = () => {
    if (issue.childCount === 0) return null;

    return (
      <div className="text-xs text-blue-600 mt-2">
        <IconChildIssues />
        {issue.childCount} subtasks
      </div>
    );
  };

  return (
    <div className="issue-card">
      {renderParentBreadcrumb()}

      <h3>{issue.title}</h3>
      <p>{issue.description}</p>

      {renderChildCount()}

      {/* Existing code... */}
    </div>
  );
};
```

**Checklist**:
- [ ] Display parent issue breadcrumb on cards
- [ ] Display child issue count
- [ ] Add "Create Child Issue" button
- [ ] Test UI updates with real data

**Acceptance Criteria**:
- Kanban board shows hierarchy information
- UI is responsive and intuitive
- No performance degradation

---

## Success Criteria (Day 15-16)

### Functional Requirements
- [x] Can create Epic → Story → Task hierarchy
- [x] Can add/remove parent-child relationships via API
- [x] Can query full hierarchy tree
- [x] Hierarchy rules enforced (Epic → Story → Task only)
- [x] Circular dependency prevention works
- [x] Max depth 3 levels enforced

### Non-Functional Requirements
- [x] Query performance < 50ms (100+ issues)
- [x] Multi-tenant isolation 100% verified
- [x] Backward compatible (no breaking changes)
- [x] Integration tests pass rate ≥ 95% (10+/10+)
- [x] API response time < 100ms

### Documentation Requirements
- [x] API documentation updated (Swagger)
- [x] Database schema documented
- [x] ADR-035 architecture decision recorded
- [x] Implementation notes for future reference

---

## Risk Mitigation

### Risk 1: Performance Issues
**Mitigation**: Use PostgreSQL CTE for recursive queries, add index on parent_issue_id

### Risk 2: Multi-Tenant Security
**Mitigation**: All queries filtered by TenantId, integration tests verify isolation

### Risk 3: Breaking Changes
**Mitigation**: ParentIssueId is nullable, existing queries unaffected

### Risk 4: Circular Dependencies
**Mitigation**: Domain logic validates before save, integration tests verify

---

## Testing Checklist

### Unit Tests (Day 15)
- [ ] Domain logic: 10+ test cases
- [ ] All edge cases covered
- [ ] 100% code coverage for hierarchy logic

### Integration Tests (Day 16)
- [ ] Valid hierarchy: Epic → Story → Task
- [ ] Invalid hierarchy: Task → Epic (rejected)
- [ ] Circular dependency (rejected)
- [ ] Cross-tenant (rejected)
- [ ] GetHierarchy endpoint
- [ ] GetChildren endpoint
- [ ] RemoveParent endpoint
- [ ] Performance: < 50ms for 100+ issues
- [ ] Multi-tenant isolation verified

### Manual Testing (Day 16 Evening)
- [ ] Postman: All 4 new endpoints work
- [ ] Frontend: Kanban shows hierarchy
- [ ] Frontend: Create child issue works
- [ ] Frontend: Real-time updates work (SignalR)

---

## Delivery Checklist

### Code Artifacts
- [ ] Database migration file
- [ ] Issue entity updated
- [ ] 2 new commands (AddChild, RemoveParent)
- [ ] 2 new queries (GetHierarchy, GetChildren)
- [ ] 4 new API endpoints
- [ ] Repository methods (CTE queries)
- [ ] 10+ unit tests
- [ ] 10+ integration tests
- [ ] Frontend updates (optional)

### Documentation
- [ ] API documentation (Swagger)
- [ ] ADR-035 architecture decision
- [ ] M1_REMAINING_TASKS.md updated
- [ ] Database schema documented
- [ ] Performance test results

### Git Commits
- [ ] Day 15 AM: Database migration + EF Core config
- [ ] Day 15 PM: Domain logic + unit tests
- [ ] Day 16 AM: Commands + queries
- [ ] Day 16 PM: API endpoints + integration tests
- [ ] Day 16 Evening: Frontend integration (optional)

---

## Next Steps (Day 17)

### If On Schedule
- Start Audit Log System Phase 1 (Day 17-23)
- Run full regression test suite
- Code review for hierarchy feature

### If Behind Schedule
- Focus on P0 features only (skip frontend integration)
- Defer performance optimization to Day 17
- Request additional development time

---

## Contact & Escalation

**Technical Questions**: Backend Agent, Architect Agent
**Requirements Clarification**: Product Manager Agent
**Testing Issues**: QA Agent
**Progress Updates**: Main Coordinator Agent

---

**Document Version**: 1.0
**Last Updated**: 2025-11-04
**Next Review**: End of Day 16 (2025-11-06)
