This commit is contained in:
2026-04-09 14:55:37 +02:00
parent 8be455ba98
commit 4a85efc270
30 changed files with 4895 additions and 1 deletions
+106
View File
@@ -0,0 +1,106 @@
# Analytics Taxonomy
## Rules
- All analytics events are structured
- No JSONB payloads
- Events live in `analytics_events`
- Attributes live in `analytics_event_attributes`
## Event Groups
### Lifecycle
- `app_opened`
- `app_backgrounded`
- `app_closed`
- `session_started`
- `session_ended`
### Onboarding
- `language_selected`
- `consent_viewed`
- `consent_accepted`
### Feed and Round Load
- `feed_viewed`
- `round_card_viewed`
- `round_loaded`
- `event_manifest_received`
- `preview_prefetch_started`
- `preview_prefetch_completed`
### Playback
- `preview_started`
- `preview_paused`
- `preview_resumed`
- `preview_completed`
- `reveal_started`
- `reveal_completed`
- `playback_error`
- `stream_reconnected`
### Timing and Odds
- `countdown_visible`
- `countdown_warning_threshold_hit`
- `odds_panel_viewed`
- `odds_version_received`
- `odds_changed`
### Selection and Settlement
- `outcome_focused`
- `outcome_selected`
- `selection_submitted`
- `selection_accepted`
- `selection_rejected`
- `duplicate_selection_attempt`
- `market_locked`
- `result_viewed`
- `next_round_requested`
### UI and Input
- `screen_viewed`
- `cta_pressed`
- `gesture_swipe`
- `gesture_tap`
- `gesture_cancelled`
- `haptic_triggered`
### Localization and Errors
- `localization_bundle_loaded`
- `locale_changed`
- `network_error`
## Common Attributes
- `screen_name`
- `event_id`
- `market_id`
- `outcome_id`
- `odds_version_id`
- `countdown_ms_remaining`
- `locale_code`
- `experiment_variant`
- `network_type`
- `device_orientation`
- `playback_position_ms`
- `latency_ms`
## Research Metrics
- Decision latency
- Round completion rate
- Missed rounds
- Post-lock selection attempts
- Replay desire
- Exit rate
- Control vs modern comparison
- Locale effects
- Device platform effects
+50
View File
@@ -0,0 +1,50 @@
# Architecture
## System Shape
Hermes uses three layers:
- Native client apps for iOS and Android
- A Rust backend API that owns timing and state
- PostgreSQL plus Valkey for durable and short-lived data
## Client Responsibilities
- Render the study experience
- Play preview and reveal video
- Handle gestures and haptics
- Show odds and lock timing
- Keep local session state
- Prefetch media
- Sync clocks with the server
- Capture structured analytics
- Load English and Swedish strings from localization assets
## Backend Responsibilities
- Auth and session binding
- Event feed and manifests
- Markets and odds distribution
- Bet intent validation and acceptance
- Settlement and audit logging
- Experiment assignment
- Localization bundle serving
- Analytics ingestion
- Admin fixture publishing
## Core Rules
- The server decides lock time and settlement
- Clients send intent, not fairness decisions
- Video playback must not block overlay updates
- Client and server state machines must match
- Localization is mandatory for every user-facing string
- Analytics and audit data stay relational
## Repo Order
1. Documents and contracts
2. Backend foundation
3. Domain modules and API coverage
4. Fixture and admin workflows
5. Native iOS and Android apps
+52
View File
@@ -0,0 +1,52 @@
# Localization Catalog
## Rules
- Every user-facing string must be localizable
- Stable key names only
- English is the fallback locale
- Swedish must be present at launch
- Keys are mirrored in `contracts/localization/en.json` and `contracts/localization/sv.json`
## Current Keys
| Key | Purpose |
| --- | --- |
| `app.name` | App name |
| `common.continue` | Continue action |
| `common.cancel` | Cancel action |
| `common.close` | Close action |
| `common.retry` | Retry action |
| `common.loading` | Loading state |
| `common.error_title` | Error title |
| `common.ok` | OK action |
| `onboarding.title` | Study intro title |
| `onboarding.subtitle` | Study intro body |
| `onboarding.consent_title` | Consent title |
| `onboarding.consent_body` | Consent body |
| `onboarding.language_title` | Language picker title |
| `onboarding.start_session` | Start session CTA |
| `feed.next_round_title` | Next round title |
| `feed.next_round_body` | Next round body |
| `feed.watch_preview` | Watch preview CTA |
| `feed.round_ready` | Round ready label |
| `round.countdown_label` | Countdown label |
| `round.locked_label` | Locked label |
| `round.selection_prompt` | Selection prompt |
| `round.selection_confirmed` | Selection accepted |
| `round.selection_submitting` | Selection submitting |
| `round.odds_label` | Odds label |
| `reveal.title` | Reveal title |
| `reveal.subtitle` | Reveal subtitle |
| `result.title` | Result title |
| `result.user_selection` | User selection label |
| `result.outcome` | Outcome label |
| `result.next_round` | Next round CTA |
| `settings.title` | Settings title |
| `settings.language` | Language setting |
| `settings.haptics` | Haptics setting |
| `settings.analytics` | Analytics setting |
| `errors.generic` | Generic error copy |
| `errors.network` | Network error copy |
| `errors.playback` | Playback error copy |
| `errors.session_expired` | Session expired copy |
+192
View File
@@ -0,0 +1,192 @@
# Relational Schema
## Rules
- UUID primary keys
- `timestamptz` for all timestamps
- No JSONB columns
- Child tables for attributes and event payloads
- Foreign keys wherever possible
## Core Tables
### users
- `id`
- `external_ref`
- `created_at`
- `preferred_language`
- `device_platform`
### sessions
- `id`
- `user_id`
- `started_at`
- `ended_at`
- `experiment_variant`
- `app_version`
- `device_model`
- `os_version`
- `locale_code`
### events
- `id`
- `sport_type`
- `source_ref`
- `title_en`
- `title_sv`
- `status`
- `preview_start_ms`
- `preview_end_ms`
- `reveal_start_ms`
- `reveal_end_ms`
- `lock_at`
- `settle_at`
### event_media
- `id`
- `event_id`
- `media_type`
- `hls_master_url`
- `poster_url`
- `duration_ms`
- `preview_start_ms`
- `preview_end_ms`
- `reveal_start_ms`
- `reveal_end_ms`
### markets
- `id`
- `event_id`
- `question_key`
- `market_type`
- `status`
- `lock_at`
- `settlement_rule_key`
### outcomes
- `id`
- `market_id`
- `outcome_code`
- `label_key`
- `sort_order`
### odds_versions
- `id`
- `market_id`
- `version_no`
- `created_at`
- `is_current`
### outcome_odds
- `id`
- `odds_version_id`
- `outcome_id`
- `decimal_odds`
- `fractional_num`
- `fractional_den`
### bet_intents
- `id`
- `user_id`
- `session_id`
- `event_id`
- `market_id`
- `outcome_id`
- `idempotency_key`
- `client_sent_at`
- `server_received_at`
- `accepted`
- `acceptance_code`
- `accepted_odds_version_id`
### settlements
- `id`
- `market_id`
- `settled_at`
- `winning_outcome_id`
### experiment_assignments
- `id`
- `user_id`
- `session_id`
- `variant`
- `assigned_at`
### localization_keys
- `id`
- `key_name`
- `description`
### localization_values
- `id`
- `localization_key_id`
- `locale_code`
- `text_value`
### analytics_event_types
- `id`
- `event_name`
- `description`
### analytics_events
- `id`
- `analytics_event_type_id`
- `session_id`
- `user_id`
- `occurred_at`
### analytics_event_attributes
- `id`
- `analytics_event_id`
- `attribute_key`
- `attribute_value`
### audit_logs
- `id`
- `created_at`
- `actor_type`
- `actor_id`
- `action_name`
- `target_type`
- `target_id`
- `trace_id`
- `note`
### audit_log_attributes
- `id`
- `audit_log_id`
- `attribute_key`
- `attribute_value`
## Indexes
- `sessions.user_id`
- `sessions.started_at`
- `events.status`
- `markets.event_id`
- `markets.lock_at`
- `bet_intents.session_id`
- `bet_intents.market_id`
- `bet_intents.idempotency_key`
- `analytics_events.session_id`
- `analytics_events.user_id`
- `analytics_events.analytics_event_type_id`
- `audit_logs.created_at`
+31
View File
@@ -0,0 +1,31 @@
# State Machines
## Event Lifecycle
`scheduled -> prefetch_ready -> preview_open -> locking -> locked -> reveal_open -> settled -> archived`
## Client Round State
`idle -> prefetching -> ready -> preview_playing -> selection_pending -> selection_submitting -> selection_accepted -> locked -> reveal_playing -> result_visible -> transitioning -> error`
## Bet Acceptance State
`received -> validated -> accepted`
Rejection states:
- `rejected_too_late`
- `rejected_invalid_market`
- `rejected_invalid_session`
- `rejected_duplicate`
Terminal state:
- `settled`
## Notes
- Locking is server authoritative
- Clients may display synced countdowns, but acceptance is decided by the server
- Selection confirmation must be visually unambiguous
- The locked state freezes the odds display for the user