Scoring Data Model
Overview
This document defines the scoring data model for Archery Apprentice, covering current individual-only behavior, data integrity systems, and planned team mode functionality. Updated September 12, 2025 to reflect Phase 5 implementations.
Core Concepts
ScoreSubject
A ScoreSubject represents the entity for which scores are calculated and displayed. It can be either:
ScoreSubject.Participant(participantId)- Individual participant scoringScoreSubject.Team(teamId)- Team-based scoring (future implementation)
Current Behavior (Individual Mode)
MU Subject Resolution: The MU (Main User) subject is always resolved as ScoreSubject.Participant(localUserId) where localUserId is the ID of the SessionParticipant.LocalUser found in the round’s participant list.
Score Storage:
round.totalScorestores MU-only score in multi-participant roundsround.maxPossibleScorerepresents the maximum possible score for the round format- This design separates MU scores from aggregate scores to avoid data corruption
UI Data Flow:
- Historical and Details screens pull score data exclusively through ViewModel helper methods
- No direct UI math calculations on raw database fields
- All score resolution goes through
resolveMuSubject()→getSubjectScore()→getSubjectMaxScore()pipeline
Data Integrity & Repair Systems ✅ IMPLEMENTED
Automated Upgrade Hook
UpgradeRepairManager (domain/repository/UpgradeRepairManager.kt):
- Purpose: Ensures data repairs run exactly once after each app upgrade
- Mechanism: Uses SharedPreferences key
"aa.repair.v1.done"to track completion - Integration: Wired into
RoundViewModelstartup; runs automatically if needed - Manual Override:
runRepairsManually()bypasses completion flag for Settings action - Error Handling: Failed repairs don’t mark as complete; allow retry on next startup
Backfill Operations
Legacy Totals Backfill (RoundRepository.backfillLegacyTotals()):
- Purpose: Repairs historical rounds where
round.totalScorestored aggregate instead of MU-only scores - Detection: Identifies completed multi-participant rounds with inflated totals via heuristics
- Process: Recalculates correct MU-only totals from arrow-level data; updates
round.totalScore - Execution:
@Transaction-wrapped; DEBUG-gated logging; idempotent operation - Return Value: List of round IDs that were actually repaired (excludes already-correct rounds)
Participant Types Backfill (RoundRepository.backfillParticipantTypes()):
- Purpose: Ensures participant JSON includes proper type discriminator fields for reliable queries
- Detection: Processes all completed rounds with non-empty participant lists
- Process: Re-serializes participant data via TypeConverter to include type information
- Execution:
@Transaction-wrapped; throttled processing; idempotent re-serialization - Return Value: List of round IDs that had participant data updated
Manual Repair Action
Settings Integration (ui/settings/SettingsPage.kt + SettingsViewModel.kt):
- UI: “Repair data now” button with confirmation dialog in Settings screen
- UX: Progress indicator during operation + snackbar results (“Repaired X totals, Y participants”)
- Backend: Calls
UpgradeRepairManager.runRepairsManually()on IO dispatcher - State Management:
repairInProgressStateFlow prevents concurrent operations - Error Handling: Try/catch with fallback counts; always clears progress state
Data Validation & Constraints
Current Model Enforces:
- MU-Only Totals:
round.totalScorecontains only Main User scores (never aggregate) - Participant Integrity: All participant data includes proper type discriminators for queries
- Historical Consistency: Automated repairs ensure legacy data matches current expectations
- Idempotent Operations: All repair methods safe to run multiple times without corruption
- Defensive UI: Historical/Details screens use VM subject resolution, never direct field math
Logging & Debug Support
Production Behavior:
- Quiet operation with minimal logging
- Only essential repair completion messages in release builds
- Error conditions logged at appropriate levels
Debug Features:
BuildConfig.DEBUGgates verbose repair logging- Detailed round-by-round repair progress in debug builds
DebugLog.participantsflag for participant-specific verbose output- Manual repair shows detailed counts in snackbar regardless of build type
Team Mode Scaffolding ✅ IMPLEMENTED
Feature Flag Infrastructure
FeatureFlags.TEAM_MODE (data/models/FeatureFlags.kt):
object FeatureFlags {
const val TEAM_MODE: Boolean = false // TODO: Flip to true when shipping
}RoundScoringMode Enum (data/models/RoundScoringMode.kt):
enum class RoundScoringMode {
INDIVIDUAL, // Current and default behavior
TEAM // Future implementation with team assignments
}Current Implementation (Feature-Gated)
Scoring Mode Inference (RoundViewModel.inferScoringMode()):
private fun inferScoringMode(round: Round): RoundScoringMode =
if (FeatureFlags.TEAM_MODE /* && team assignments exist later */)
RoundScoringMode.TEAM
else
RoundScoringMode.INDIVIDUALEnhanced Subject Resolution (RoundViewModel.resolveMuSubject()):
fun resolveMuSubject(round: Round, settings: Settings?): ScoreSubject {
if (inferScoringMode(round) == RoundScoringMode.TEAM && FeatureFlags.TEAM_MODE) {
// TODO(team-mode): resolve MU's team when team assignments exist
return ScoreSubject.Participant(resolveLocalParticipantId(round)) // placeholder
} else {
return ScoreSubject.Participant(resolveLocalParticipantId(round))
}
}Subject-Aware Score Methods (with team stubs):
getSubjectScore(): Team branch falls back to participant calculationgetSubjectMaxScore(): Team branch falls back to participant calculation- All team branches preserve current behavior until schema implementation
UI Label Preparation
HistoricalRoundsScreen + RoundDetailsScreen:
// TODO(team-mode): switch to real team score when assignments wired.
val isTeam = FeatureFlags.TEAM_MODE && /* future: has team assignments */ false
val scoreLabel = if (isTeam) "Team Score" else "Your Score"Current Behavior: Labels use “Your Score” while feature is gated off; ready to flip when assignments are implemented.
Team Mode Implementation Plan — FUTURE SCHEMA WORK
Schema Extensions (Not Yet Implemented)
Planned additions (when team mode ships):
// Add to Round model:
val scoringMode: RoundScoringMode // INDIVIDUAL or TEAM
val teamAssignments: Map<ParticipantId, TeamId>? // null for individual rounds
// New team-specific data structures:
data class TeamScore(val teamId: TeamId, val totalScore: Int, val maxScore: Int)
data class TeamRanking(val teamId: TeamId, val rank: Int, val members: List<ParticipantId>)Future Subject Resolution Logic
Enhanced resolveMuSubject() (when schema ready):
fun resolveMuSubject(round: Round, settings: Settings?): ScoreSubject {
if (round.scoringMode == RoundScoringMode.TEAM && FeatureFlags.TEAM_MODE) {
val muParticipantId = resolveLocalParticipantId(round)
val teamId = round.teamAssignments[muParticipantId]
return if (teamId != null) ScoreSubject.Team(teamId) else ScoreSubject.Participant(muParticipantId)
} else {
return ScoreSubject.Participant(resolveLocalParticipantId(round))
}
}Score Computation
Team Score Calculation:
- Sum individual member scores:
teamScore = members.sumOf { getParticipantScore(it) } - Sum individual member max scores:
teamMaxScore = members.sumOf { getParticipantMaxScore(it) } - Team accuracy:
teamAccuracy = (teamScore / teamMaxScore) * 100
Team Ranking:
- Rank teams by total team score (descending)
- Handle ties using standard competition ranking rules
- Compute per-team statistics similar to current per-participant stats
UI Changes
Label Updates:
- “Your Score” becomes “Team Score” when
FeatureFlags.TEAM_MODE && hasTeamAssignments - Ranking displays show team names instead of individual names
- Team member lists shown in expanded views
Toggles & Configuration:
- Team vs Individual view toggle in completed round displays
- Team assignment UI in round setup (future)
- Settings for team mode preferences
Implementation Status
Phase 5: Completed ✅ (September 5-12, 2025)
- Data Integrity Systems: Backfill infrastructure + UpgradeRepairManager + Settings repair action
- Historical Screen Fixes: MU vs aggregate confusion solved; subject-aware display pipeline
- Team Mode Scaffolding:
FeatureFlags.TEAM_MODE = false(feature gated) - RoundScoringMode Enum: Presentation-tier scoring mode abstraction
- VM Method Stubs: Subject resolution + score calculation stubs with feature flag guards
- UI Label Preparation: “Your Score” vs “Team Score” variables (using current values while gated)
- TODO Anchors: Comprehensive
TODO(team-mode)markers for future implementation - Documentation: KDoc coverage for all affected methods + comprehensive scoring_data_model.md
- Test Scaffolding:
RoundViewModelTeamScaffoldingTestwith team mode test structure
Next Phase: Schema Implementation (🔄 When Ready)
- Round Model Extensions: Add
scoringModeandteamAssignmentsfields to Round data class - Database Migration: Schema update + migration logic for new Round fields
- Team Score Calculation: Implement real team totals/max/accuracy computation in VM methods
- Team Assignment UI: Round creation flow with team assignment interface
- Team Ranking System: Enable team-based leaderboards and statistics computation
- Team Display Components: Team member lists, expanded views, toggle interfaces
- Comprehensive Testing: Team mode unit tests, integration tests, UI tests
- Feature Flag Flip: Enable
FeatureFlags.TEAM_MODE = truefor release
TODO Search Tags
Use these tags to find all team-mode related code:
TODO(team-mode)- General team mode implementation tasksTODO(team-mode): introduce teamAssignments- Schema-related changesTODO(team-mode): compute teamRank- Ranking calculation updatesTODO(team-mode): expose "Team Score" label- UI label updatesTODO(team-mode): switch to real team score- Score calculation updates
Testing Strategy
Current Tests ✅ IMPLEMENTED
- Individual Mode Preservation: All behavior unchanged under
FeatureFlags.TEAM_MODE = false - Subject Resolution:
resolveMuSubject()returns participant for MU in all cases - Score Calculations: VM score methods produce identical results to previous implementation
- Backfill Operations:
RoundRepositoryrepair methods are idempotent and safe to re-run - Settings Integration: Manual repair action UI and backend integration tested
- Test Scaffolding:
RoundViewModelTeamScaffoldingTest.ktprovides structure for team mode tests
Future Team Tests (When Schema Ready)
- Team Score Computation: Accuracy of team total/max/accuracy calculations
- Team Ranking: Leaderboard ordering with ties and complex scenarios
- Team Assignment Validation: Proper participant-to-team mapping and edge cases
- Mixed Round Handling: Individual and team rounds coexisting in same app instance
- Migration Testing: Conversion from individual to team mode for existing rounds
- UI Integration: Team vs individual label switching and display toggling
- Performance: Team calculations with large participant counts
Architecture Integration Notes
Relationship to Phase 4 (Multi-Participant Foundation)
- Builds On: Per-participant scoring isolation and DB schema from Phase 4
- Extends: Subject abstraction layer enables both individual participants and teams
- Preserves: All Phase 4 functionality (Previous Ends, per-participant stats) unchanged
Integration with Existing Systems
- Equipment Models: Team mode compatible with existing guest bow setup system
- Network Participants: Team assignments work with arbitrary
participantIdvalues - Statistics Pipeline: Team stats can reuse existing per-participant calculation methods
- Historical Data: Repair systems ensure clean foundation for team mode implementation
Performance Considerations
- Display Data Caching:
createRoundDisplayData()caching works for both individual and team modes - Repair Throttling: Backfill operations use in-process throttling to prevent UI blocking
- Feature Flag Isolation: Zero performance impact when
TEAM_MODE = false - Future Optimization: Team score calculations designed for efficient member aggregation