# Shadow Run Endpoint Contract (AGENTS.md v16.0)

## Endpoint Specification

```
POST /api/shadow-runs

Request (JSON):
{
  "model_id": "uuid",
  "window_start": "2024-01-02",
  "window_end": "2026-08-02",
  "phase_filter": "All" | "BullMarket" | "BearMarket" | "Sideways" | "HighVolatility"
}

Response (202 Accepted):
{
  "run_id": "uuid",
  "status": "Queued",
  "job_id": "uuid (hangfire job id)",
  "estimated_seconds": 3600,
  "created_at": "2026-08-02T12:34:56Z"
}

Error Responses:
- 400 Bad Request: Invalid model_id, date window, or phase_filter
- 401 Unauthorized: Missing/invalid authentication
- 403 Forbidden: Insufficient role (researcher required)
- 409 Conflict: Duplicate run (Idempotency-Key already exists)
- 500 Internal Server Error: Hangfire queue unavailable
```

## Idempotency

**Header:** `Idempotency-Key: {uuid}`

- Client generates UUID for each request
- Server deduplicates: same Idempotency-Key → same response (202)
- Stored in database: shadow_run_idempotency_key table

## Authorization

**RBAC Role:** Researcher (can initiate shadow runs)

- Enforced via PermissionGuard middleware
- Logged in audit trail (correlationId)

## Workflow

1. **Endpoint receives request**
   - Validate model_id exists
   - Validate date window (window_end >= window_start)
   - Validate phase_filter enum
   - Check Idempotency-Key (return cached response if duplicate)

2. **Handler creates command**
   - Instantiate ShadowRunCommand (with RunId, CorrelationId, IdempotencyKey)
   - Inject into Hangfire job queue (q-research)

3. **Response returns immediately (202)**
   - run_id for polling
   - job_id for monitoring
   - estimated_seconds for UX guidance

4. **Async Hangfire Job**
   - Executes ShadowRunJob in background
   - Phases: DataBackfill → Replay → Evaluation → Persist
   - Updates shadow_run.status as phases complete

5. **Client polls for results**
   - GET /api/shadow-runs/{run_id}
   - Returns status, metrics (when complete)

## Audit Trail

All requests logged with:
- CorrelationId (trace end-to-end)
- ModelId (which model was tested)
- WindowStart/End (date range)
- UserId (who initiated)
- IpAddress (security audit)

## Failure Modes

| Scenario | Status Code | Recovery |
|----------|-------------|----------|
| Model not found | 400 | User corrects model_id |
| Invalid date window | 400 | User corrects dates |
| Hangfire queue down | 500 | Retry (exponential backoff) |
| Duplicate Idempotency-Key | 202 | Return cached run_id |
| DB constraint (run_id collision) | 500 | Retry (UUID collision is ~impossible) |

---

## AGENTS.md v16.0 Checklist

| Criterion | Status | Notes |
|-----------|--------|-------|
| SOLID | ✅ | Endpoint → Handler → Policy separation; DI for repos, services |
| Complexity | ✅ | Endpoint: validation only; Handler: orchestration; Cyclomatic < 10 |
| Audit | ✅ | CorrelationId, UserId, timestamps in shadow_run record |
| Necessity | ✅ | Grounded in "Validation Gates" requirement (CLAUDE.md) |
| Normalization | ✅ | Writes atomic (single shadow_run row + idempotency record) |
| Simplicity | ✅ | No hidden state; explicit validation errors |
| Pattern | ✅ | Vertical Slice (Endpoint → Handler → Policy); FastEndpoints |
| Guardrails | ✅ | Idempotent (Idempotency-Key), no partial success, rollback-safe |
| Traceability | ✅ | CorrelationId preserved across logs, audit trail |
| Safety | ✅ | Idempotent retry; Hangfire durable queue; no data loss |
| Maturity | ✅ | Contract → Implementation → Test sequencing |
| Right Way | ✅ | No shortcuts; proper error handling; code reviewed |
| Debt | ✅ | No new unbounded debt |
