using System.Text; using ClawdDotNet.Core.Organisation; namespace ClawdDotNet.Core.Tasks; /// /// #714: Why a task exists — its parent, that one's parent, up to the goal of the unit. /// Taken from Paperclip (Organisation-Konzept, 9a): an agent that only sees its own order /// cannot weigh it. "Write a product text" is a different job when it serves "launch the /// spring collection by Friday" than when it serves "clean up old listings". /// /// Purpose, not precondition. says when a task /// may run; the parent says what it is for. Both can point to the same task, and mean /// different things. /// /// Walked at run time, never stored as text: a parent that is renamed or finished /// shows up as it is now. The walk stops at a cycle and after steps, /// so a broken chain cannot hang a run or flood its prompt. /// public static class GoalChain { public const int MaxDepth = 8; /// The parents of a task, nearest first. Stops at a missing parent or a cycle. public static async Task> ParentsAsync( TaskItem task, Func> lookup, CancellationToken ct) { var parents = new List(); var seen = new HashSet(StringComparer.OrdinalIgnoreCase) { task.Id }; var next = task.ParentId; while (!string.IsNullOrWhiteSpace(next) && parents.Count < MaxDepth && seen.Add(next)) { if (await lookup(next, ct) is not { } parent) break; parents.Add(parent); next = parent.ParentId; } return parents; } /// /// Whether making the parent of /// would close a circle — the parent is the task itself or lies below it. /// public static async Task WouldCycleAsync( string taskId, string parentId, Func> lookup, CancellationToken ct) { if (string.Equals(taskId, parentId, StringComparison.OrdinalIgnoreCase)) return true; var seen = new HashSet(StringComparer.OrdinalIgnoreCase); var next = parentId; while (!string.IsNullOrWhiteSpace(next) && seen.Add(next)) { if (string.Equals(next, taskId, StringComparison.OrdinalIgnoreCase)) return true; next = (await lookup(next, ct))?.ParentId; } return false; } /// /// The goal above the chain: that of the unit of the topmost task that belongs to one — /// a goal a human set on the top level belongs to no unit, so the next one down counts — /// or of the nearest unit above it that has a goal. null without units or without /// a goal. /// public static (string UnitName, string Goal)? UnitGoal(IReadOnlyList fromTop, OrgChart? org) { if (org is not { HasUnits: true }) return null; var unitId = fromTop.Select(t => TaskBoard.UnitOf(t, org)).FirstOrDefault(u => u is not null); if (org.Unit(unitId) is not { } unit) return null; foreach (var candidate in new[] { unit }.Concat(org.Ancestors(unit.Id))) { if (!string.IsNullOrWhiteSpace(candidate.Goal)) return (candidate.Name, candidate.Goal.Trim()); } return null; } /// /// The paragraph that goes into the run: "you do this because … because … because". /// null when there is nothing above the task — then the run looks as before. /// public static string? Describe(TaskItem task, IReadOnlyList parents, OrgChart? org) { var goal = UnitGoal([.. parents.Reverse(), task], org); if (parents.Count == 0 && goal is null) return null; var sb = new StringBuilder(); sb.AppendLine("Why this task exists — it serves, from nearest to farthest:"); foreach (var parent in parents) sb.AppendLine($"- {parent.Id} \"{parent.Title}\" ({TaskText.Of(parent.Status)})"); if (goal is { } g) sb.AppendLine($"- the goal of the unit \"{g.UnitName}\": {g.Goal}"); sb.Append("Weigh your work against this. If the task no longer serves it — the parent is done or canceled, or the task contradicts the goal — say so in your result instead of working blindly."); return sb.ToString(); } }