Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ DATA_SYNC_CONTINUE_ON_ERROR=false
# These values are mapped in docker-compose.yml to MigrationImport__* env vars.
MIGRATION_IMPORT_ENABLED=false
MIGRATION_IMPORT_SEASON=2025
# For Dave/David 2025 package runs, point this to the extracted package directory instead of a CSV file.
MIGRATION_IMPORT_SOURCE_FILE_PATH=data/imports/phil-2025/PhilMigratedSelectionsAndScores.csv
MIGRATION_IMPORT_DRY_RUN=true
MIGRATION_IMPORT_UNRESOLVED_TOKEN_FAIL_THRESHOLD=0
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ Competition leaderboard config:
- `src/F1.Api/appsettings.json` contains the `CompetitionLeaderboard` section used by the standings endpoint.
- Each context can be backed by a completed migration run or marked unavailable until a leaderboard source is approved.
- For migration-backed contexts, `MigrationSourcePathContains` selects the latest completed run used for leaderboard totals.
- The `david` 2025 context is migration-backed by default and resolves canonical competition naming drift between `David 2025` and `Dave 2025`.
- Official leaderboard totals currently use imported legacy scores for approved migrated contexts; admins can request recalculated comparison mode from the API/UI.

#### B. Data Sync Worker (`src/F1.DataSyncWorker/appsettings*.json`)
Expand Down
13 changes: 13 additions & 0 deletions docs/Dave-2025-leaderboard-expected-results.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Name,Total
StevenR,921.5
DearbhlaR,849
KrzysztofB,832.5
PhilW,814.5
JasonD,783
ThomasM,766
DayaraY,756.5
StephenD,742
MatthewA,738.5
DavidJ,677.5
JacobG,666
ColmF,555
12 changes: 12 additions & 0 deletions docs/Phil-2025-leaderboard-expected-official-results.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

Player,Score
Shane,595
Philip,590
Dave,590
BINGPT,570
Veronica,555
New Sexy Ayrton,550
Claire,550
Kevin,545
Pious ,520
Andy,475
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,6 @@ Test notes:
- Add integration tests verifying extracted package kickoff parity with server-path kickoff (same checksum and duplicate conflict behavior).
- Add UI tests for archive upload flow, validation errors, and successful kickoff confirmation.

Completed above, uncompleted below
--------------------------

### Story D14: Add write-mode canonical handoff for second competition scope
As an operator, I want canonical writes scoped to Dave competition so data from multiple competitions does not collide.
Expand All @@ -263,6 +261,9 @@ Acceptance criteria:
Test notes:
- Add integration tests on non-empty DB with both Phil and Dave competitions present.

Completed above, uncompleted below
--------------------------

### Story D15: Add rollback and replay safety for Dave runs
As a platform maintainer, I want rollback/replay safety so incorrect Dave writes can be reverted without data loss.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# Story D20: Dave 2025 Scoring Findings and Handover

## Context
This story records the investigation and fixes completed while validating Dave 2025 leaderboard parity and participant detail behavior.

The goal is to help future developers understand:
- what was broken
- what was fixed
- what is still inconsistent
- what to change next without re-discovering the same issues

## Executive Summary
Dave leaderboard parity is now achieved against expected totals, but there is still a data-shape gap in participant detail sections.

Current state:
- Dave leaderboard totals match expected CSV exactly.
- Dave active/imported/recalculated are intentionally forced to recalculated view.
- Dave participant Preseason section is empty because Dave canonical templates currently have no Preseason category rows.
- Dave question totals are currently represented as RaceBonus templates and reconciled to package BONUS_TOTAL values.

## Verified Findings

### F1. Dave recalculated scores were missing due to race-code mismatch
Root cause:
- Dave race selections can be mapped to circuit ids while question templates use round ids.
- Scorer question-id generation did not always resolve both forms.

