feat(claude-do): depends_on als Menge statt Einzelfeld

Ein Task kann jetzt auf beliebig viele Vorgaenger warten. Die Einzelspalte
tasks.depends_on_task_id wird durch die Beziehungstabelle task_dependencies
ersetzt (Composite-PK, beide FKs ON DELETE CASCADE -- das reproduziert das
alte ON DELETE SET NULL: geloeschter Vorgaenger = Kante weg = Task wieder
pickbar). BlockedByTaskId bleibt unberuehrt.

- Migration AddTaskDependencies kopiert jede bestehende depends_on_task_id
  verlustfrei in die Tabelle, bevor die Spalte fallengelassen wird
- QueuePicker gatet per doppelt negiertem NOT EXISTS: kein Task mit 0 Kanten
  wird blockiert, alle Kanten muessen done sein, eine Kante auf einen
  verschwundenen Task blockiert weiter
- SetDependsOnAsync ersetzt die ganze Menge; Zykluspruefung ist jetzt eine
  BFS ueber den Graphen, faengt also auch A->B->C->A
- blockedReason nennt alle offenen Vorgaenger (External/TaskBlocking, von
  ExternalMcpService und TaskWaitMcpTools gemeinsam genutzt)
- add_task/update_task nehmen dependsOnTaskIds; die alte Skalarform bleibt
  als Ein-Element-Menge inkl. ""-Clear-Sentinel gueltig
- UI: Fan-in-Zeile bleibt flach und listet alle Vorgaenger im After-Label,
  die lineare Kette rendert unveraendert
This commit is contained in:
Mika Kuns
2026-09-02 15:08:05 +02:00
parent 84d955dfc7
commit c25732cff8
31 changed files with 1893 additions and 206 deletions
@@ -0,0 +1,32 @@
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace ClaudeDo.Data.Configuration;
public class TaskDependencyEntityConfiguration : IEntityTypeConfiguration<TaskDependencyEntity>
{
public void Configure(EntityTypeBuilder<TaskDependencyEntity> builder)
{
builder.ToTable("task_dependencies");
builder.HasKey(d => new { d.TaskId, d.DependsOnTaskId });
builder.Property(d => d.TaskId).HasColumnName("task_id");
builder.Property(d => d.DependsOnTaskId).HasColumnName("depends_on_task_id");
// Cascade on both sides reproduces what the old single depends_on_task_id column got from
// ON DELETE SET NULL: deleting the predecessor drops the edge, so the dependent becomes
// pickable rather than blocked forever on a task that no longer exists.
builder.HasOne<TaskEntity>()
.WithMany(t => t.Dependencies)
.HasForeignKey(d => d.TaskId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasOne<TaskEntity>()
.WithMany()
.HasForeignKey(d => d.DependsOnTaskId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(d => d.DependsOnTaskId).HasDatabaseName("idx_task_dependencies_depends_on");
}
}