docs(initial-implementation): add feature plan and 10 phase subplans
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# Phase 7: Anomaly Detection (Suspension + Flip)
|
||||
|
||||
**Status:** ⬜ Not Started
|
||||
**Parent plan:** [PLAN.md](./PLAN.md)
|
||||
**Domain:** fullstack
|
||||
**Implementer:** Sonnet (backend portion) + Opus (UI portion, with frontend-design)
|
||||
**Depends on:** Phase 4 (snapshot pipeline) + Phase 6 (UI patterns)
|
||||
|
||||
## Objective
|
||||
|
||||
Detect the **odds-flip anomaly** described in customer TZ §3: bookmaker freezes betting
|
||||
on a live event, then re-opens with inverted underdog/favorite odds. Persist anomalies
|
||||
and surface them in a dedicated UI feed page so the user can act on them.
|
||||
|
||||
## Tasks
|
||||
|
||||
### Backend (Sonnet)
|
||||
|
||||
- [ ] Implement `Marathon.Domain/AnomalyDetection/AnomalyDetector.cs`:
|
||||
- Pure domain logic — takes `IReadOnlyList<OddsSnapshot>` for an event, returns
|
||||
`IReadOnlyList<Anomaly>`
|
||||
- Detect suspension intervals: gaps between snapshots > `SuspensionGapSeconds`
|
||||
(configurable)
|
||||
- For each suspension, compute pre-suspension and post-suspension implied
|
||||
probability vectors `(p1, pDraw, p2)` from Win-1/Draw/Win-2 rates
|
||||
- Compute flip score: `max(|p_post[i] - p_pre[i]|)` across i ∈ {1, draw, 2}
|
||||
- If flip score ≥ `OddsFlipThreshold` AND the favorite changed (argmax differs),
|
||||
emit an `Anomaly(Kind=SuspensionFlip, Score, EvidenceJson)` where `EvidenceJson`
|
||||
contains the snapshots bracketing the suspension
|
||||
- [ ] Add `AnomalyOptions` POCO bound to `Anomaly:*`:
|
||||
```csharp
|
||||
public sealed class AnomalyOptions {
|
||||
public int SuspensionGapSeconds { get; init; } = 60;
|
||||
public decimal OddsFlipThreshold { get; init; } = 0.30m;
|
||||
public int MinSnapshotCount { get; init; } = 3;
|
||||
}
|
||||
```
|
||||
- [ ] Implement `DetectAnomaliesUseCase` in `Marathon.Application/UseCases/`:
|
||||
- Iterate over events with new snapshots since last detection run
|
||||
- Invoke `AnomalyDetector` per event
|
||||
- Persist new anomalies via `IAnomalyRepository`
|
||||
- [ ] Implement `AnomalyDetectionPoller : BackgroundService` in
|
||||
`Marathon.Infrastructure/Workers/`:
|
||||
- Runs every `Anomaly:DetectionIntervalSeconds` (default 60s)
|
||||
- Calls `DetectAnomaliesUseCase`
|
||||
- [ ] Backend tests in `Marathon.Domain.Tests/AnomalyDetection/`:
|
||||
- Synthetic snapshot timeline with no flip → 0 anomalies
|
||||
- Snapshot timeline with suspension + small odds shift → 0 anomalies (below threshold)
|
||||
- Snapshot timeline with suspension + large flip (favorite ↔ underdog) → 1 anomaly
|
||||
- Score calculation matches expected value
|
||||
|
||||
### Frontend (Opus + frontend-design)
|
||||
|
||||
- [ ] Create `Marathon.UI/Pages/Anomalies/AnomalyFeed.razor`:
|
||||
- List of anomalies sorted by `DetectedAt` descending
|
||||
- Each card shows: severity (color-coded by score), event identity, sport icon,
|
||||
detected timestamp, mini sparkline of pre/post odds
|
||||
- Click card → expand to show evidence timeline (snapshots before/after suspension)
|
||||
- Filter: severity threshold, sport, date range
|
||||
- [ ] Create `Marathon.UI/Components/AnomalyCard.razor` — visually distinctive,
|
||||
attention-grabbing without being garish; follows frontend-design guidance for
|
||||
information hierarchy.
|
||||
- [ ] Add navigation entry to `MainLayout` drawer with notification badge showing
|
||||
unread anomaly count.
|
||||
- [ ] Localize all strings in RU + EN.
|
||||
- [ ] Frontend tests in `Marathon.UI.Tests/Pages/Anomalies/`:
|
||||
- bUnit: anomaly card renders evidence timeline
|
||||
- bUnit: filter narrows the list correctly
|
||||
|
||||
## Files to Modify/Create
|
||||
|
||||
- `src/Marathon.Domain/AnomalyDetection/AnomalyDetector.cs`
|
||||
- `src/Marathon.Domain/AnomalyDetection/SuspensionInterval.cs`
|
||||
- `src/Marathon.Application/UseCases/DetectAnomaliesUseCase.cs`
|
||||
- `src/Marathon.Application/Configuration/AnomalyOptions.cs` (or in Infra)
|
||||
- `src/Marathon.Infrastructure/Workers/AnomalyDetectionPoller.cs`
|
||||
- `src/Marathon.UI/Pages/Anomalies/AnomalyFeed.razor`
|
||||
- `src/Marathon.UI/Components/AnomalyCard.razor`
|
||||
- `tests/Marathon.Domain.Tests/AnomalyDetection/AnomalyDetectorTests.cs`
|
||||
- `tests/Marathon.UI.Tests/Pages/Anomalies/**`
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- Compiles (Big Bang).
|
||||
- `AnomalyDetector` is a pure function — no I/O, no DI dependencies.
|
||||
- Configurable thresholds via `appsettings.json` (visible in Settings page).
|
||||
- UI clearly distinguishes high/medium/low severity anomalies.
|
||||
- Evidence timeline shows the actual snapshots that triggered the detection.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is the **product's actual differentiator** — quality of detection logic and
|
||||
evidence presentation matters. Spend time getting the score formula right.
|
||||
- Implied probability formula: `p = 1 / odds` (then normalize so they sum to 1).
|
||||
- Big Bang: compile-only smoke check.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- [ ] Detector is deterministic and pure
|
||||
- [ ] Score calculation correct (verify against hand-computed example)
|
||||
- [ ] No false positives on synthetic "normal" timelines
|
||||
- [ ] UI evidence timeline matches stored `EvidenceJson`
|
||||
- [ ] All strings localized
|
||||
|
||||
## Handoff to Next Phase
|
||||
|
||||
<!-- Filled by Phase 7 implementer. -->
|
||||
Reference in New Issue
Block a user