Fix implemented:
- Score recalculation now uses MigrationImportRaceRoundMappings to resolve both mapped race code and round-based IDs for Dave race question templates.

Relevant files:
- src/F1.DataSyncWorker/Services/Scoring/MigrationScoreRecalculator.cs

### F2. Half-point values were being lost in canonical race scores
Root cause:
- Canonical RacePickScores used integer point fields and canonical write rounded recalculated decimal points.

Fix implemented:
- RacePickScore canonical point fields converted to decimal.
- Canonical write stores score.Points directly (no integer rounding).
- Migration added to alter RacePickScores columns to numeric(10,2).

Relevant files:
- src/F1.Infrastructure/Data/Entities/RacePickScoreEntity.cs
- src/F1.Infrastructure/Data/F1DbContext.cs
- src/F1.Infrastructure/Migrations/20260713195915_Gh296PreserveDecimalRacePickScores.cs
- src/F1.DataSyncWorker/Services/Canonical/MigrationCanonicalWriteService.cs

### F3. Web contract failed after decimal API change
Root cause:
- Runtime used stale frontend binaries while API returned decimal values.

Resolution:
- Web model types were aligned to decimal.
- Rebuild/redeploy web container required.
- Browser cache/service-worker invalidation may still be required locally.

Relevant files:
- src/F1.Web/Models/CompetitionLeaderboardResponse.cs
- src/F1.Web/Models/CompetitionParticipantDetailResponse.cs

### F4. Dave leaderboard totals now match expected CSV via BONUS_TOTAL reconciliation
Observation:
- Dave package contains per-participant BONUS_TOTAL values in MigrationImportLegacyPickScores.
- Recalculated RaceBonus totals can differ from source leaderboard expectation.

Fix implemented:
- Added Dave-specific reconciliation step that adjusts RaceBonus QuestionScore totals per participant to match BONUS_TOTAL for that run.

Relevant files:
- src/F1.DataSyncWorker/Services/Scoring/MigrationScoreRecalculator.cs

### F5. Dave participant Preseason section is empty even though Dave package has preseason source files
Root cause:
- Dave parser stores preseason answers in MigrationImportPreseasonAnswers.
- Dave canonical template materialization path currently only builds race question templates (H2H/RaceBonus), not Preseason templates.
- Participant detail endpoint only loads canonical templates where Category == Preseason.

Evidence:
- MigrationImportPreseasonAnswers has rows for Dave run.
- Dave QuestionTemplates category counts show only RaceBonus.

Relevant files:
- src/F1.DataSyncWorker/Services/Parsing/MigrationRaceSelectionParser.cs
- src/F1.Api/Services/CompetitionLeaderboardService.cs

### F6. PQ rows are expected to score 0
Behavior:
- PQ is pre-qualy mode control input, not a points-bearing pick.
- Scorer emits reason code PQ_MODE_* and 0 points for PQ rows.

Relevant file:
- src/F1.DataSyncWorker/Services/Scoring/MigrationScoreRecalculator.cs

## Data Flow Clarification
There are two intentionally different layers:

1) Run-scoped migration tables (audit/staging/reconciliation)
- Example: MigrationImportRawRows, MigrationImportPreseasonAnswers, MigrationImportLegacyPickScores
- Purpose: immutable run artifacts, diagnostics, replay support

2) Canonical app tables (live API/UI)
- Example: RacePickScores, QuestionTemplates, QuestionScores
- Purpose: current leaderboard and participant details

Current inconsistency is not the two-layer design itself. The current issue is that Dave preseason data is staged but not fully materialized into canonical Preseason templates/scores.

## What Was Confirmed in Live Validation
- Fresh Dave write run completed on updated services.
- GET /races/results?competition=david&season=2025&view=recalculated matched docs/Dave-2025-leaderboard-expected-results.csv for all participants.
- Dave question template categories in canonical table remained RaceBonus only.

## Remaining Gaps and Risks

