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();
}
}