refactor: collapse single-implementation interfaces

Nine interfaces had exactly one implementation and no test double — they existed
only to be named twice in a DI registration: IFindingsStore, IFindingsStoreLocator,
IPrimeScheduleSignal, IRefineRunner, IWeekReportService, IMergeCoordinator,
IMissionControlPane, IOnlineLoginService, ITaskListFilter. Consumers now depend on
the concrete type; the DTO records that shared those files moved next to their
implementation. IInteractiveLaunchSpecService stays — it carries 54 lines of
contract documentation, which is not ceremony.

IMergeCoordinator in particular had a redundant null object: MergeCoordinator with
a null Handler already no-ops, and every test used the real class with Handler set.

Filtering/ collapses from 8 files to 1. ITaskListFilter and TaskListFilterBase were
a double abstraction over four predicates, with MatchesAsContext => false declared
in both. SmartFlagFilter also compiled its expression twice (its own _flag plus the
inherited Matches cache) — it now uses the cache.

StaticTokenAuthProvider was in src but production uses ZitadelAuthProvider; it is
a test double, so it moves to the test project. Its own test goes away with it.
This commit is contained in:
mika kuns
2026-08-26 10:12:23 +02:00
parent 76d836a278
commit 296251e27e
45 changed files with 174 additions and 327 deletions
+17 -1
View File
@@ -4,13 +4,29 @@ using ClaudeDo.Worker.Git;
namespace ClaudeDo.Worker.Findings;
/// <summary>One durable finding, as handed in by a caller. Git and DB lookups happen above this layer.</summary>
public sealed record FindingInput(
string Slug,
string Title,
string Body,
string Scope,
string SourceTaskId,
string VerifiedAgainst);
public sealed record SaveFindingOutcome(
string Slug,
string Path,
bool Created,
int TotalFindings,
bool NearCapacity);
/// <summary>
/// Owns &lt;working-dir&gt;/.claudedo/ — one markdown file per finding plus a rebuilt INDEX.md.
/// Pure filesystem: the caller supplies the head commit and source task id.
/// INDEX.md is always regenerated from the files on disk, never patched, so findings the user
/// deleted or renamed by hand disappear from the index on the next write.
/// </summary>
public sealed class FindingsStore : IFindingsStore
public sealed class FindingsStore
{
/// <summary>INDEX.md is read by every run; past this many entries it stops paying for itself.</summary>
public const int WarnThreshold = 80;
@@ -3,12 +3,15 @@ using ClaudeDo.Data.Repositories;
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);
/// <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
public sealed class FindingsStoreLocator
{
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
@@ -1,22 +0,0 @@
namespace ClaudeDo.Worker.Findings;
/// <summary>One durable finding, as handed in by a caller. Git and DB lookups happen above this layer.</summary>
public sealed record FindingInput(
string Slug,
string Title,
string Body,
string Scope,
string SourceTaskId,
string VerifiedAgainst);
public sealed record SaveFindingOutcome(
string Slug,
string Path,
bool Created,
int TotalFindings,
bool NearCapacity);
public interface IFindingsStore
{
Task<SaveFindingOutcome> SaveAsync(string workingDir, FindingInput input, CancellationToken ct, bool tracked = false);
}
@@ -1,10 +0,0 @@
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);
}