Three fixes surfaced when launching the WPF host for the first time:
1. App.xaml.cs — call MarathonDbContextInitializer.InitializeAsync()
between Host.Build() and Host.Start() so EF migrations + WAL pragma
are applied BEFORE BackgroundServices race to query the DB. Without
this, all pollers crashed on 'no such table: Events'.
2. wwwroot/index.html — added <script src='https://cdn.plot.ly/plotly-2.35.2.min.js'>
before blazor.webview.js. Phase 6 reviewer flagged this for Phase 9,
but charts are unrenderable without it; better to ship now.
3. Migrations/20260505000000_InitialCreate.cs — added [DbContext] and
[Migration('20260505000000_InitialCreate')] attributes. Phase 2's
hand-written migration was missing both, so EF saw 'no migrations to
apply' even on a fresh DB. With the attributes, the migration runs
on first launch and creates all tables (Events, Snapshots, Bets,
EventResults, Anomalies, Sports, Leagues).
Verified: clean DB → migration applied → all 7 tables created → pollers
run with empty results (no data yet — UpcomingEventsPoller fires every 6h
by default; first scrape will populate the DB).
Phase 7 reviewer (Sonnet, combined backend + frontend) flagged 3 🟡 warnings;
two real fixes here, one tracking:
W1 — DetectAnomaliesUseCase had an undocumented N+1: _anomalyRepo.ListAsync
was called inside ProcessEventAsync, once per event. Hoisted to ExecuteAsync
before the loop and threaded into ProcessEventAsync as a parameter. The
per-event slice happens in-memory now. O(N_events) DB round-trips → 1.
W2 — AnomalyDetector.ExtractMatchWinProbabilities had a dead expression
'(decimal?)null ?? 0m' that always evaluated to 0m. Simplified to
'drawBet is not null ? rawDraw / total : 0m'. The 0m is never surfaced
anyway (PDraw in the return uses the same null guard), so behaviour is
identical.
W3 — PLAN.md row updated with both Phase 7 commit hashes (a6ff368 backend
+ 12208a4 frontend) and review verdict.
Build 0/0, 276 tests still passing.
- AnomalyDetector (pure domain): detects odds-flip pattern from live snapshot
timelines using implied-probability vectors (p=1/rate, normalised), flip score
= max(|p_post−p_pre|), gated by both threshold AND favourite-changed test
- SuspensionInterval record: typed pair of (pre, post) OddsSnapshot bracketing a gap
- AnomalyOptions POCO (Application layer): bound to Anomaly:* config section with
four fields (SuspensionGapSeconds=60, OddsFlipThreshold=0.30, MinSnapshotCount=3,
DetectionIntervalSeconds=60)
- DetectAnomaliesUseCase: iterates all events, loads last-24h live snapshots, runs
detector, persists new anomalies with 1-minute dedup window
- AnomalyDetectionPoller: BackgroundService polling every DetectionIntervalSeconds,
gated by WorkerOptions.AnomalyDetectionEnabled (default true)
- DI wiring: DetectAnomaliesUseCase registered Scoped in ApplicationModule;
AnomalyOptions bound + AnomalyDetectionPoller hosted in InfrastructureModule
- WorkerOptions.AnomalyDetectionEnabled added; appsettings.json updated
- 13 domain tests + 4 application tests; total 245/245 passing (no regression)
Phase 4 reviewer (Sonnet) flagged two 🟡 warnings; both addressed:
1. PullLiveOddsUseCase: removed dead 'liveEvents' assignment that called
ScrapeUpcomingAsync but never read the result. Replaced misleading
comment block with a single TODO(phase-6/8) note pointing to the
ListLiveAsync(cutoff) follow-up.
2. Marathon.UI.Services.WorkerOptions.UpcomingScheduleCron: changed default
from '0 */5 * * * *' (every 5 min) to '0 0 */6 * * *' (every 6 hours)
to match Marathon.Infrastructure.Configuration.WorkerOptions and the
appsettings.json default.
PLAN.md: Phase 4 row updated (review status, commit hash); top-level
checkbox in Phases list ticked.
Build 0/0, all 202 tests still passing.
Combined-batch reviewer flagged three real blockers + two test-infra
issues across the parallel P2/P3/P5 batch. All resolved:
PHASE 3 — DateTimeOffset UTC-kind constructor (3 sites)
EventListingParserBase.cs:39, EventOddsParser.cs:72, ResultsParser.cs:104
Replaced `new DateTimeOffset(DateTimeOffset.UtcNow.UtcDateTime, MoscowOffset)`
(throws ArgumentException because UtcDateTime has Kind=Utc) with
`DateTimeOffset.UtcNow.ToOffset(MoscowOffset)`.
PHASE 2 — EF string.Compare not translatable (3 sites)
EventRepository.cs:34, SnapshotRepository.cs:46, ExcelExporter.cs:35
Replaced `string.Compare(col, str, StringComparison.Ordinal)` with
`col.CompareTo(str)` so EF Core's SQLite provider can translate the
expression. Semantics unchanged (SQLite default collation = BINARY = ordinal).
PHASE 3 — ServerTimeProvider regex misses JSON-quoted key
Regex `serverTime\s*:\s*"..."` only matched bare-key form. Updated to
`"?serverTime"?\s*:\s*"..."` so the JSON-quoted form (the actual
marathonbet.by production format) is matched.
PHASE 3 — fixture: orphan <td> elements stripped by HTML5 parser
tests/.../Fixtures/marathonbet/event-football-sample.html — wrapped
the <td> blocks in a proper <table><tbody><tr> hierarchy so AngleSharp
preserves them and `td.Closest("td")` succeeds in the parser.
PHASE 2 — InMemoryDbFixture shared state across parallel tests
All fixture instances used `Data Source=marathon_tests` causing xUnit's
parallel-within-class runs to contaminate each other's data. Each fixture
now uses a Guid-suffixed unique data source name.
PLAN.md — P2/P3/P5 rows updated to ✅ Done with batch commit reference.
Test status:
Domain.Tests: 96/96 ✅
Application.Tests: 1/1 ✅
Infrastructure.Tests: 77/77 ✅
UI.Tests: 11/11 ✅
TOTAL: 185/185 ✅
Build: 0 warnings, 0 errors.
Deferred to later phases (per reviewer 🟡 / 🔵 notes):
- SnapshotRepository.GetAsync(Guid) uses lossy GetHashCode workaround;
Phase 4 to fix or remove from interface.
- Excel Sport name column writes string.Empty (need lookup join in Phase 6).
- PeriodScopeMapper football n>2 falls through to "Quarter" token;
guarded by MaxPeriods today, but defensive cleanup at Phase 9.
- Settings.razor duplicate m-rise-5 class on Localization section.
Three minimal fixes to make Marathon.sln build with 0/0:
1. Marathon.Infrastructure.csproj — add InternalsVisibleTo for
Marathon.Infrastructure.Tests so test code can reference internal
repository and exporter classes (Phase 2 issue blocking Phase 3 tests).
2. EventOddsParserTests.cs — add 'using Marathon.Domain.ValueObjects' so
MatchScope/PeriodScope resolve.
3. RoundTripTests.cs — add 'using Microsoft.EntityFrameworkCore' so the
ExecuteSqlRawAsync extension method on DatabaseFacade resolves.
Phase 5's anticipated LocalizationOptions / Serilog issues were already
resolved by its agent before being killed — no changes needed there.
Build status: 0 warnings, 0 errors.
Test status: Domain 96/96, UI 11/11, Infrastructure 42/77 (35 failing —
parser fixture issues + a real DateTimeOffset bug; reviewer will assess).
Per Phase 1 reviewer notes — strips Component1.razor, ExampleJsInterop, and
the wwwroot template assets generated by 'dotnet new razorclasslib'. Phase 5
will populate Marathon.UI from scratch with the real layout, components, and
wwwroot/index.html for BlazorWebView. Build still green (0/0).
Creates the 9-project .NET 8 solution (5 src + 4 test) with Marathon.Domain
fully implemented: value objects (SportCode, EventId, OddsRate, OddsValue,
BetScope hierarchy), enums (Side, BetType, OddsSource, AnomalyKind), and
entities (Sport, Country, League, Event, Bet, OddsSnapshot, EventResult,
Anomaly) with all invariants enforced in constructors. 96 domain tests pass
(FluentAssertions + xUnit). Directory.Build.props and Directory.Packages.props
centralise build settings and NuGet versions. Both Marathon.sln and Marathon.slnx
are committed; dotnet build Marathon.sln succeeds with 0 warnings/errors.
Anonymous scraping confirmed feasible for marathonbet.by — site is fully SSR
(nginx), no Cloudflare or JS challenge. HttpClient + AngleSharp + Polly v8 is
sufficient; Playwright not required (kept as a future-flag).
Spike outputs:
- spike/SCRAPE_FINDINGS.md — page rendering, URL templates, anti-bot, rate
limits, recommended scraping strategy for Phase 3.
- spike/SCHEMA_DRAFT.md — customer-spec field → DOM selector mapping for
Match + Period-N scope across football/basketball/tennis (hockey TBD).
Phase 1+ handoff captured in subplan + CLAUDE.md. Critical Phase 8 finding:
no public results endpoint at /su/results — phase 8 must switch to polling
event-detail until eventJsonInfo.matchIsComplete=true (deviation flagged).
Reviewer notes addressed:
- Period market outcome codes corrected to RN_H/RN_D/RN_A (not 1/draw/3) and
market name vocabulary clarified per-sport in SCHEMA_DRAFT §3.1.
- results-page.html capture added to file list with caveat about live-landing
score-state and unsampled hockey selectors.