Files
ClaudeDo/src/ClaudeDo.Worker/External/TaskBlocking.cs
T
Mika Kuns c25732cff8 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
2026-09-02 15:08:05 +02:00

43 lines
1.6 KiB
C#

using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.External;
/// <summary>
/// Renders the `blockedReason` string shared by <see cref="ExternalMcpService"/> and
/// <see cref="TaskWaitMcpTools"/>. Lives here so the two can't drift on the wording an agent
/// parses.
/// </summary>
internal static class TaskBlocking
{
/// <summary>
/// The predecessors of <paramref name="dependencyIds"/> that have not reached Done — an id
/// missing from <paramref name="statuses"/> counts as unmet (the picker treats a dangling
/// edge as blocking too).
/// </summary>
public static List<(string Id, TaskStatus? Status)> Unmet(
IEnumerable<string> dependencyIds, IReadOnlyDictionary<string, TaskStatus> statuses)
{
var unmet = new List<(string Id, TaskStatus? Status)>();
foreach (var id in dependencyIds)
{
if (statuses.TryGetValue(id, out var status))
{
if (status != TaskStatus.Done) unmet.Add((id, status));
}
else
{
unmet.Add((id, null));
}
}
return unmet;
}
/// <summary>Names every open predecessor, not just the first — a task can wait on n of them.</summary>
public static string Describe(IReadOnlyList<(string Id, TaskStatus? Status)> unmet)
{
var noun = unmet.Count == 1 ? "task" : "tasks";
var parts = unmet.Select(u => $"{u.Id} (status: {(u.Status?.ToString() ?? "not found")})");
return $"Blocked: depends on {noun} {string.Join(", ", parts)}.";
}
}