feat(findings): resolve the target store from task or list
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
|
||||
namespace ClaudeDo.Worker.Findings;
|
||||
|
||||
/// <summary>
|
||||
/// Maps an MCP call to the project whose findings store it targets. Always resolves to the list's
|
||||
/// WorkingDir — the main checkout — even when the caller runs inside a worktree, because concurrent
|
||||
/// writes into worktree copies would produce INDEX.md merge conflicts.
|
||||
/// </summary>
|
||||
public sealed class FindingsStoreLocator : IFindingsStoreLocator
|
||||
{
|
||||
private readonly TaskRepository _tasks;
|
||||
private readonly ListRepository _lists;
|
||||
|
||||
public FindingsStoreLocator(TaskRepository tasks, ListRepository lists)
|
||||
{
|
||||
_tasks = tasks;
|
||||
_lists = lists;
|
||||
}
|
||||
|
||||
public async Task<FindingsTarget> ResolveForTaskAsync(string taskId, CancellationToken ct)
|
||||
{
|
||||
var task = await _tasks.GetByIdAsync(taskId, ct)
|
||||
?? throw new InvalidOperationException($"Task {taskId} not found.");
|
||||
var list = await _lists.GetByIdAsync(task.ListId, ct)
|
||||
?? throw new InvalidOperationException($"List {task.ListId} not found.");
|
||||
return ToTarget(list);
|
||||
}
|
||||
|
||||
public async Task<FindingsTarget> ResolveForListAsync(string listIdOrName, CancellationToken ct)
|
||||
{
|
||||
var all = await _lists.GetAllAsync(ct);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(listIdOrName))
|
||||
{
|
||||
var match = all.FirstOrDefault(l => l.Id == listIdOrName)
|
||||
?? all.FirstOrDefault(l => string.Equals(l.Name, listIdOrName, StringComparison.OrdinalIgnoreCase))
|
||||
?? throw new InvalidOperationException(
|
||||
$"No list matches '{listIdOrName}'. Known lists: {Names(all)}.");
|
||||
return ToTarget(match);
|
||||
}
|
||||
|
||||
var withDir = all.Where(l => !string.IsNullOrWhiteSpace(l.WorkingDir)).ToList();
|
||||
return withDir.Count switch
|
||||
{
|
||||
1 => ToTarget(withDir[0]),
|
||||
0 => throw new InvalidOperationException("No list has a working directory, so there is nowhere to save a finding."),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Several lists have a working directory — pass 'list' to pick one: {Names(withDir)}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static string Names(IEnumerable<ListEntity> lists) => string.Join(", ", lists.Select(l => l.Name));
|
||||
|
||||
private static FindingsTarget ToTarget(ListEntity list)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(list.WorkingDir))
|
||||
throw new InvalidOperationException($"List '{list.Name}' has no working directory, so it has no findings store.");
|
||||
return new FindingsTarget(list.Id, list.Name, list.WorkingDir, list.FindingsTracked);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace ClaudeDo.Worker.Findings;
|
||||
|
||||
/// <summary>The main checkout a finding belongs to, plus whether its store is committed with the repo.</summary>
|
||||
public sealed record FindingsTarget(string ListId, string ListName, string WorkingDir, bool Tracked);
|
||||
|
||||
public interface IFindingsStoreLocator
|
||||
{
|
||||
Task<FindingsTarget> ResolveForTaskAsync(string taskId, CancellationToken ct);
|
||||
Task<FindingsTarget> ResolveForListAsync(string listIdOrName, CancellationToken ct);
|
||||
}
|
||||
Reference in New Issue
Block a user