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:
+42
@@ -0,0 +1,42 @@
|
||||
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)}.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user