feat(insights): anomaly outcome validator — hit-rate calibration page
Adds a calibration dashboard that joins persisted SuspensionFlip anomalies with EventResult rows and reports whether the post-flip favourite actually won — the single metric that says whether the detector is doing its job. Domain: - AnomalyEvidenceData + AnomalyEvidenceParser to read the JSON written by AnomalyDetector without re-implementing the schema. - AnomalyOutcomeEvaluator: pure function returning Hit / Miss / Unresolved. Tennis-style two-way markets with a Draw winner are downgraded to Unresolved rather than silently counted as Miss. - AnomalySeverityThresholds: shared Low/Medium/High constants so the UI badge and the report buckets cannot drift. Application: - EvaluateAnomalyOutcomesUseCase orchestrates the join + aggregation. - AnomalyOutcomeReport carries totals, hit rate, three breakdowns (severity / sport / score bins) and a per-event title lookup so the UI needs no second pass over IEventRepository. - Score bins extend below 0.30 automatically when the operator lowers the detector threshold so the histogram total always equals ResolvedCount. UI: - Insights page at /anomalies/insights — hero header, 4-card KPI strip (hit rate tinted by tone), three breakdown grids with bar visualisation, drill-down tables for resolved and unresolved anomalies. Honors prefers-reduced-motion. RU + EN localisation. - Nav entry under Analysis section + chip button on the Anomaly Feed. Tests: +42 across Domain + Application (evaluator boundary cases including tennis two-way and Draw guard, score-bin edges, dynamic floor when threshold is lowered, event-title pass-through). All 324 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -305,4 +305,46 @@
|
||||
<data name="Results.Loader.Progress.Failed"><value>Failed</value></data>
|
||||
<data name="Results.Loader.Summary.Format"><value>Loaded {0}, skipped {1}, processed {2} total.</value></data>
|
||||
<data name="Results.Loader.Empty.NoCandidates"><value>No events to load in this range.</value></data>
|
||||
|
||||
<data name="Nav.Insights"><value>Insights</value></data>
|
||||
<data name="Insights.Kicker"><value>Calibration</value></data>
|
||||
<data name="Insights.Title"><value>Did the flips predict the winner?</value></data>
|
||||
<data name="Insights.Lede"><value>Every persisted suspension-flip anomaly joined against the final event result. The hit rate tells you whether the post-flip favourite is the side that actually won — the only metric that says the detector is doing its job.</value></data>
|
||||
<data name="Insights.Stat.HitRate"><value>Hit rate</value></data>
|
||||
<data name="Insights.Stat.HitRate.Hint"><value>Post-flip favourite won.</value></data>
|
||||
<data name="Insights.Stat.Resolved"><value>Resolved</value></data>
|
||||
<data name="Insights.Stat.Resolved.Hint"><value>Anomalies with a graded event.</value></data>
|
||||
<data name="Insights.Stat.Unresolved"><value>Unresolved</value></data>
|
||||
<data name="Insights.Stat.Unresolved.Hint"><value>Awaiting event result.</value></data>
|
||||
<data name="Insights.Stat.Hits"><value>Hits</value></data>
|
||||
<data name="Insights.Stat.Misses"><value>Misses</value></data>
|
||||
<data name="Insights.Stat.Total"><value>Total anomalies</value></data>
|
||||
<data name="Insights.Section.BySeverity"><value>By severity</value></data>
|
||||
<data name="Insights.Section.BySport"><value>By sport</value></data>
|
||||
<data name="Insights.Section.ByScore"><value>By confidence score</value></data>
|
||||
<data name="Insights.Section.Resolved"><value>Resolved anomalies</value></data>
|
||||
<data name="Insights.Section.Unresolved"><value>Awaiting results</value></data>
|
||||
<data name="Insights.Column.DetectedAt"><value>Detected</value></data>
|
||||
<data name="Insights.Column.Match"><value>Match</value></data>
|
||||
<data name="Insights.Column.Sport"><value>Sport</value></data>
|
||||
<data name="Insights.Column.Score"><value>Score</value></data>
|
||||
<data name="Insights.Column.PreFavourite"><value>Pre-flip pick</value></data>
|
||||
<data name="Insights.Column.PostFavourite"><value>Post-flip pick</value></data>
|
||||
<data name="Insights.Column.Winner"><value>Actual winner</value></data>
|
||||
<data name="Insights.Column.Outcome"><value>Verdict</value></data>
|
||||
<data name="Insights.Column.Bucket"><value>Bucket</value></data>
|
||||
<data name="Insights.Column.HitRate"><value>Hit rate</value></data>
|
||||
<data name="Insights.Column.HitsOfTotal"><value>Hits / total</value></data>
|
||||
<data name="Insights.Outcome.Hit"><value>Hit</value></data>
|
||||
<data name="Insights.Outcome.Miss"><value>Miss</value></data>
|
||||
<data name="Insights.Outcome.Unresolved"><value>Pending</value></data>
|
||||
<data name="Insights.Side.Side1"><value>Side 1</value></data>
|
||||
<data name="Insights.Side.Side2"><value>Side 2</value></data>
|
||||
<data name="Insights.Side.Draw"><value>Draw</value></data>
|
||||
<data name="Insights.Side.Unknown"><value>—</value></data>
|
||||
<data name="Insights.Empty.None"><value>No anomalies have been recorded yet. Once the detector flags one and the matching event finishes, its verdict will appear here.</value></data>
|
||||
<data name="Insights.Empty.NoneResolved"><value>Anomalies exist but no matching events have been graded yet. Run the results loader or wait for matches to complete.</value></data>
|
||||
<data name="Insights.Action.Refresh"><value>Refresh</value></data>
|
||||
<data name="Insights.Action.OpenAnomaly"><value>Open</value></data>
|
||||
<data name="Insights.Bucket.NotApplicable"><value>—</value></data>
|
||||
</root>
|
||||
|
||||
Reference in New Issue
Block a user