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
+3 -3
View File
@@ -11,10 +11,10 @@ public sealed record SaveFindingResult(
[McpServerToolType]
public sealed class FindingsMcpTools
{
private readonly IFindingsStore _store;
private readonly IFindingsStoreLocator _locator;
private readonly FindingsStore _store;
private readonly FindingsStoreLocator _locator;
public FindingsMcpTools(IFindingsStore store, IFindingsStoreLocator locator)
public FindingsMcpTools(FindingsStore store, FindingsStoreLocator locator)
{
_store = store;
_locator = locator;
+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);
}
+6 -6
View File
@@ -229,11 +229,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
private readonly PlanningAggregator _planningAggregator;
private readonly PlanningMergeOrchestrator _planningMergeOrchestrator;
private readonly PlanningChainCoordinator _planningChain;
private readonly IPrimeScheduleSignal _primeSignal;
private readonly PrimeScheduleSignal _primeSignal;
private readonly IPrimeRunner _primeRunner;
private readonly ITaskStateService _state;
private readonly IWeekReportService _report;
private readonly IRefineRunner _refineRunner;
private readonly WeekReportService _report;
private readonly RefineRunner _refineRunner;
private readonly WorkerConfig _cfg;
private readonly OnlineInboxConfig _onlineInboxConfig;
private readonly OnlineTokenStore _onlineTokenStore;
@@ -264,11 +264,11 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
PlanningAggregator planningAggregator,
PlanningMergeOrchestrator planningMergeOrchestrator,
PlanningChainCoordinator planningChain,
IPrimeScheduleSignal primeSignal,
PrimeScheduleSignal primeSignal,
IPrimeRunner primeRunner,
ITaskStateService state,
IWeekReportService report,
IRefineRunner refineRunner,
WeekReportService report,
RefineRunner refineRunner,
WorkerConfig cfg,
OnlineInboxConfig onlineInboxConfig,
OnlineTokenStore onlineTokenStore,
@@ -1,24 +0,0 @@
using ClaudeDo.Worker.Online.Interfaces;
namespace ClaudeDo.Worker.Online;
/// <summary>
/// Simple <see cref="IOnlineAuthProvider"/> that returns a fixed token supplied at construction.
/// Used as the default DI registration until <c>ZitadelAuthProvider</c> is wired (Phase 2).
/// Also serves as the test double.
/// </summary>
public sealed class StaticTokenAuthProvider : IOnlineAuthProvider
{
private readonly string? _token;
public StaticTokenAuthProvider(string? token = null)
{
_token = token;
}
public Task<string?> GetAccessTokenAsync(CancellationToken ct = default)
=> Task.FromResult(_token);
public Task<string?> GetAccessTokenAsync(bool forceRefresh, CancellationToken ct = default)
=> Task.FromResult(_token);
}
@@ -86,7 +86,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher
return Task.CompletedTask;
}
// The raw claude CLI args for a --resume launch, shared with InteractiveLaunchSpecService
// The raw claude CLI args for a --resume launch, shared with IInteractiveLaunchSpecService
// (which needs the bare Exe/Args pair for a ConPTY host, not a wrapped pwsh command line).
internal static IReadOnlyList<string> BuildResumeArgs(string claudeSessionId) =>
new[] { "--resume", claudeSessionId };
@@ -99,7 +99,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher
BuildPwshCommand(claudePath, BuildPlanningStartArgs(ctx));
// The raw claude CLI args for an interactive planning START, shared with the embedded-ConPTY
// planning path (InteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than
// planning path (IInteractiveLaunchSpecService), which needs the bare Exe/Args pair rather than
// a pwsh-wrapped command line. Arg order matters: variadic flags (--allowedTools, --add-dir)
// come first; the single-line kickoff prompt is positional, so it must follow a single-value
// flag (--append-system-prompt-file) or a variadic flag would swallow it.
@@ -176,7 +176,7 @@ public sealed class WindowsTerminalLauncher : ITerminalLauncher
?? throw new TerminalLaunchException("Failed to start Windows Terminal process.");
}
// Not private: reused by InteractiveLaunchSpecService to resolve the claude executable
// Not private: reused by IInteractiveLaunchSpecService to resolve the claude executable
// for a ConPTY-hosted launch spec the same way a wt-launched terminal resolves it.
internal static string? Resolve(string pathOrName)
{
@@ -1,6 +0,0 @@
namespace ClaudeDo.Worker.Prime;
public interface IPrimeScheduleSignal
{
void Signal();
CancellationToken CurrentToken { get; }
}
@@ -1,6 +1,6 @@
namespace ClaudeDo.Worker.Prime;
public sealed class PrimeScheduleSignal : IPrimeScheduleSignal, IDisposable
public sealed class PrimeScheduleSignal : IDisposable
{
private CancellationTokenSource _cts = new();
private readonly object _lock = new();
+2 -2
View File
@@ -21,7 +21,7 @@ public sealed class PrimeScheduler : BackgroundService
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private readonly IPrimeRunner _runner;
private readonly IPrimeClock _clock;
private readonly IPrimeScheduleSignal _signal;
private readonly PrimeScheduleSignal _signal;
private readonly IPrimeBroadcaster _broadcaster;
private readonly PrimeSchedulerOptions _options;
private readonly ILogger<PrimeScheduler> _logger;
@@ -30,7 +30,7 @@ public sealed class PrimeScheduler : BackgroundService
IDbContextFactory<ClaudeDoDbContext> dbFactory,
IPrimeRunner runner,
IPrimeClock clock,
IPrimeScheduleSignal signal,
PrimeScheduleSignal signal,
IPrimeBroadcaster broadcaster,
PrimeSchedulerOptions options,
ILogger<PrimeScheduler> logger)
+6 -7
View File
@@ -67,7 +67,7 @@ builder.Services.AddDbContextFactory<ClaudeDoDbContext>(opt =>
builder.Services.AddSingleton(cfg);
builder.Services.AddSingleton<AttachmentStore>();
// Singleton so the always-on and in-task MCP servers share the same index write lock.
builder.Services.AddSingleton<IFindingsStore, FindingsStore>();
builder.Services.AddSingleton<FindingsStore>();
builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddHostedService<OrphanRecovery>();
builder.Services.AddHostedService<AttachmentOrphanRecovery>();
@@ -140,7 +140,7 @@ builder.Services.AddSingleton<OverrideSlotService>();
builder.Services.AddSingleton<IClaudeHistoryReader>(_ =>
new ClaudeHistoryReader(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".claude", "projects")));
builder.Services.AddSingleton<IWeekReportService, WeekReportService>();
builder.Services.AddSingleton<WeekReportService>();
// Usage
builder.Services.AddSingleton<ITranscriptUsageReader, TranscriptUsageReader>();
@@ -154,14 +154,13 @@ builder.Services.AddSingleton<TokenTrackerService>();
// Prime Claude
builder.Services.AddSingleton<IPrimeClock, PrimeClock>();
builder.Services.AddSingleton<PrimeScheduleSignal>();
builder.Services.AddSingleton<IPrimeScheduleSignal>(sp => sp.GetRequiredService<PrimeScheduleSignal>());
builder.Services.AddSingleton<IPrimeRunner, PrimeRunner>();
builder.Services.AddSingleton(PrimeSchedulerOptions.Default);
builder.Services.AddSingleton<IPrimeBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());
builder.Services.AddHostedService<PrimeScheduler>();
// Refine
builder.Services.AddSingleton<IRefineRunner, RefineRunner>();
builder.Services.AddSingleton<RefineRunner>();
builder.Services.AddSingleton<IRefineBroadcaster>(sp => sp.GetRequiredService<HubBroadcaster>());
// "Continue on session limit reset" toggle — depends on UsageState, registered below.
@@ -199,7 +198,7 @@ builder.Services.AddScoped<ClaudeDoDbContext>(sp =>
builder.Services.AddScoped<TaskRepository>();
builder.Services.AddScoped<ListRepository>();
builder.Services.AddScoped<PlanningMcpService>();
builder.Services.AddScoped<IFindingsStoreLocator, FindingsStoreLocator>();
builder.Services.AddScoped<FindingsStoreLocator>();
builder.Services.AddScoped<TaskRunFindingsMcpTools>();
builder.Services.AddMcpServer()
.WithHttpTransport()
@@ -339,8 +338,8 @@ if (cfg.ExternalMcpPort > 0)
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<AttachmentStore>());
externalBuilder.Services.AddScoped<TaskAttachmentRepository>();
externalBuilder.Services.AddScoped<AttachmentMcpTools>();
externalBuilder.Services.AddSingleton<IFindingsStore>(app.Services.GetRequiredService<IFindingsStore>());
externalBuilder.Services.AddScoped<IFindingsStoreLocator, FindingsStoreLocator>();
externalBuilder.Services.AddSingleton(app.Services.GetRequiredService<FindingsStore>());
externalBuilder.Services.AddScoped<FindingsStoreLocator>();
externalBuilder.Services.AddScoped<FindingsMcpTools>();
externalBuilder.Services.AddMcpServer()
.WithHttpTransport()
@@ -1,8 +0,0 @@
namespace ClaudeDo.Worker.Refine;
public interface IRefineRunner
{
Task<RefineRunOutcome> RefineAsync(string taskId, CancellationToken ct);
}
public sealed record RefineRunOutcome(bool Success, string Message);
+3 -1
View File
@@ -7,7 +7,9 @@ using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Refine;
public sealed class RefineRunner : IRefineRunner
public sealed record RefineRunOutcome(bool Success, string Message);
public sealed class RefineRunner
{
private static readonly TimeSpan RunTimeout = TimeSpan.FromMinutes(5);
private const int MaxTurns = 5;
@@ -1,7 +0,0 @@
namespace ClaudeDo.Worker.Report.Interfaces;
public interface IWeekReportService
{
Task<string?> GetStoredAsync(DateOnly start, DateOnly end, CancellationToken ct = default);
Task<string> GenerateAsync(DateOnly start, DateOnly end, CancellationToken ct = default);
}
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
namespace ClaudeDo.Worker.Report;
public sealed class WeekReportService : IWeekReportService
public sealed class WeekReportService
{
private static readonly string[] DefaultExcludes = { @"C:\Private" };
private const string NoActivity = "_No activity in this period._";
@@ -11,12 +11,12 @@ public sealed record SaveTaskFindingResult(
[McpServerToolType]
public sealed class TaskRunFindingsMcpTools
{
private readonly IFindingsStore _store;
private readonly IFindingsStoreLocator _locator;
private readonly FindingsStore _store;
private readonly FindingsStoreLocator _locator;
private readonly TaskRunMcpContextAccessor _ctx;
public TaskRunFindingsMcpTools(
IFindingsStore store, IFindingsStoreLocator locator, TaskRunMcpContextAccessor ctx)
FindingsStore store, FindingsStoreLocator locator, TaskRunMcpContextAccessor ctx)
{
_store = store;
_locator = locator;