1723 lines
58 KiB
Markdown
1723 lines
58 KiB
Markdown
# EF Core Migration Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the raw ADO.NET data layer with Entity Framework Core and LINQ queries.
|
|
|
|
**Architecture:** Single `ClaudeDoDbContext` in `ClaudeDo.Data` with Fluent API configuration. All 6 repositories rewritten to LINQ. EF Core migrations replace `schema.sql` and `SchemaInitializer`. Atomic queue claim kept as `FromSqlRaw`.
|
|
|
|
**Tech Stack:** .NET 8.0, Microsoft.EntityFrameworkCore.Sqlite 8.x, EF Core Migrations
|
|
|
|
**Spec:** `docs/superpowers/specs/2026-04-16-efcore-migration-design.md`
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
### New files
|
|
- `src/ClaudeDo.Data/ClaudeDoDbContext.cs` — DbContext with DbSets and `OnModelCreating`
|
|
- `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs` — Fluent API for TaskEntity
|
|
- `src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs` — Fluent API for ListEntity
|
|
- `src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs` — Fluent API for TagEntity
|
|
- `src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs` — Fluent API for ListConfigEntity
|
|
- `src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs` — Fluent API for WorktreeEntity
|
|
- `src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs` — Fluent API for TaskRunEntity
|
|
- `src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs` — Fluent API for SubtaskEntity
|
|
- `src/ClaudeDo.Data/Migrations/` — Generated by `dotnet ef migrations add`
|
|
|
|
### Modified files
|
|
- `src/ClaudeDo.Data/ClaudeDo.Data.csproj` — Swap packages
|
|
- `src/ClaudeDo.Data/Models/TaskEntity.cs` — Add navigation properties
|
|
- `src/ClaudeDo.Data/Models/ListEntity.cs` — Add navigation properties
|
|
- `src/ClaudeDo.Data/Models/TagEntity.cs` — Add navigation properties
|
|
- `src/ClaudeDo.Data/Models/TaskRunEntity.cs` — Add navigation property
|
|
- `src/ClaudeDo.Data/Models/SubtaskEntity.cs` — Add navigation property
|
|
- `src/ClaudeDo.Data/Models/WorktreeEntity.cs` — Add navigation property
|
|
- `src/ClaudeDo.Data/Models/ListConfigEntity.cs` — Add navigation property
|
|
- `src/ClaudeDo.Data/Repositories/TagRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.Data/Repositories/SubtaskRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.Data/Repositories/ListRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.Data/Repositories/TaskRunRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.Data/Repositories/TaskRepository.cs` — Rewrite to EF Core
|
|
- `src/ClaudeDo.App/Program.cs` — EF Core DI registration
|
|
- `src/ClaudeDo.Worker/Program.cs` — EF Core DI registration
|
|
- `src/ClaudeDo.Worker/Services/QueueService.cs` — Adapt to scoped repos via IDbContextFactory
|
|
- `src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs` — Adapt to scoped repos via IDbContextFactory
|
|
- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — Adapt to scoped repos via IDbContextFactory
|
|
- `src/ClaudeDo.Worker/Runner/WorktreeManager.cs` — Adapt to scoped repos via IDbContextFactory
|
|
- `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs` — Adapt to IDbContextFactory
|
|
- `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` — Adapt to IDbContextFactory
|
|
- `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` — Adapt to IDbContextFactory
|
|
- `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs` — Adapt to IDbContextFactory
|
|
- `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj` — Swap packages
|
|
- `tests/ClaudeDo.Worker.Tests/Infrastructure/DbFixture.cs` — EF Core fixture
|
|
|
|
### Deleted files
|
|
- `src/ClaudeDo.Data/SqliteConnectionFactory.cs`
|
|
- `src/ClaudeDo.Data/SchemaInitializer.cs`
|
|
- `schema/schema.sql`
|
|
|
|
---
|
|
|
|
### Task 1: Update Package References
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/ClaudeDo.Data.csproj`
|
|
- Modify: `tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj`
|
|
|
|
- [ ] **Step 1: Update ClaudeDo.Data.csproj**
|
|
|
|
Replace package references and remove the embedded resource:
|
|
|
|
```xml
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
<Nullable>enable</Nullable>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.11">
|
|
<PrivateAssets>all</PrivateAssets>
|
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
</PackageReference>
|
|
</ItemGroup>
|
|
|
|
</Project>
|
|
```
|
|
|
|
- [ ] **Step 2: Update Worker.Tests.csproj**
|
|
|
|
Replace `Microsoft.Data.Sqlite` with EF Core:
|
|
|
|
```xml
|
|
<ItemGroup>
|
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.11" />
|
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
|
<PackageReference Include="xunit" Version="2.5.3" />
|
|
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
- [ ] **Step 3: Verify packages restore**
|
|
|
|
Run: `dotnet restore ClaudeDo.slnx`
|
|
Expected: Restore succeeds (build will fail — repositories still reference old types).
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/ClaudeDo.Data.csproj tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj
|
|
git commit -m "chore(data): swap Microsoft.Data.Sqlite for EF Core packages"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add Navigation Properties to Models
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Models/TaskEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/ListEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/TagEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/WorktreeEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/ListConfigEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/TaskRunEntity.cs`
|
|
- Modify: `src/ClaudeDo.Data/Models/SubtaskEntity.cs`
|
|
|
|
- [ ] **Step 1: Update TaskEntity.cs**
|
|
|
|
Add navigation properties at the end of the class, before the closing brace:
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public enum TaskStatus
|
|
{
|
|
Manual,
|
|
Queued,
|
|
Running,
|
|
Done,
|
|
Failed,
|
|
}
|
|
|
|
public sealed class TaskEntity
|
|
{
|
|
public required string Id { get; init; }
|
|
public required string ListId { get; init; }
|
|
public required string Title { get; set; }
|
|
public string? Description { get; set; }
|
|
public TaskStatus Status { get; set; } = TaskStatus.Manual;
|
|
public DateTime? ScheduledFor { get; set; }
|
|
public string? Result { get; set; }
|
|
public string? LogPath { get; set; }
|
|
public required DateTime CreatedAt { get; init; }
|
|
public DateTime? StartedAt { get; set; }
|
|
public DateTime? FinishedAt { get; set; }
|
|
public string CommitType { get; set; } = "chore";
|
|
public string? Model { get; set; }
|
|
public string? SystemPrompt { get; set; }
|
|
public string? AgentPath { get; set; }
|
|
|
|
// Navigation properties
|
|
public ListEntity List { get; set; } = null!;
|
|
public WorktreeEntity? Worktree { get; set; }
|
|
public ICollection<TagEntity> Tags { get; set; } = new List<TagEntity>();
|
|
public ICollection<TaskRunEntity> Runs { get; set; } = new List<TaskRunEntity>();
|
|
public ICollection<SubtaskEntity> Subtasks { get; set; } = new List<SubtaskEntity>();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update ListEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public sealed class ListEntity
|
|
{
|
|
public required string Id { get; init; }
|
|
public required string Name { get; set; }
|
|
public required DateTime CreatedAt { get; init; }
|
|
public string? WorkingDir { get; set; }
|
|
public string DefaultCommitType { get; set; } = "chore";
|
|
|
|
// Navigation properties
|
|
public ListConfigEntity? Config { get; set; }
|
|
public ICollection<TaskEntity> Tasks { get; set; } = new List<TaskEntity>();
|
|
public ICollection<TagEntity> Tags { get; set; } = new List<TagEntity>();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Update TagEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public sealed class TagEntity
|
|
{
|
|
public long Id { get; init; }
|
|
public required string Name { get; set; }
|
|
|
|
// Navigation properties
|
|
public ICollection<ListEntity> Lists { get; set; } = new List<ListEntity>();
|
|
public ICollection<TaskEntity> Tasks { get; set; } = new List<TaskEntity>();
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Update WorktreeEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public enum WorktreeState
|
|
{
|
|
Active,
|
|
Merged,
|
|
Discarded,
|
|
Kept,
|
|
}
|
|
|
|
public sealed class WorktreeEntity
|
|
{
|
|
public required string TaskId { get; init; }
|
|
public required string Path { get; set; }
|
|
public required string BranchName { get; set; }
|
|
public required string BaseCommit { get; set; }
|
|
public string? HeadCommit { get; set; }
|
|
public string? DiffStat { get; set; }
|
|
public WorktreeState State { get; set; } = WorktreeState.Active;
|
|
public required DateTime CreatedAt { get; init; }
|
|
|
|
// Navigation property
|
|
public TaskEntity Task { get; set; } = null!;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Update ListConfigEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public sealed class ListConfigEntity
|
|
{
|
|
public required string ListId { get; init; }
|
|
public string? Model { get; set; }
|
|
public string? SystemPrompt { get; set; }
|
|
public string? AgentPath { get; set; }
|
|
|
|
// Navigation property
|
|
public ListEntity List { get; set; } = null!;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Update TaskRunEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public sealed class TaskRunEntity
|
|
{
|
|
public required string Id { get; init; }
|
|
public required string TaskId { get; init; }
|
|
public required int RunNumber { get; init; }
|
|
public string? SessionId { get; set; }
|
|
public required bool IsRetry { get; init; }
|
|
public required string Prompt { get; init; }
|
|
public string? ResultMarkdown { get; set; }
|
|
public string? StructuredOutputJson { get; set; }
|
|
public string? ErrorMarkdown { get; set; }
|
|
public int? ExitCode { get; set; }
|
|
public int? TurnCount { get; set; }
|
|
public int? TokensIn { get; set; }
|
|
public int? TokensOut { get; set; }
|
|
public string? LogPath { get; set; }
|
|
public DateTime? StartedAt { get; set; }
|
|
public DateTime? FinishedAt { get; set; }
|
|
|
|
// Navigation property
|
|
public TaskEntity Task { get; set; } = null!;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Update SubtaskEntity.cs**
|
|
|
|
```csharp
|
|
namespace ClaudeDo.Data.Models;
|
|
|
|
public sealed class SubtaskEntity
|
|
{
|
|
public required string Id { get; init; }
|
|
public required string TaskId { get; init; }
|
|
public required string Title { get; set; }
|
|
public bool Completed { get; set; }
|
|
public int OrderNum { get; set; }
|
|
public required DateTime CreatedAt { get; init; }
|
|
|
|
// Navigation property
|
|
public TaskEntity Task { get; set; } = null!;
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 8: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Models/
|
|
git commit -m "feat(data): add navigation properties to all entity models"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Create ClaudeDoDbContext and Fluent API Configurations
|
|
|
|
**Files:**
|
|
- Create: `src/ClaudeDo.Data/ClaudeDoDbContext.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/ListEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/TagEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/ListConfigEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/WorktreeEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/TaskRunEntityConfiguration.cs`
|
|
- Create: `src/ClaudeDo.Data/Configuration/SubtaskEntityConfiguration.cs`
|
|
|
|
- [ ] **Step 1: Create ClaudeDoDbContext.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data;
|
|
|
|
public class ClaudeDoDbContext : DbContext
|
|
{
|
|
public ClaudeDoDbContext(DbContextOptions<ClaudeDoDbContext> options) : base(options) { }
|
|
|
|
public DbSet<TaskEntity> Tasks => Set<TaskEntity>();
|
|
public DbSet<ListEntity> Lists => Set<ListEntity>();
|
|
public DbSet<TagEntity> Tags => Set<TagEntity>();
|
|
public DbSet<ListConfigEntity> ListConfigs => Set<ListConfigEntity>();
|
|
public DbSet<WorktreeEntity> Worktrees => Set<WorktreeEntity>();
|
|
public DbSet<TaskRunEntity> TaskRuns => Set<TaskRunEntity>();
|
|
public DbSet<SubtaskEntity> Subtasks => Set<SubtaskEntity>();
|
|
|
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
{
|
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(ClaudeDoDbContext).Assembly);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Create TaskEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class TaskEntityConfiguration : IEntityTypeConfiguration<TaskEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TaskEntity> builder)
|
|
{
|
|
builder.ToTable("tasks");
|
|
|
|
builder.HasKey(t => t.Id);
|
|
builder.Property(t => t.Id).HasColumnName("id");
|
|
builder.Property(t => t.ListId).HasColumnName("list_id").IsRequired();
|
|
builder.Property(t => t.Title).HasColumnName("title").IsRequired();
|
|
builder.Property(t => t.Description).HasColumnName("description");
|
|
builder.Property(t => t.Status).HasColumnName("status").IsRequired()
|
|
.HasConversion(
|
|
v => v switch
|
|
{
|
|
TaskStatus.Manual => "manual",
|
|
TaskStatus.Queued => "queued",
|
|
TaskStatus.Running => "running",
|
|
TaskStatus.Done => "done",
|
|
TaskStatus.Failed => "failed",
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
},
|
|
v => v switch
|
|
{
|
|
"manual" => TaskStatus.Manual,
|
|
"queued" => TaskStatus.Queued,
|
|
"running" => TaskStatus.Running,
|
|
"done" => TaskStatus.Done,
|
|
"failed" => TaskStatus.Failed,
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
});
|
|
builder.Property(t => t.ScheduledFor).HasColumnName("scheduled_for");
|
|
builder.Property(t => t.Result).HasColumnName("result");
|
|
builder.Property(t => t.LogPath).HasColumnName("log_path");
|
|
builder.Property(t => t.CreatedAt).HasColumnName("created_at").IsRequired();
|
|
builder.Property(t => t.StartedAt).HasColumnName("started_at");
|
|
builder.Property(t => t.FinishedAt).HasColumnName("finished_at");
|
|
builder.Property(t => t.CommitType).HasColumnName("commit_type").IsRequired().HasDefaultValue("chore");
|
|
builder.Property(t => t.Model).HasColumnName("model");
|
|
builder.Property(t => t.SystemPrompt).HasColumnName("system_prompt");
|
|
builder.Property(t => t.AgentPath).HasColumnName("agent_path");
|
|
|
|
builder.HasOne(t => t.List)
|
|
.WithMany(l => l.Tasks)
|
|
.HasForeignKey(t => t.ListId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasOne(t => t.Worktree)
|
|
.WithOne(w => w.Task)
|
|
.HasForeignKey<WorktreeEntity>(w => w.TaskId);
|
|
|
|
builder.HasMany(t => t.Tags)
|
|
.WithMany(tag => tag.Tasks)
|
|
.UsingEntity("task_tags",
|
|
l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade),
|
|
r => r.HasOne(typeof(TaskEntity)).WithMany().HasForeignKey("task_id").OnDelete(DeleteBehavior.Cascade),
|
|
j =>
|
|
{
|
|
j.HasKey("task_id", "tag_id");
|
|
j.ToTable("task_tags");
|
|
});
|
|
|
|
builder.HasIndex(t => t.ListId).HasDatabaseName("idx_tasks_list_id");
|
|
builder.HasIndex(t => t.Status).HasDatabaseName("idx_tasks_status");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 3: Create ListEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class ListEntityConfiguration : IEntityTypeConfiguration<ListEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<ListEntity> builder)
|
|
{
|
|
builder.ToTable("lists");
|
|
|
|
builder.HasKey(l => l.Id);
|
|
builder.Property(l => l.Id).HasColumnName("id");
|
|
builder.Property(l => l.Name).HasColumnName("name").IsRequired();
|
|
builder.Property(l => l.CreatedAt).HasColumnName("created_at").IsRequired();
|
|
builder.Property(l => l.WorkingDir).HasColumnName("working_dir");
|
|
builder.Property(l => l.DefaultCommitType).HasColumnName("default_commit_type").IsRequired().HasDefaultValue("chore");
|
|
|
|
builder.HasOne(l => l.Config)
|
|
.WithOne(c => c.List)
|
|
.HasForeignKey<ListConfigEntity>(c => c.ListId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasMany(l => l.Tags)
|
|
.WithMany(tag => tag.Lists)
|
|
.UsingEntity("list_tags",
|
|
l => l.HasOne(typeof(TagEntity)).WithMany().HasForeignKey("tag_id").OnDelete(DeleteBehavior.Cascade),
|
|
r => r.HasOne(typeof(ListEntity)).WithMany().HasForeignKey("list_id").OnDelete(DeleteBehavior.Cascade),
|
|
j =>
|
|
{
|
|
j.HasKey("list_id", "tag_id");
|
|
j.ToTable("list_tags");
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 4: Create TagEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class TagEntityConfiguration : IEntityTypeConfiguration<TagEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TagEntity> builder)
|
|
{
|
|
builder.ToTable("tags");
|
|
|
|
builder.HasKey(t => t.Id);
|
|
builder.Property(t => t.Id).HasColumnName("id").ValueGeneratedOnAdd();
|
|
builder.Property(t => t.Name).HasColumnName("name").IsRequired();
|
|
builder.HasIndex(t => t.Name).IsUnique();
|
|
|
|
builder.HasData(
|
|
new TagEntity { Id = 1, Name = "agent" },
|
|
new TagEntity { Id = 2, Name = "manual" });
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 5: Create ListConfigEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class ListConfigEntityConfiguration : IEntityTypeConfiguration<ListConfigEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<ListConfigEntity> builder)
|
|
{
|
|
builder.ToTable("list_config");
|
|
|
|
builder.HasKey(c => c.ListId);
|
|
builder.Property(c => c.ListId).HasColumnName("list_id");
|
|
builder.Property(c => c.Model).HasColumnName("model");
|
|
builder.Property(c => c.SystemPrompt).HasColumnName("system_prompt");
|
|
builder.Property(c => c.AgentPath).HasColumnName("agent_path");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 6: Create WorktreeEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class WorktreeEntityConfiguration : IEntityTypeConfiguration<WorktreeEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<WorktreeEntity> builder)
|
|
{
|
|
builder.ToTable("worktrees");
|
|
|
|
builder.HasKey(w => w.TaskId);
|
|
builder.Property(w => w.TaskId).HasColumnName("task_id");
|
|
builder.Property(w => w.Path).HasColumnName("path").IsRequired();
|
|
builder.Property(w => w.BranchName).HasColumnName("branch_name").IsRequired();
|
|
builder.Property(w => w.BaseCommit).HasColumnName("base_commit").IsRequired();
|
|
builder.Property(w => w.HeadCommit).HasColumnName("head_commit");
|
|
builder.Property(w => w.DiffStat).HasColumnName("diff_stat");
|
|
builder.Property(w => w.State).HasColumnName("state").IsRequired()
|
|
.HasDefaultValue(WorktreeState.Active)
|
|
.HasConversion(
|
|
v => v switch
|
|
{
|
|
WorktreeState.Active => "active",
|
|
WorktreeState.Merged => "merged",
|
|
WorktreeState.Discarded => "discarded",
|
|
WorktreeState.Kept => "kept",
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
},
|
|
v => v switch
|
|
{
|
|
"active" => WorktreeState.Active,
|
|
"merged" => WorktreeState.Merged,
|
|
"discarded" => WorktreeState.Discarded,
|
|
"kept" => WorktreeState.Kept,
|
|
_ => throw new ArgumentOutOfRangeException()
|
|
});
|
|
builder.Property(w => w.CreatedAt).HasColumnName("created_at").IsRequired();
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 7: Create TaskRunEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class TaskRunEntityConfiguration : IEntityTypeConfiguration<TaskRunEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<TaskRunEntity> builder)
|
|
{
|
|
builder.ToTable("task_runs");
|
|
|
|
builder.HasKey(r => r.Id);
|
|
builder.Property(r => r.Id).HasColumnName("id");
|
|
builder.Property(r => r.TaskId).HasColumnName("task_id").IsRequired();
|
|
builder.Property(r => r.RunNumber).HasColumnName("run_number").IsRequired();
|
|
builder.Property(r => r.SessionId).HasColumnName("session_id");
|
|
builder.Property(r => r.IsRetry).HasColumnName("is_retry").IsRequired().HasDefaultValue(false);
|
|
builder.Property(r => r.Prompt).HasColumnName("prompt").IsRequired();
|
|
builder.Property(r => r.ResultMarkdown).HasColumnName("result_markdown");
|
|
builder.Property(r => r.StructuredOutputJson).HasColumnName("structured_output");
|
|
builder.Property(r => r.ErrorMarkdown).HasColumnName("error_markdown");
|
|
builder.Property(r => r.ExitCode).HasColumnName("exit_code");
|
|
builder.Property(r => r.TurnCount).HasColumnName("turn_count");
|
|
builder.Property(r => r.TokensIn).HasColumnName("tokens_in");
|
|
builder.Property(r => r.TokensOut).HasColumnName("tokens_out");
|
|
builder.Property(r => r.LogPath).HasColumnName("log_path");
|
|
builder.Property(r => r.StartedAt).HasColumnName("started_at");
|
|
builder.Property(r => r.FinishedAt).HasColumnName("finished_at");
|
|
|
|
builder.HasOne(r => r.Task)
|
|
.WithMany(t => t.Runs)
|
|
.HasForeignKey(r => r.TaskId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasIndex(r => r.TaskId).HasDatabaseName("idx_task_runs_task_id");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 8: Create SubtaskEntityConfiguration.cs**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
|
|
namespace ClaudeDo.Data.Configuration;
|
|
|
|
public class SubtaskEntityConfiguration : IEntityTypeConfiguration<SubtaskEntity>
|
|
{
|
|
public void Configure(EntityTypeBuilder<SubtaskEntity> builder)
|
|
{
|
|
builder.ToTable("subtasks");
|
|
|
|
builder.HasKey(s => s.Id);
|
|
builder.Property(s => s.Id).HasColumnName("id");
|
|
builder.Property(s => s.TaskId).HasColumnName("task_id").IsRequired();
|
|
builder.Property(s => s.Title).HasColumnName("title").IsRequired();
|
|
builder.Property(s => s.Completed).HasColumnName("completed").IsRequired().HasDefaultValue(false);
|
|
builder.Property(s => s.OrderNum).HasColumnName("order_num").IsRequired();
|
|
builder.Property(s => s.CreatedAt).HasColumnName("created_at").IsRequired();
|
|
|
|
builder.HasOne(s => s.Task)
|
|
.WithMany(t => t.Subtasks)
|
|
.HasForeignKey(s => s.TaskId)
|
|
.OnDelete(DeleteBehavior.Cascade);
|
|
|
|
builder.HasIndex(s => s.TaskId).HasDatabaseName("idx_subtasks_task_id");
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 9: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/ClaudeDoDbContext.cs src/ClaudeDo.Data/Configuration/
|
|
git commit -m "feat(data): add ClaudeDoDbContext with Fluent API configurations"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Rewrite TagRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/TagRepository.cs`
|
|
|
|
- [ ] **Step 1: Rewrite TagRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class TagRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public TagRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
public async Task<List<TagEntity>> GetAllAsync(CancellationToken ct = default)
|
|
{
|
|
return await _context.Tags.OrderBy(t => t.Id).ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<long> GetOrCreateAsync(string name, CancellationToken ct = default)
|
|
{
|
|
var existing = await _context.Tags.FirstOrDefaultAsync(t => t.Name == name, ct);
|
|
if (existing is not null)
|
|
return existing.Id;
|
|
|
|
var tag = new TagEntity { Name = name };
|
|
_context.Tags.Add(tag);
|
|
await _context.SaveChangesAsync(ct);
|
|
return tag.Id;
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/TagRepository.cs
|
|
git commit -m "refactor(data): rewrite TagRepository to EF Core LINQ"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Rewrite SubtaskRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/SubtaskRepository.cs`
|
|
|
|
- [ ] **Step 1: Rewrite SubtaskRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class SubtaskRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public SubtaskRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
public async Task AddAsync(SubtaskEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Subtasks.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<List<SubtaskEntity>> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Subtasks
|
|
.Where(s => s.TaskId == taskId)
|
|
.OrderBy(s => s.OrderNum)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(SubtaskEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Subtasks.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string subtaskId, CancellationToken ct = default)
|
|
{
|
|
await _context.Subtasks.Where(s => s.Id == subtaskId).ExecuteDeleteAsync(ct);
|
|
}
|
|
|
|
public async Task DeleteByTaskIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
await _context.Subtasks.Where(s => s.TaskId == taskId).ExecuteDeleteAsync(ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/SubtaskRepository.cs
|
|
git commit -m "refactor(data): rewrite SubtaskRepository to EF Core LINQ"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Rewrite WorktreeRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/WorktreeRepository.cs`
|
|
|
|
- [ ] **Step 1: Rewrite WorktreeRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class WorktreeRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public WorktreeRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
public async Task AddAsync(WorktreeEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Worktrees.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<WorktreeEntity?> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Worktrees.FirstOrDefaultAsync(w => w.TaskId == taskId, ct);
|
|
}
|
|
|
|
public async Task UpdateHeadAsync(string taskId, string headCommit, string? diffStat, CancellationToken ct = default)
|
|
{
|
|
await _context.Worktrees
|
|
.Where(w => w.TaskId == taskId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(w => w.HeadCommit, headCommit)
|
|
.SetProperty(w => w.DiffStat, diffStat), ct);
|
|
}
|
|
|
|
public async Task SetStateAsync(string taskId, WorktreeState state, CancellationToken ct = default)
|
|
{
|
|
await _context.Worktrees
|
|
.Where(w => w.TaskId == taskId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(w => w.State, state), ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
await _context.Worktrees.Where(w => w.TaskId == taskId).ExecuteDeleteAsync(ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/WorktreeRepository.cs
|
|
git commit -m "refactor(data): rewrite WorktreeRepository to EF Core LINQ"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Rewrite ListRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/ListRepository.cs`
|
|
|
|
- [ ] **Step 1: Rewrite ListRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class ListRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public ListRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
public async Task AddAsync(ListEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Lists.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(ListEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Lists.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
await _context.Lists.Where(l => l.Id == listId).ExecuteDeleteAsync(ct);
|
|
}
|
|
|
|
public async Task<ListEntity?> GetByIdAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Lists.FirstOrDefaultAsync(l => l.Id == listId, ct);
|
|
}
|
|
|
|
public async Task<List<ListEntity>> GetAllAsync(CancellationToken ct = default)
|
|
{
|
|
return await _context.Lists.OrderBy(l => l.CreatedAt).ToListAsync(ct);
|
|
}
|
|
|
|
// Tag management via junction table
|
|
|
|
public async Task<List<TagEntity>> GetTagsAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Lists
|
|
.Where(l => l.Id == listId)
|
|
.SelectMany(l => l.Tags)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task AddTagAsync(string listId, long tagId, CancellationToken ct = default)
|
|
{
|
|
var list = await _context.Lists.Include(l => l.Tags).FirstAsync(l => l.Id == listId, ct);
|
|
var tag = await _context.Tags.FindAsync([tagId], ct);
|
|
if (tag is not null && !list.Tags.Any(t => t.Id == tagId))
|
|
{
|
|
list.Tags.Add(tag);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
|
|
public async Task RemoveTagAsync(string listId, long tagId, CancellationToken ct = default)
|
|
{
|
|
var list = await _context.Lists.Include(l => l.Tags).FirstAsync(l => l.Id == listId, ct);
|
|
var tag = list.Tags.FirstOrDefault(t => t.Id == tagId);
|
|
if (tag is not null)
|
|
{
|
|
list.Tags.Remove(tag);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
|
|
// Config management (1:1 with list)
|
|
|
|
public async Task<ListConfigEntity?> GetConfigAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
return await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == listId, ct);
|
|
}
|
|
|
|
public async Task UpsertConfigAsync(ListConfigEntity config, CancellationToken ct = default)
|
|
{
|
|
var existing = await _context.ListConfigs.FirstOrDefaultAsync(c => c.ListId == config.ListId, ct);
|
|
if (existing is null)
|
|
{
|
|
_context.ListConfigs.Add(config);
|
|
}
|
|
else
|
|
{
|
|
existing.Model = config.Model;
|
|
existing.SystemPrompt = config.SystemPrompt;
|
|
existing.AgentPath = config.AgentPath;
|
|
}
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/ListRepository.cs
|
|
git commit -m "refactor(data): rewrite ListRepository to EF Core LINQ"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Rewrite TaskRunRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/TaskRunRepository.cs`
|
|
|
|
- [ ] **Step 1: Rewrite TaskRunRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class TaskRunRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public TaskRunRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
public async Task AddAsync(TaskRunEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.TaskRuns.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(TaskRunEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.TaskRuns.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task<TaskRunEntity?> GetByIdAsync(string id, CancellationToken ct = default)
|
|
{
|
|
return await _context.TaskRuns.FirstOrDefaultAsync(r => r.Id == id, ct);
|
|
}
|
|
|
|
public async Task<List<TaskRunEntity>> GetByTaskIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.TaskRuns
|
|
.Where(r => r.TaskId == taskId)
|
|
.OrderBy(r => r.RunNumber)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<TaskRunEntity?> GetLatestByTaskIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.TaskRuns
|
|
.Where(r => r.TaskId == taskId)
|
|
.OrderByDescending(r => r.RunNumber)
|
|
.FirstOrDefaultAsync(ct);
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/TaskRunRepository.cs
|
|
git commit -m "refactor(data): rewrite TaskRunRepository to EF Core LINQ"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Rewrite TaskRepository
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Data/Repositories/TaskRepository.cs`
|
|
|
|
This is the most complex repository. The atomic queue claim stays as raw SQL.
|
|
|
|
- [ ] **Step 1: Rewrite TaskRepository to EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data.Models;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
|
|
|
namespace ClaudeDo.Data.Repositories;
|
|
|
|
public sealed class TaskRepository
|
|
{
|
|
private readonly ClaudeDoDbContext _context;
|
|
|
|
public TaskRepository(ClaudeDoDbContext context) => _context = context;
|
|
|
|
#region CRUD
|
|
|
|
public async Task AddAsync(TaskEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Tasks.Add(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default)
|
|
{
|
|
_context.Tasks.Update(entity);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
|
|
public async Task DeleteAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks.Where(t => t.Id == taskId).ExecuteDeleteAsync(ct);
|
|
}
|
|
|
|
public async Task<TaskEntity?> GetByIdAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Tasks.FirstOrDefaultAsync(t => t.Id == taskId, ct);
|
|
}
|
|
|
|
public async Task<List<TaskEntity>> GetByListIdAsync(string listId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Tasks
|
|
.Where(t => t.ListId == listId)
|
|
.OrderByDescending(t => t.CreatedAt)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Status transitions
|
|
|
|
public async Task MarkRunningAsync(string taskId, DateTime startedAt, CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(t => t.Status, TaskStatus.Running)
|
|
.SetProperty(t => t.StartedAt, startedAt), ct);
|
|
}
|
|
|
|
public async Task MarkDoneAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(t => t.Status, TaskStatus.Done)
|
|
.SetProperty(t => t.FinishedAt, finishedAt)
|
|
.SetProperty(t => t.Result, result), ct);
|
|
}
|
|
|
|
public async Task MarkFailedAsync(string taskId, DateTime finishedAt, string? result, CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(t => t.Status, TaskStatus.Failed)
|
|
.SetProperty(t => t.FinishedAt, finishedAt)
|
|
.SetProperty(t => t.Result, result), ct);
|
|
}
|
|
|
|
public async Task SetLogPathAsync(string taskId, string logPath, CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.ExecuteUpdateAsync(s => s.SetProperty(t => t.LogPath, logPath), ct);
|
|
}
|
|
|
|
public async Task FlipAllRunningToFailedAsync(CancellationToken ct = default)
|
|
{
|
|
await _context.Tasks
|
|
.Where(t => t.Status == TaskStatus.Running)
|
|
.ExecuteUpdateAsync(s => s
|
|
.SetProperty(t => t.Status, TaskStatus.Failed)
|
|
.SetProperty(t => t.FinishedAt, DateTime.UtcNow), ct);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Tags
|
|
|
|
public async Task AddTagAsync(string taskId, long tagId, CancellationToken ct = default)
|
|
{
|
|
var task = await _context.Tasks.Include(t => t.Tags).FirstAsync(t => t.Id == taskId, ct);
|
|
var tag = await _context.Tags.FindAsync([tagId], ct);
|
|
if (tag is not null && !task.Tags.Any(t => t.Id == tagId))
|
|
{
|
|
task.Tags.Add(tag);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
|
|
public async Task RemoveTagAsync(string taskId, long tagId, CancellationToken ct = default)
|
|
{
|
|
var task = await _context.Tasks.Include(t => t.Tags).FirstAsync(t => t.Id == taskId, ct);
|
|
var tag = task.Tags.FirstOrDefault(t => t.Id == tagId);
|
|
if (tag is not null)
|
|
{
|
|
task.Tags.Remove(tag);
|
|
await _context.SaveChangesAsync(ct);
|
|
}
|
|
}
|
|
|
|
public async Task<List<TagEntity>> GetTagsAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
return await _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.SelectMany(t => t.Tags)
|
|
.ToListAsync(ct);
|
|
}
|
|
|
|
public async Task<List<TagEntity>> GetEffectiveTagsAsync(string taskId, CancellationToken ct = default)
|
|
{
|
|
var taskTags = _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.SelectMany(t => t.Tags);
|
|
var listTags = _context.Tasks
|
|
.Where(t => t.Id == taskId)
|
|
.SelectMany(t => t.List.Tags);
|
|
return await taskTags.Union(listTags).Distinct().ToListAsync(ct);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Queue selection
|
|
|
|
public async Task<TaskEntity?> GetNextQueuedAgentTaskAsync(DateTime now, CancellationToken ct = default)
|
|
{
|
|
// Atomic queue claim: UPDATE + SELECT in one statement to prevent TOCTOU races.
|
|
// Not expressible in LINQ — kept as raw SQL.
|
|
var result = await _context.Tasks.FromSqlRaw("""
|
|
UPDATE tasks SET status = 'running'
|
|
WHERE id = (
|
|
SELECT t.id FROM tasks t
|
|
JOIN task_tags tt ON tt.task_id = t.id
|
|
JOIN tags tg ON tg.id = tt.tag_id AND tg.name = 'agent'
|
|
WHERE t.status = 'queued'
|
|
AND (t.scheduled_for IS NULL OR t.scheduled_for <= {0})
|
|
ORDER BY t.created_at
|
|
LIMIT 1
|
|
)
|
|
RETURNING *
|
|
""", now.ToString("o")).ToListAsync(ct);
|
|
|
|
return result.FirstOrDefault();
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Repositories/TaskRepository.cs
|
|
git commit -m "refactor(data): rewrite TaskRepository to EF Core LINQ with raw SQL queue claim"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 10: Delete Old Infrastructure and Schema
|
|
|
|
**Files:**
|
|
- Delete: `src/ClaudeDo.Data/SqliteConnectionFactory.cs`
|
|
- Delete: `src/ClaudeDo.Data/SchemaInitializer.cs`
|
|
- Delete: `schema/schema.sql`
|
|
|
|
- [ ] **Step 1: Delete SqliteConnectionFactory.cs**
|
|
|
|
```bash
|
|
rm src/ClaudeDo.Data/SqliteConnectionFactory.cs
|
|
```
|
|
|
|
- [ ] **Step 2: Delete SchemaInitializer.cs**
|
|
|
|
```bash
|
|
rm src/ClaudeDo.Data/SchemaInitializer.cs
|
|
```
|
|
|
|
- [ ] **Step 3: Delete schema.sql**
|
|
|
|
```bash
|
|
rm schema/schema.sql
|
|
```
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add -u src/ClaudeDo.Data/SqliteConnectionFactory.cs src/ClaudeDo.Data/SchemaInitializer.cs schema/schema.sql
|
|
git commit -m "chore(data): remove raw ADO.NET infrastructure and schema.sql"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 11: Update DI Registration — Worker
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Worker/Program.cs`
|
|
|
|
- [ ] **Step 1: Rewrite Worker Program.cs DI section**
|
|
|
|
Replace lines 1-28 with EF Core registration. The `SqliteConnectionFactory` and `SchemaInitializer` calls are replaced by `AddDbContext` and `Database.Migrate()`.
|
|
|
|
The full updated `Program.cs`:
|
|
|
|
```csharp
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Git;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Worker.Config;
|
|
using ClaudeDo.Worker.Hub;
|
|
using ClaudeDo.Worker.Runner;
|
|
using ClaudeDo.Worker.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
var cfg = WorkerConfig.Load();
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// When launched by the Windows SCM, speak the Service Control Protocol so SCM
|
|
// doesn't think we crashed (~30s timeout). No-op when running interactively.
|
|
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
|
|
|
|
builder.Services.AddSingleton(cfg);
|
|
|
|
// EF Core DbContext — scoped lifetime (one context per scope/request).
|
|
builder.Services.AddDbContext<ClaudeDoDbContext>(opt =>
|
|
opt.UseSqlite($"Data Source={cfg.DbPath}"));
|
|
|
|
// Repositories — scoped (they depend on the scoped DbContext).
|
|
builder.Services.AddScoped<TagRepository>();
|
|
builder.Services.AddScoped<ListRepository>();
|
|
builder.Services.AddScoped<TaskRepository>();
|
|
builder.Services.AddScoped<SubtaskRepository>();
|
|
builder.Services.AddScoped<WorktreeRepository>();
|
|
builder.Services.AddScoped<TaskRunRepository>();
|
|
|
|
builder.Services.AddHostedService<StaleTaskRecovery>();
|
|
builder.Services.AddSignalR();
|
|
|
|
// Runner stack.
|
|
builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
|
|
builder.Services.AddSingleton<HubBroadcaster>();
|
|
builder.Services.AddSingleton<GitService>();
|
|
builder.Services.AddSingleton<WorktreeManager>();
|
|
builder.Services.AddSingleton<ClaudeArgsBuilder>();
|
|
builder.Services.AddSingleton(new AgentFileService(Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
|
".todo-app", "agents")));
|
|
|
|
// QueueService: singleton + hosted service (same instance).
|
|
builder.Services.AddSingleton<QueueService>();
|
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>());
|
|
|
|
// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
|
|
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");
|
|
|
|
var app = builder.Build();
|
|
|
|
// Apply EF Core migrations at startup.
|
|
using (var scope = app.Services.CreateScope())
|
|
{
|
|
var db = scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>();
|
|
db.Database.Migrate();
|
|
}
|
|
|
|
app.MapHub<WorkerHub>("/hub");
|
|
|
|
app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})",
|
|
cfg.SignalRPort, cfg.DbPath);
|
|
|
|
app.Run();
|
|
```
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Worker/Program.cs
|
|
git commit -m "refactor(worker): switch DI from raw ADO.NET to EF Core"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 12: Update DI Registration — App
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.App/Program.cs`
|
|
|
|
- [ ] **Step 1: Rewrite App Program.cs DI section**
|
|
|
|
The App uses `ServiceCollection` directly (no WebApplication builder). ViewModels are singletons that need database access — they take `IDbContextFactory<ClaudeDoDbContext>` instead of repositories.
|
|
|
|
```csharp
|
|
using Avalonia;
|
|
using ClaudeDo.Data;
|
|
using ClaudeDo.Data.Git;
|
|
using ClaudeDo.Data.Repositories;
|
|
using ClaudeDo.Ui;
|
|
using ClaudeDo.Ui.Services;
|
|
using ClaudeDo.Ui.ViewModels;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using System;
|
|
|
|
namespace ClaudeDo.App;
|
|
|
|
sealed class Program
|
|
{
|
|
[STAThread]
|
|
public static void Main(string[] args)
|
|
{
|
|
var services = BuildServices();
|
|
App.Services = services;
|
|
|
|
// Apply EF Core migrations at startup.
|
|
using (var scope = services.CreateScope())
|
|
{
|
|
scope.ServiceProvider.GetRequiredService<ClaudeDoDbContext>().Database.Migrate();
|
|
}
|
|
|
|
try
|
|
{
|
|
BuildAvaloniaApp()
|
|
.StartWithClassicDesktopLifetime(args);
|
|
}
|
|
finally
|
|
{
|
|
// Dispose the container so WorkerClient.DisposeAsync runs —
|
|
// cancels the retry loop and closes the SignalR connection cleanly
|
|
// instead of abandoning it.
|
|
try { services.DisposeAsync().AsTask().GetAwaiter().GetResult(); }
|
|
catch { /* best effort on shutdown */ }
|
|
}
|
|
}
|
|
|
|
public static AppBuilder BuildAvaloniaApp()
|
|
=> AppBuilder.Configure<App>()
|
|
.UsePlatformDetect()
|
|
#if DEBUG
|
|
.WithDeveloperTools()
|
|
#endif
|
|
.WithInterFont()
|
|
.LogToTrace();
|
|
|
|
private static ServiceProvider BuildServices()
|
|
{
|
|
var settings = AppSettings.Load();
|
|
var dbPath = Paths.Expand(settings.DbPath);
|
|
|
|
var sc = new ServiceCollection();
|
|
|
|
// Infrastructure
|
|
sc.AddSingleton(settings);
|
|
|
|
// EF Core — register both the factory (for singletons) and scoped context.
|
|
sc.AddDbContextFactory<ClaudeDoDbContext>(opt =>
|
|
opt.UseSqlite($"Data Source={dbPath}"));
|
|
sc.AddScoped<ClaudeDoDbContext>(sp =>
|
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>().CreateDbContext());
|
|
|
|
// Repositories — scoped.
|
|
sc.AddScoped<ListRepository>();
|
|
sc.AddScoped<TaskRepository>();
|
|
sc.AddScoped<SubtaskRepository>();
|
|
sc.AddScoped<TagRepository>();
|
|
sc.AddScoped<WorktreeRepository>();
|
|
|
|
// Services
|
|
sc.AddSingleton<GitService>();
|
|
sc.AddSingleton(sp => new WorkerClient(sp.GetRequiredService<AppSettings>().SignalRUrl));
|
|
|
|
// ViewModels — singletons that use IDbContextFactory for on-demand DB access.
|
|
sc.AddTransient<ListEditorViewModel>();
|
|
sc.AddTransient<TaskEditorViewModel>();
|
|
sc.AddSingleton<StatusBarViewModel>();
|
|
sc.AddSingleton<IDbContextFactory<ClaudeDoDbContext>>(sp =>
|
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>());
|
|
|
|
// ViewModel factory registrations — these need to be updated in Task 13
|
|
// to take IDbContextFactory instead of repositories directly.
|
|
// For now, leave them unchanged — they won't compile until Task 13.
|
|
|
|
return sc.BuildServiceProvider();
|
|
}
|
|
}
|
|
```
|
|
|
|
Note: This file will NOT compile until Task 13 updates the ViewModels. That is expected.
|
|
|
|
- [ ] **Step 2: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.App/Program.cs
|
|
git commit -m "refactor(app): switch DI from raw ADO.NET to EF Core"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 13: Update ViewModel and Worker Service Consumers
|
|
|
|
All singleton ViewModels and Worker services currently take repository instances directly. Since repositories are now scoped, these consumers must take `IDbContextFactory<ClaudeDoDbContext>` and create a fresh context + repositories per operation.
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs`
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs`
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs`
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs`
|
|
- Modify: `src/ClaudeDo.Worker/Services/QueueService.cs`
|
|
- Modify: `src/ClaudeDo.Worker/Services/StaleTaskRecovery.cs`
|
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
|
|
- Modify: `src/ClaudeDo.Worker/Runner/WorktreeManager.cs`
|
|
|
|
- [ ] **Step 1: Update each consumer**
|
|
|
|
For each consumer, apply this pattern:
|
|
|
|
**Before (example — QueueService):**
|
|
```csharp
|
|
private readonly TaskRepository _taskRepo;
|
|
public QueueService(TaskRepository taskRepo, ...) { _taskRepo = taskRepo; ... }
|
|
|
|
// In method:
|
|
var task = await _taskRepo.GetNextQueuedAgentTaskAsync(now, ct);
|
|
```
|
|
|
|
**After:**
|
|
```csharp
|
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
|
public QueueService(IDbContextFactory<ClaudeDoDbContext> dbFactory, ...) { _dbFactory = dbFactory; ... }
|
|
|
|
// In method:
|
|
using var context = _dbFactory.CreateDbContext();
|
|
var taskRepo = new TaskRepository(context);
|
|
var task = await taskRepo.GetNextQueuedAgentTaskAsync(now, ct);
|
|
```
|
|
|
|
Apply this to every file listed above. Each method that touches the DB creates a context and the repositories it needs. The context is disposed at method end.
|
|
|
|
**Consumer → repositories used:**
|
|
|
|
| Consumer | Repositories |
|
|
|---|---|
|
|
| `MainWindowViewModel` | `ListRepository` |
|
|
| `TaskDetailViewModel` | `TaskRepository`, `WorktreeRepository`, `ListRepository`, `TagRepository`, `SubtaskRepository` |
|
|
| `TaskListViewModel` | `TaskRepository`, `TagRepository`, `ListRepository` |
|
|
| `TaskEditorViewModel` | `SubtaskRepository` |
|
|
| `QueueService` | `TaskRepository` |
|
|
| `StaleTaskRecovery` | `TaskRepository` |
|
|
| `TaskRunner` | `TaskRepository`, `TaskRunRepository`, `ListRepository`, `WorktreeRepository`, `SubtaskRepository` |
|
|
| `WorktreeManager` | `WorktreeRepository` |
|
|
|
|
For each file:
|
|
1. Replace repository fields with a single `IDbContextFactory<ClaudeDoDbContext> _dbFactory` field.
|
|
2. Update constructor to take `IDbContextFactory<ClaudeDoDbContext>`.
|
|
3. In each method that uses repos: `using var context = _dbFactory.CreateDbContext();` then instantiate needed repos.
|
|
|
|
- [ ] **Step 2: Update App Program.cs ViewModel registrations**
|
|
|
|
Now that ViewModels take `IDbContextFactory` instead of repos, simplify the registrations. The long factory lambdas that manually resolved repos can be replaced with simpler registrations since ViewModels now only need `IDbContextFactory` and non-repo services:
|
|
|
|
```csharp
|
|
// ViewModels
|
|
sc.AddTransient<ListEditorViewModel>();
|
|
sc.AddTransient<TaskEditorViewModel>();
|
|
sc.AddSingleton<StatusBarViewModel>();
|
|
sc.AddSingleton<TaskDetailViewModel>();
|
|
sc.AddSingleton<TaskListViewModel>();
|
|
sc.AddSingleton<MainWindowViewModel>();
|
|
```
|
|
|
|
If the constructors still take non-repo dependencies (GitService, WorkerClient, StatusBarViewModel, etc.), keep those in the constructor and let DI resolve them naturally.
|
|
|
|
- [ ] **Step 3: Update Worker Program.cs singleton services**
|
|
|
|
Worker singleton services (QueueService, TaskRunner, WorktreeManager) now take `IDbContextFactory` instead of repos. Add factory registration and update service registrations:
|
|
|
|
Add to Worker Program.cs after `AddDbContext`:
|
|
```csharp
|
|
builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
|
|
opt.UseSqlite($"Data Source={cfg.DbPath}"));
|
|
```
|
|
|
|
Singleton services will resolve `IDbContextFactory` from DI and create scoped contexts per operation.
|
|
|
|
- [ ] **Step 4: Verify the solution builds**
|
|
|
|
Run: `dotnet build ClaudeDo.slnx`
|
|
Expected: Build succeeds with zero errors.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/ViewModels/ src/ClaudeDo.Worker/Services/ src/ClaudeDo.Worker/Runner/ src/ClaudeDo.App/Program.cs src/ClaudeDo.Worker/Program.cs
|
|
git commit -m "refactor(ui,worker): switch consumers from direct repos to IDbContextFactory"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Generate Initial EF Core Migration
|
|
|
|
**Files:**
|
|
- Create: `src/ClaudeDo.Data/Migrations/` (generated)
|
|
|
|
- [ ] **Step 1: Install EF Core tools if needed**
|
|
|
|
Run: `dotnet tool list -g | grep dotnet-ef || dotnet tool install --global dotnet-ef --version 8.0.11`
|
|
|
|
- [ ] **Step 2: Generate the initial migration**
|
|
|
|
Run from the repo root:
|
|
|
|
```bash
|
|
dotnet ef migrations add InitialCreate --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker
|
|
```
|
|
|
|
Expected: Creates `src/ClaudeDo.Data/Migrations/` with `*_InitialCreate.cs` and `ClaudeDoDbContextModelSnapshot.cs`.
|
|
|
|
- [ ] **Step 3: Add existing-database compatibility shim**
|
|
|
|
Edit the generated `*_InitialCreate.cs` `Up` method. Wrap all `CreateTable` calls in a check for existing tables. Add this at the very start of `Up`:
|
|
|
|
```csharp
|
|
protected override void Up(MigrationBuilder migrationBuilder)
|
|
{
|
|
// If upgrading from the old schema.sql-based setup, tables already exist.
|
|
// Check for the 'lists' table as a sentinel — if it exists, skip DDL and
|
|
// just ensure the migration is recorded.
|
|
migrationBuilder.Sql("""
|
|
CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" (
|
|
"MigrationId" TEXT NOT NULL CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY,
|
|
"ProductVersion" TEXT NOT NULL
|
|
);
|
|
""");
|
|
|
|
// The rest of the migration uses IF NOT EXISTS for safety.
|
|
// ... (keep the generated code but add IF NOT EXISTS to each CreateTable)
|
|
}
|
|
```
|
|
|
|
Alternatively, since SQLite `CREATE TABLE IF NOT EXISTS` is supported, prefix each generated `CREATE TABLE` statement. The simplest approach: in `Up`, replace each `migrationBuilder.CreateTable(...)` with equivalent `migrationBuilder.Sql("CREATE TABLE IF NOT EXISTS ...")` calls. This is a one-time adaptation.
|
|
|
|
- [ ] **Step 4: Verify migration applies to a fresh database**
|
|
|
|
```bash
|
|
rm -f /tmp/test_migration.db
|
|
dotnet ef database update --project src/ClaudeDo.Data --startup-project src/ClaudeDo.Worker -- --DbPath /tmp/test_migration.db
|
|
```
|
|
|
|
Or simply run the Worker pointed at a new DB path and check it starts.
|
|
|
|
- [ ] **Step 5: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Data/Migrations/
|
|
git commit -m "feat(data): add InitialCreate EF Core migration with existing-DB compat shim"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 15: Update Test Infrastructure
|
|
|
|
**Files:**
|
|
- Modify: `tests/ClaudeDo.Worker.Tests/Infrastructure/DbFixture.cs`
|
|
- Modify: `tests/ClaudeDo.Worker.Tests/Runner/WorktreeManagerTests.cs` (and any other test files that use DbFixture)
|
|
|
|
- [ ] **Step 1: Rewrite DbFixture to use EF Core**
|
|
|
|
```csharp
|
|
using ClaudeDo.Data;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace ClaudeDo.Worker.Tests.Infrastructure;
|
|
|
|
public sealed class DbFixture : IDisposable
|
|
{
|
|
public string DbPath { get; }
|
|
public DbContextOptions<ClaudeDoDbContext> Options { get; }
|
|
|
|
public DbFixture()
|
|
{
|
|
DbPath = Path.Combine(Path.GetTempPath(), $"claudedo_test_{Guid.NewGuid():N}.db");
|
|
Options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
|
|
.UseSqlite($"Data Source={DbPath}")
|
|
.Options;
|
|
|
|
using var context = CreateContext();
|
|
context.Database.Migrate();
|
|
}
|
|
|
|
public ClaudeDoDbContext CreateContext() => new(Options);
|
|
|
|
public void Dispose()
|
|
{
|
|
try { File.Delete(DbPath); } catch { /* best effort */ }
|
|
try { File.Delete(DbPath + "-wal"); } catch { }
|
|
try { File.Delete(DbPath + "-shm"); } catch { }
|
|
}
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Update WorktreeManagerTests and other test files**
|
|
|
|
Replace `new ListRepository(db.Factory)` with `new ListRepository(db.CreateContext())` etc. For example in WorktreeManagerTests `CreateManagerAsync`:
|
|
|
|
```csharp
|
|
private async Task<(WorktreeManager mgr, WorktreeRepository wtRepo)> CreateManagerAsync(
|
|
TaskEntity task, ListEntity list, string strategy = "sibling", string? centralRoot = null)
|
|
{
|
|
var db = new DbFixture();
|
|
_dbFixtures.Add(db);
|
|
|
|
var context = db.CreateContext();
|
|
var listRepo = new ListRepository(context);
|
|
var taskRepo = new TaskRepository(context);
|
|
var wtRepo = new WorktreeRepository(context);
|
|
// ... seed and create manager as before, but using context-based repos
|
|
```
|
|
|
|
Apply the same pattern to all test files that use `DbFixture.Factory` to construct repositories.
|
|
|
|
- [ ] **Step 3: Run tests**
|
|
|
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests`
|
|
Expected: All existing tests pass.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add tests/ClaudeDo.Worker.Tests/
|
|
git commit -m "refactor(tests): switch test infrastructure to EF Core DbContext"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 16: Full Build and Test Verification
|
|
|
|
- [ ] **Step 1: Clean build**
|
|
|
|
Run: `dotnet build ClaudeDo.slnx --no-incremental`
|
|
Expected: Build succeeds with zero errors.
|
|
|
|
- [ ] **Step 2: Run all tests**
|
|
|
|
Run: `dotnet test ClaudeDo.slnx`
|
|
Expected: All tests pass.
|
|
|
|
- [ ] **Step 3: Smoke test — start Worker**
|
|
|
|
Run the Worker and verify it starts, creates/migrates the DB, and logs the listening message.
|
|
|
|
- [ ] **Step 4: Smoke test — start App**
|
|
|
|
Run the App and verify it starts, connects to Worker (if running), and the UI loads.
|
|
|
|
- [ ] **Step 5: Verify no references to old types remain**
|
|
|
|
```bash
|
|
grep -rn "SqliteConnectionFactory\|SchemaInitializer\|Microsoft\.Data\.Sqlite\|using Microsoft\.Data\.Sqlite" src/ tests/ --include="*.cs" --include="*.csproj"
|
|
```
|
|
|
|
Expected: Zero matches.
|
|
|
|
- [ ] **Step 6: Final commit if any cleanup was needed**
|
|
|
|
```bash
|
|
git add -A
|
|
git commit -m "chore(data): final cleanup after EF Core migration"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 17: Update Documentation
|
|
|
|
**Files:**
|
|
- Modify: `CLAUDE.md`
|
|
- Modify: `src/ClaudeDo.Data/CLAUDE.md`
|
|
|
|
- [ ] **Step 1: Update project CLAUDE.md**
|
|
|
|
Update the Tech Stack, Conventions, and Key Paths sections to reflect EF Core:
|
|
- Replace "SQLite (WAL mode) via Microsoft.Data.Sqlite — raw ADO.NET, no ORM" with "SQLite (WAL mode) via Entity Framework Core + Microsoft.EntityFrameworkCore.Sqlite"
|
|
- Remove references to `schema/schema.sql`, `SqliteConnectionFactory`, `SchemaInitializer`
|
|
- Update repository description to mention LINQ instead of raw SQL
|
|
- Add note about `IDbContextFactory` pattern for singleton consumers
|
|
|
|
- [ ] **Step 2: Update ClaudeDo.Data/CLAUDE.md**
|
|
|
|
Rewrite the Infrastructure and Repositories sections:
|
|
- Infrastructure: `ClaudeDoDbContext` replaces `SqliteConnectionFactory` + `SchemaInitializer`
|
|
- Repositories: "All repositories use EF Core LINQ. Exception: `TaskRepository.GetNextQueuedAgentTaskAsync` uses `FromSqlRaw` for atomic queue claim."
|
|
- Remove Conventions about `ToDb()`/`FromDb()` and `DBNull.Value`
|
|
- Add Conventions about `IEntityTypeConfiguration<T>` in `Configuration/` folder
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add CLAUDE.md src/ClaudeDo.Data/CLAUDE.md
|
|
git commit -m "docs: update CLAUDE.md files for EF Core migration"
|
|
```
|