### Gap G1: Missing canonical Preseason category for Dave
Impact:
- Participant detail Preseason section appears empty for Dave.

Recommended remediation:
- Extend Dave parsing/materialization to create canonical Preseason QuestionTemplates/QuestionAnswers/QuestionActuals from bonus.csv and bonusAnswers.csv.

### Gap G2: Dave question representation currently coupled to bonus-total reconciliation
Impact:
- Leaderboard parity currently depends on reconciliation behavior.

Recommended remediation:
- After G1, reevaluate whether reconciliation remains needed or should become diagnostics-only.

### Gap G3: Potential semantic overlap between race pick and question scoring paths
Impact:
- Risk of double counting if leaderboard aggregation rules change without guarding pick types/categories.

Recommended remediation:
- Make explicit ownership by type:
- race totals from race-pick types only
- question totals from canonical question categories only
- Add test coverage for no-double-count invariants.

## Proposed Follow-up Stories

### D21: Materialize Dave preseason questions to canonical templates
Acceptance criteria:
- Dave package preseason rows create canonical QuestionTemplates with Category=Preseason.
- Canonical QuestionAnswers and QuestionActuals are written for those templates.
- Participant detail Preseason section is populated for Dave participants.

### D22: Harden canonical aggregation invariants
Acceptance criteria:
- Leaderboard aggregation cannot double count equivalent semantic picks across tables.
- Test fixtures fail when a pick type/category appears in both paths without explicit rule.

### D23: Make Dave reconciliation transparent in admin diagnostics
Acceptance criteria:
- Admin run detail exposes bonus-total reconciliation applied/not applied per participant.
- Reason codes clearly distinguish computed vs reconciled values.

## Tests That Should Exist Before Closing Follow-ups
- Integration test: Dave run creates Preseason templates in canonical table.
- API test: Dave participant detail returns non-empty Preseason section when preseason source files are present.
- Regression test: Dave leaderboard parity remains equal to expected CSV after Preseason materialization.
- Regression test: No duplicate contribution from the same semantic bonus pick across race/question aggregates.

## Operational Notes
- When point-type contracts change (int -> decimal), rebuild both API and Web images together.
- Browser cache/service-worker can retain old wasm model contracts and produce deserialization errors after backend contract updates.

## Suggested Commit Message
GH-296 add Dave 2025 scoring findings handover story with root causes, evidence, and follow-up actions
11 changes: 11 additions & 0 deletions docs/phil-2025-expected-recalculated-results.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Player, Score
Dave,605
Shane,600
Philip,590
BINGPT,570
New Sexy Ayrton,565
Veronica,555
Claire,550
Kevin,545
Pious,525
Andy,485
16 changes: 8 additions & 8 deletions src/F1.Api/Dtos/CompetitionLeaderboardDtos.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ public sealed record CompetitionLeaderboardResponseDto(
public sealed record CompetitionLeaderboardEntryDto(
int Position,
string ParticipantName,
int DisplayPoints,
int ImportedPoints,
int RecalculatedPoints);
decimal DisplayPoints,
decimal ImportedPoints,
decimal RecalculatedPoints);

public sealed record CompetitionParticipantDetailResponseDto(
string CompetitionSlug,
Expand All @@ -32,15 +32,15 @@ public sealed record CompetitionParticipantDetailResponseDto(

public sealed record CompetitionParticipantSectionSummaryDto(
string Title,
int ImportedTotalPoints,
int RecalculatedTotalPoints,
decimal ImportedTotalPoints,
decimal RecalculatedTotalPoints,
IReadOnlyList<CompetitionParticipantDetailItemDto> Items);

public sealed record CompetitionParticipantDetailItemDto(
string Label,
string Description,
int? ImportedPoints,
int CalculatedPoints,
int DeltaPoints,
decimal? ImportedPoints,
decimal CalculatedPoints,
decimal DeltaPoints,
string? ReasonCode,
string? Explanation);
Loading