using System.Globalization;
using ClaudeDo.Data.Repositories;
namespace ClaudeDo.Worker.External;
///
/// Resolves a task-id MCP parameter that may be a display number (#123 or bare 123)
/// into the task's GUID. A GUID is never all-digits, so the two forms never collide. Anything
/// else — including an empty string, which update_task's dependsOnTaskId uses as a
/// "clear the link" sentinel — passes through untouched.
///
internal static class TaskIdResolver
{
public static async Task ResolveAsync(TaskRepository tasks, string idOrNumber, CancellationToken ct = default)
{
return (await ResolveCoreAsync(tasks, idOrNumber, ct))!;
}
public static async Task ResolveOptionalAsync(TaskRepository tasks, string? idOrNumber, CancellationToken ct = default)
{
return idOrNumber is null ? null : await ResolveCoreAsync(tasks, idOrNumber, ct);
}
public static async Task> ResolveManyAsync(
TaskRepository tasks, IReadOnlyList idsOrNumbers, CancellationToken ct = default)
{
var resolved = new string[idsOrNumbers.Count];
for (var i = 0; i < idsOrNumbers.Count; i++)
resolved[i] = await ResolveAsync(tasks, idsOrNumbers[i], ct);
return resolved;
}
private static async Task ResolveCoreAsync(TaskRepository tasks, string idOrNumber, CancellationToken ct)
{
if (idOrNumber.Length == 0)
return idOrNumber;
var candidate = idOrNumber[0] == '#' ? idOrNumber[1..] : idOrNumber;
if (!int.TryParse(candidate, NumberStyles.None, CultureInfo.InvariantCulture, out var number))
return idOrNumber;
var task = await tasks.GetByNumberAsync(number, ct)
?? throw new InvalidOperationException($"no task with number {number}");
return task.Id;
}
}