2bb13ce2d5
Implements AGENTS.md v16.0 final integration for shadow run lifecycle:
Registration & Startup (Program.cs):
- AddMemoryCache() + AddHttpClient()
- GetShadowRunQuery registered for dependency injection
- Services ready for async job execution
Query Service (GetShadowRunQuery.cs):
- PIT-safe SELECT: published_at <= @cutoff
- Deserializes JSONB metrics/gates (typed DTOs)
- Returns null for missing run_id (404 handler)
Polling Endpoint (GET /api/shadow-runs/{run_id}):
- Returns 200 with status (in-progress) or metrics (complete)
- Returns 404 if run not found
- Supports async job polling pattern (202 POST → GET until done)
Response DTOs:
- GetShadowRunResponse: Mirrors shadow_run table columns
- ShadowRunMetricsDto: Typed deserialize from JSONB
- ValidationGatesDto: Typed deserialize from JSONB
- Optional fields: metrics/gates null if status ≠ EvaluationComplete
Tests (6/6 passing):
- In-progress status (no metrics/gates)
- Complete status (all gates passed)
- Partial gate failure (PBO > 20%)
- Failed status (error message preserved)
- Response deserialization (all fields)
- Request with valid run_id
Architecture Adherence (AGENTS.md v16.0):
- SOLID: Query service separation, DI injection
- Complexity: Endpoint/Query cyclomatic < 10
- Audit: PIT safety, CorrelationId in logs
- Safety: Idempotent reads, eventual consistency
- Maturity: Contract → Test → Implementation
Integration Complete:
✅ Phase 1: Shadow Run Design (Domain + Jobs)
✅ Phase 2: Infrastructure (DB Schema + Services)
✅ Phase 3: API Endpoint (FastEndpoints trigger)
✅ Phase 4: Endpoint validation (Fluent validators)
✅ Phase 5: Hangfire registration + polling
Shadow Run System Ready:
- User POSTs /api/shadow-runs (202 Accepted)
- Hangfire job enqueues to q-research
- User polls GET /api/shadow-runs/{run_id}
- Results available after job completion
- Metrics/gates validated per CLAUDE.md requirements
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
56 lines
1.8 KiB
C#
56 lines
1.8 KiB
C#
using FastEndpoints;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace KArtSell.Host.Features.ShadowRun;
|
|
|
|
/// <summary>
|
|
/// GET /api/shadow-runs/{run_id}
|
|
/// Poll shadow run status and retrieve results.
|
|
/// Returns 200 with status (in progress) or 200 with metrics (complete).
|
|
/// </summary>
|
|
public sealed class GetShadowRunPollingEndpoint : Endpoint<GetShadowRunPollingRequest, GetShadowRunResponse>
|
|
{
|
|
private GetShadowRunQuery? _query;
|
|
private ILogger<GetShadowRunPollingEndpoint>? _logger;
|
|
|
|
private static readonly Action<ILogger, Guid, Exception?> LogPolling =
|
|
LoggerMessage.Define<Guid>(
|
|
LogLevel.Debug,
|
|
new EventId(1, nameof(LogPolling)),
|
|
"Polling shadow run {RunId}");
|
|
|
|
private static readonly Action<ILogger, Guid, Exception?> LogNotFound =
|
|
LoggerMessage.Define<Guid>(
|
|
LogLevel.Information,
|
|
new EventId(2, nameof(LogNotFound)),
|
|
"Shadow run {RunId} not found");
|
|
|
|
public override void Configure()
|
|
{
|
|
Get("/api/shadow-runs/{RunId}");
|
|
AllowAnonymous(); // TODO: Add RBAC
|
|
}
|
|
|
|
public override async Task HandleAsync(GetShadowRunPollingRequest req, CancellationToken ct)
|
|
{
|
|
_query = Resolve<GetShadowRunQuery>();
|
|
_logger = Resolve<ILogger<GetShadowRunPollingEndpoint>>();
|
|
|
|
LogPolling(_logger, req.RunId, null);
|
|
|
|
var result = await _query.GetAsync(req.RunId, ct);
|
|
|
|
if (result == null)
|
|
{
|
|
LogNotFound(_logger, req.RunId, null);
|
|
ThrowError($"Shadow run {req.RunId} not found", 404);
|
|
return;
|
|
}
|
|
|
|
HttpContext.Response.StatusCode = 200;
|
|
await HttpContext.Response.WriteAsJsonAsync(result, ct);
|
|
}
|
|
}
|
|
|
|
public sealed record GetShadowRunPollingRequest(Guid RunId);
|