# StrideClear — build spec

A walking-workout app with the subscription price shown before purchase, self-serve cancellation/refund paths, Health Connect step sync, and adaptive routines that reduce repeated workouts.

## Project context

This spec describes an independent, alternative Android app you are building from scratch to compete with an existing incumbent app on Google Play — not a modification, clone, or reskin of the incumbent's own code, assets, or branding. Use the incumbent only as a market reference (via the report data below), and design working_name/package_id/store_listing/design_system so the result is clearly its own product.

## Incumbent app

- **Name:** Weight Loss Walking: WalkFit
- **Package id:** `com.walkfit.weightloss.steptracker.pedometer`
- **Google Play:** https://play.google.com/store/apps/details?id=com.walkfit.weightloss.steptracker.pedometer
- **appy.fyi report:** https://appy.fyi/report/com.walkfit.weightloss.steptracker.pedometer
- **Category:** Health & Fitness

## Overview

- **Working name:** StrideClear (trademark cleared: no)
- **Package id:** `fyi.appy.strideclear`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 5 weeks
- **Pricing:** subscription, $6.99 via `revenuecat`
- **Runtime AI:** none
- **Permissions:** `INTERNET`

## Non-goals (out of scope for v1)

- No free trial, teaser price, discounted first month, or silent conversion pricing in v1.
- No medical, weight-loss, diagnosis, treatment, or injury-recovery claims.
- No social feed, leaderboards, coaching chat, or community features.
- No custom wearable companion app; v1 syncs wearable step data only through Health Connect.
- No server-side accounts, cloud sync, or cross-device workout-history backup.
- No LLM runtime dependency; adaptive routines use deterministic rules from onboarding answers and difficulty feedback.
- No attempt to process refunds directly through Google Play Developer APIs from the client app; v1 provides self-serve Google Play cancellation and refund entry points inside the app.

## Tech stack

- **Language / UI:** Kotlin, Jetpack Compose
- **Kotlin:** 2.0.21
- **Compose BOM:** 2024.10.01
- **Gradle:** 8.9

| Purpose | Gradle coordinate |
| --- | --- |
| Core Android Kotlin extensions | `androidx.core:core-ktx:1.15.0` |
| Compose activity host | `androidx.activity:activity-compose:1.9.3` |
| Compose Navigation graph | `androidx.navigation:navigation-compose:2.8.3` |
| Lifecycle-aware Compose state collection | `androidx.lifecycle:lifecycle-runtime-compose:2.8.7` |
| Compose ViewModel integration | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7` |
| Room local database runtime | `androidx.room:room-runtime:2.6.1` |
| Room Kotlin coroutines extensions | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor via KSP | `androidx.room:room-compiler:2.6.1` |
| Media3 ExoPlayer for bundled guided workout videos | `androidx.media3:media3-exoplayer:1.4.1` |
| Media3 PlayerView controls embedded in Compose with AndroidView | `androidx.media3:media3-ui:1.4.1` |
| Health Connect client for step summaries and wearable-origin step data | `androidx.health.connect:connect-client:1.1.0-alpha10` |
| RevenueCat subscription purchase, restore, entitlement, and management URL handling | `com.revenuecat.purchases:purchases:8.10.4` |

## Design system

- **Primary color:** `#1B6E5C`
- **Background color:** `#F7FAF6`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `person-simple-walk` (regular weight)
- **Theme notes:** Use Material 3 light and dark themes. Light theme background is #F7FAF6, primary is #1B6E5C, error is #B3261E. Dark theme uses #9AD5C3 as primary, #101815 as background, and #F2B8B5 as error. Use rounded 16dp cards, 24dp screen horizontal padding, and high-contrast billing copy with price text never smaller than MaterialTheme.typography.titleLarge.

## Screens

### WelcomePricing
- **Route:** `welcome`
- **Purpose:** Introduce the app and show the honest $6.99/month subscription before any purchase attempt.
- **Reached via:** app launch when no active RevenueCat entitlement exists; tap Subscribe from TodayPlan; tap Manage subscription from Settings when not subscribed
- **Key UI elements:** Headline: "Walking workouts for $6.99/month"; Plain-language line: "No free trial. No teaser price. Cancel through Google Play anytime."; Three bullet cards for guided walks, step sync, and adaptive routine rotation; Required acknowledgement checkbox: "I understand this is $6.99 every month until cancelled."; Primary button: "Subscribe for $6.99/month"; Secondary button: "Restore purchase"; Text link: "Not now" to continue to limited preview
- **States:** loading_purchase_configuration, purchase_configuration_error, not_subscribed, purchase_pending, subscribed, restore_not_found

### OnboardingQuiz
- **Route:** `onboarding`
- **Purpose:** Collect walking experience and schedule preferences used to seed the first routine plan.
- **Reached via:** after successful subscription on WelcomePricing; tap Retake quiz from Settings; app launch when subscription is active but UserProfile does not exist
- **Key UI elements:** Step indicator showing 1 of 4 through 4 of 4; Goal selection chips: build habit, increase endurance, low-impact movement; Experience selection: beginner, regular walker, advanced; Days-per-week selector from 2 to 6; Preferred pace selector: gentle, moderate, brisk; Back and Continue buttons; Finish button that creates UserProfile and first seven RoutinePlan rows
- **States:** unstarted, in_progress, validation_error, saving, completed

### TodayPlan
- **Route:** `today`
- **Purpose:** Home dashboard showing today's planned workout, current step summary, and sync/subscription status.
- **Reached via:** app launch when onboarding is complete; bottom navigation tap Today; finish OnboardingQuiz; finish WorkoutPlayer feedback
- **Key UI elements:** Today step count card with source label; Health Connect connection status chip; Today's routine card with title, duration, intensity, and Start button; Adaptive note explaining why the workout was selected; Progress shortcut; Library shortcut; Settings icon; Subscription banner if entitlement is inactive
- **States:** loading, empty_no_profile, empty_no_routine_for_today, health_connect_unavailable, health_permission_denied, subscription_inactive, populated, error

### WorkoutLibrary
- **Route:** `library`
- **Purpose:** Browse guided walking workout videos and identify completed or repeated routines.
- **Reached via:** bottom navigation tap Library; tap Change workout from TodayPlan; tap Browse more after completing a workout
- **Key UI elements:** Search field for workout title; Filter chips: beginner, moderate, brisk, new, completed; LazyColumn of workout cards with title, category, duration, intensity, and completion count; Empty message when filters hide every workout; Button to clear filters
- **States:** loading, empty_catalog, empty_filtered, populated, error

### WorkoutPlayer
- **Route:** `workout/{workoutId}`
- **Purpose:** Play a selected guided walking workout and collect difficulty feedback at completion.
- **Reached via:** tap Start on TodayPlan routine card; tap a workout card in WorkoutLibrary
- **Key UI elements:** Video player surface with play, pause, seek, and full-screen controls; Workout title, duration, and intensity label; Complete workout button enabled after 80% playback progress; Difficulty feedback dialog with choices 1 very easy through 5 too hard; Save feedback button; Back button with confirmation if playback has started
- **States:** loading_workout, workout_not_found, playback_ready, playback_error, completion_unlocked, saving_feedback, completed_feedback_saved

### Progress
- **Route:** `progress`
- **Purpose:** Show recent step totals, completed workouts, and difficulty trends.
- **Reached via:** bottom navigation tap Progress; tap Progress shortcut from TodayPlan
- **Key UI elements:** Seven-day step summary list; Completed workouts count; Average difficulty card; Recent workout sessions list; Health Connect sync button
- **States:** loading, empty_no_steps_or_sessions, health_permission_denied, populated, error

### BillingManage
- **Route:** `billing`
- **Purpose:** Let the user view price, restore purchases, open cancellation, and start the Google Play refund flow without emailing support.
- **Reached via:** tap Manage subscription from Settings; tap billing banner from TodayPlan; tap Manage after purchase on WelcomePricing
- **Key UI elements:** Current plan card showing "$6.99/month"; Entitlement status label from RevenueCat CustomerInfo; Button: "Cancel in Google Play"; Button: "Request refund from Google Play"; Button: "Restore purchase"; Copy explaining that Google Play handles cancellation and refunds for Play Store purchases
- **States:** loading_customer_info, not_subscribed, subscribed, purchase_pending, restore_not_found, external_intent_unavailable, error

### Settings
- **Route:** `settings`
- **Purpose:** Configure Health Connect sync, retake onboarding, view billing, and open privacy information.
- **Reached via:** tap Settings icon from TodayPlan; bottom navigation tap Settings
- **Key UI elements:** Health Connect status row; Connect Health Connect button; Sync steps now button; Retake onboarding quiz row; Manage subscription row; Privacy policy row; Clear local history button with confirmation dialog
- **States:** loading, populated, health_connect_unavailable, health_permission_denied, clearing_data, error

## Data model

### UserProfile (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key; always 1 for the single local user |
| goal | `String` | one of build_habit, increase_endurance, low_impact_movement |
| experienceLevel | `String` | one of beginner, regular_walker, advanced |
| daysPerWeek | `Int` | validated range 2..6 |
| preferredPace | `String` | one of gentle, moderate, brisk |
| createdAtEpochMillis | `Long` |  |
| updatedAtEpochMillis | `Long` |  |

### Workout (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; stable seeded id such as beginner_walk_01 |
| title | `String` |  |
| category | `String` | walking category label such as indoor_walk, outdoor_walk, low_impact |
| durationMinutes | `Int` |  |
| intensityLevel | `Int` | validated range 1..5 |
| assetUri | `String` | android.resource:// URI string for the bundled MP4 in res/raw |
| description | `String` |  |
| programWeek | `Int` | used by routine generator to avoid showing all content immediately |
| isNewInCurrentCatalog | `Boolean` | true for workouts added in the latest seeded catalog version |

### WorkoutSession (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| workoutId | `String` | foreign key to Workout.id |
| startedAtEpochMillis | `Long` |  |
| completedAtEpochMillis | `Long` | nullable; null until completion is saved |
| difficultyRating | `Int` | nullable; 1 very easy through 5 too hard |
| completed | `Boolean` |  |
| stepsDuringSession | `Int` | nullable; computed from nearest Health Connect step summaries when available |

### DailyStepSummary (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| dateIso | `String` | primary key; yyyy-MM-dd in device local time |
| steps | `Int` | sum of Health Connect StepsRecord count for this date |
| source | `String` | HealthConnect |
| syncedAtEpochMillis | `Long` |  |

### RoutinePlan (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| dateIso | `String` | yyyy-MM-dd in device local time; indexed |
| workoutId | `String` | foreign key to Workout.id |
| targetIntensity | `Int` | validated range 1..5 |
| reason | `String` | short deterministic explanation shown on TodayPlan |
| completed | `Boolean` | set true when a WorkoutSession for this workout/date is completed |

## Features

### Honest disclosed subscription purchase

Show the $6.99/month subscription plainly before invoking the purchase sheet and require explicit acknowledgement.

- **Answers complaint:** Deceptive trial-to-charge billing A "free" or low one-off trial price silently converts into recurring charges of $18.99-$89.99+ — the dominant complaint by far.

- **Screens:** WelcomePricing, TodayPlan, BillingManage

- **Estimated hours:** 24

**Implementation notes:** Configure RevenueCat with one monthly entitlement named premium and one monthly product whose Play Console price is $6.99/month. On app start call Purchases.sharedInstance.getCustomerInfo() and, if premium is inactive, route to WelcomePricing. Load Offerings with Purchases.sharedInstance.getOfferings(); render only the monthly package. Do not render trial language anywhere. The Subscribe button remains disabled until the acknowledgement checkbox is checked. On click call Purchases.sharedInstance.purchaseWith(...) for the selected package. After success, verify CustomerInfo.entitlements["premium"]?.isActive == true before navigating to OnboardingQuiz or TodayPlan. If the Play Billing purchase is pending, show purchase_pending and do not unlock premium content until RevenueCat reports the entitlement active.

**Acceptance criteria:**
- WelcomePricing displays "$6.99/month" before any purchase button can be tapped.
- WelcomePricing contains no text containing "free trial", "intro price", "$9.99 trial", "$18.99", "$39.99", "$40", "$51", or "$89.99".
- Subscribe button is disabled when the acknowledgement checkbox is unchecked.
- A pending purchase does not unlock WorkoutPlayer for premium workouts.
- A restored active RevenueCat premium entitlement bypasses WelcomePricing on next launch.

### Self-serve cancellation and refund entry points

Provide in-app buttons that open Google Play subscription management and Google Play refund help without requiring email support.

- **Answers complaint:** Refund/cancellation friction Getting money back requires repeated emailing; several reviewers call it a scam even though support does eventually resolve most cases.

- **Screens:** BillingManage, Settings

- **Estimated hours:** 12

**Implementation notes:** In BillingManage, call Purchases.sharedInstance.getCustomerInfo() and display the premium entitlement state. For cancellation, first use CustomerInfo.managementURL when non-null and launch it with Intent.ACTION_VIEW. If managementURL is null, launch Uri.parse("https://play.google.com/store/account/subscriptions") with ACTION_VIEW. For refund requests, launch Uri.parse("https://support.google.com/googleplay/workflow/9813244") with ACTION_VIEW. Wrap each launch in a try/catch for ActivityNotFoundException and show external_intent_unavailable with the URL text selectable so the user can copy it. Do not implement email-only refund routing as the primary path.

**Acceptance criteria:**
- BillingManage has visible buttons labeled "Cancel in Google Play" and "Request refund from Google Play".
- Tapping "Cancel in Google Play" launches CustomerInfo.managementURL when RevenueCat returns a non-null URL.
- When CustomerInfo.managementURL is null, tapping "Cancel in Google Play" launches https://play.google.com/store/account/subscriptions.
- Tapping "Request refund from Google Play" launches https://support.google.com/googleplay/workflow/9813244.
- If no browser or Play Store handler exists, the screen shows the exact destination URL as selectable text instead of crashing.

### Onboarding personalization quiz

Collect goal, experience, weekly frequency, and preferred pace to create an initial walking routine.

- **Answers complaint:** baseline parity

- **Screens:** OnboardingQuiz, TodayPlan, Settings

- **Estimated hours:** 18

**Implementation notes:** Build a four-step Compose form backed by a ViewModel state data class. Validate that exactly one goal, experience level, and preferred pace are selected and that daysPerWeek is between 2 and 6. On Finish, upsert UserProfile id=1 in Room, then create seven RoutinePlan rows starting from LocalDate.now(). The seed intensity is 1 for beginner, 3 for regular_walker, and 4 for advanced; subtract 1 when preferredPace is gentle and add 1 when preferredPace is brisk, clamped to 1..5. For each scheduled day, choose the least-recently-used Workout whose intensityLevel is within +/-1 of targetIntensity and whose programWeek is not greater than the calendar week offset plus 1.

**Acceptance criteria:**
- Finish is disabled until all four quiz answers are valid.
- Saving the quiz creates exactly one UserProfile row with id 1.
- Saving the quiz creates RoutinePlan rows for the next seven local dates.
- A beginner plus gentle pace profile never creates an initial targetIntensity below 1.
- An advanced plus brisk pace profile never creates an initial targetIntensity above 5.

### Guided walking workout video library and player

Offer a browsable library of bundled guided walking workout videos with playback and completion tracking.

- **Answers complaint:** Content depth A smaller complaint: a promised program (e.g. Tai Chi) turns out thinner than advertised, or workouts repeat.

- **Screens:** WorkoutLibrary, WorkoutPlayer, TodayPlan, Progress

- **Estimated hours:** 72

**Implementation notes:** Seed the Room Workout table on first launch from a JSON asset named workout_catalog_v1.json. Each catalog item must point to a bundled MP4 resource in res/raw using an android.resource:// URI. Use Media3 ExoPlayer in WorkoutPlayer and embed PlayerView through AndroidView. Track playback progress with Player.Listener and currentPosition polling every second while playing. Enable the Complete workout button only after currentPosition / duration >= 0.80. On completion, insert a WorkoutSession with completed=true, completedAtEpochMillis=System.currentTimeMillis(), and the selected difficultyRating. In WorkoutLibrary, compute completion count with a Room query joining WorkoutSession by workoutId and show filters for beginner/moderate/brisk/new/completed. New catalog additions are handled by incrementing the asset JSON version and upserting workouts; do not delete existing WorkoutSession rows when the catalog changes.

**Acceptance criteria:**
- WorkoutLibrary shows seeded workouts after first app launch with no network connection.
- Tapping a workout opens WorkoutPlayer for the matching workoutId route argument.
- Complete workout button is disabled before 80% playback progress.
- Saving difficulty feedback inserts one completed WorkoutSession linked to the workoutId.
- A completed workout shows a completed count greater than zero in WorkoutLibrary.

### Health Connect step and wearable sync

Read daily step totals from Health Connect so phone and wearable-origin steps can appear in the app.

- **Answers complaint:** baseline parity

- **Screens:** TodayPlan, Progress, Settings

- **Estimated hours:** 54

**Implementation notes:** Use HealthConnectClient.getOrCreate(context). On Settings and TodayPlan, check HealthConnectClient.getSdkStatus(context). If SDK_UNAVAILABLE, show health_connect_unavailable. Request Health Connect read permission for StepsRecord using PermissionController.createRequestPermissionResultContract(). After permission is granted, read StepsRecord for each local-day TimeRangeFilter between startOfDay and endOfDay for the last seven dates using healthConnectClient.readRecords(ReadRecordsRequest(StepsRecord::class, timeRangeFilter)). Sum each StepsRecord.count regardless of metadata.dataOrigin so wearable-origin records are included when the wearable provider writes to Health Connect. Upsert DailyStepSummary(dateIso, steps, source="HealthConnect", syncedAtEpochMillis). Provide a Sync steps now button that repeats the seven-day sync and updates TodayPlan and Progress.

**Acceptance criteria:**
- When Health Connect SDK status is unavailable, TodayPlan shows health_connect_unavailable and does not crash.
- When StepsRecord read permission is denied, Settings shows health_permission_denied and a Connect Health Connect button.
- After a successful sync with two StepsRecord entries of 1000 and 2500 for the same local date, DailyStepSummary.steps for that date equals 3500.
- Progress displays seven local dates after sync, including dates with zero steps.
- Sync steps now updates the syncedAtEpochMillis value for each upserted DailyStepSummary.

### Adaptive routine rotation from difficulty feedback

Adjust upcoming walking workout intensity and avoid immediate repeats using deterministic rules.

- **Answers complaint:** Keep the routine library growing so a paying subscriber doesn't run out of new content after the first month.

- **Screens:** TodayPlan, WorkoutPlayer, WorkoutLibrary, Progress

- **Estimated hours:** 20

**Implementation notes:** After every completed WorkoutSession with difficultyRating, regenerate RoutinePlan rows for future dates only; never modify past or completed plans. Calculate recentAverageDifficulty from the last three completed sessions. If average >= 4.0, reduce targetIntensity by 1 for the next two planned workouts. If average <= 2.0, increase targetIntensity by 1 for the next two planned workouts. Otherwise keep the profile-derived target intensity. Clamp intensity to 1..5. Select workouts using this order: exclude workouts used in the last three completed sessions; prefer isNewInCurrentCatalog=true; then choose the least completed workout within +/-1 targetIntensity; if none exists, choose the least completed workout at any intensity. Store a human-readable reason such as "Easier pick because recent walks felt hard" in RoutinePlan.reason.

**Acceptance criteria:**
- A difficulty sequence of 4, 4, and 5 reduces the next generated targetIntensity by exactly 1, clamped at 1.
- A difficulty sequence of 1, 2, and 2 increases the next generated targetIntensity by exactly 1, clamped at 5.
- The generator does not choose any workoutId used in the last three completed sessions when another eligible workout exists.
- Past RoutinePlan rows are unchanged after regeneration.
- TodayPlan displays the stored RoutinePlan.reason for the selected workout.

## Store listing

- **Title:** StrideClear Walk Workouts
- **Short description:** Guided walking workouts with honest $6.99/month billing.
- **Category:** Health & Fitness
- **Keywords:** walking workout, guided walk, step tracker, Health Connect, walking plan, fitness routine, honest subscription
- **Icon prompt:** Create a clean Android app icon for a walking workout app named StrideClear. Use a rounded square background in deep teal #1B6E5C with a simple white line-art walking person glyph centered, inspired by a fitness route but without maps or medical symbols. Style should be modern Material Design, high contrast, no text, no gradients, no brand references, suitable at small launcher-icon sizes.

**Long description:**

StrideClear is a guided walking workout app built around clear billing and repeatable routines. Subscribe for $6.99/month with the price shown before purchase — no teaser trial, no silent jump to a higher renewal, and cancellation/refund links available inside the app.

Plan walks from a short onboarding quiz, play guided workout videos, sync daily steps through Health Connect, and give difficulty feedback after each session. StrideClear uses that feedback to rotate future routines, adjust intensity, and avoid repeating the same workout when other options are available.

Features:
• Clearly disclosed $6.99/month subscription
• No free trial that converts into a surprise charge
• In-app Google Play cancellation and refund entry points
• Guided walking workout video library
• Personalized routine plan from your quiz answers
• Health Connect step summaries, including wearable-origin steps when available
• Difficulty feedback that adjusts upcoming workout intensity

StrideClear is for general fitness and habit-building only. It does not provide medical advice, diagnosis, treatment, or guaranteed weight-loss results.

## Legal

- **Regulated category:** none
- **Privacy policy URL:** https://strideclear.example.com/privacy (privacy claims verified: no)
- **Data collected:** RevenueCat subscription entitlement and purchase status; local onboarding fitness preferences; local workout completion history and difficulty feedback; local Health Connect daily step summaries after permission

## Test plan

### 1. Deceptive trial-to-charge billing A "free" or low one-off trial price silently converts into recurring charges of $18.99-$89.99+ — the dominant complaint by far. (instrumented)

1. Launch the app with a fake RevenueCat CustomerInfo where premium entitlement is inactive.
2. Wait for WelcomePricing to render.
3. Assert that text "$6.99/month" is displayed.
4. Assert that no node contains text "free trial" ignoring case.
5. Assert that no node contains text "$39.99", "$40", "$51", or "$89.99".
6. Assert that the Subscribe button is disabled.
7. Tap the acknowledgement checkbox.
8. Assert that the Subscribe button is enabled.

**Expected:** The purchase path cannot start until the user sees and acknowledges the $6.99/month recurring price, and no trial-conversion language is present.

### 2. Price scale mismatch Some report renewal jumps to $40-51/month for a video-guided walking app — far above what reviewers say they'd pay. (unit)

1. Instantiate the billing display formatter with a mock monthly package price string of "$6.99/month".
2. Render the WelcomePricing state model.
3. Read the primary price label and acknowledgement copy from the state model.
4. Check both strings for "$6.99/month".
5. Check both strings do not contain "$40", "$51", "$89.99", or "price may change after trial".

**Expected:** The billing state model exposes only the disclosed $6.99/month price and no higher renewal or trial-conversion copy.

### 3. Refund/cancellation friction Getting money back requires repeated emailing; several reviewers call it a scam even though support does eventually resolve most cases. (instrumented)

1. Launch BillingManage with fake CustomerInfo containing premium active and managementURL set to https://play.google.com/store/account/subscriptions.
2. Assert that "Cancel in Google Play" is visible.
3. Assert that "Request refund from Google Play" is visible.
4. Tap "Cancel in Google Play".
5. Capture the outgoing ACTION_VIEW intent.
6. Verify the intent data is https://play.google.com/store/account/subscriptions.
7. Return to BillingManage and tap "Request refund from Google Play".
8. Capture the outgoing ACTION_VIEW intent.
9. Verify the intent data is https://support.google.com/googleplay/workflow/9813244.

**Expected:** Both cancellation and refund actions are available from inside the app and open self-serve Google Play destinations without any email step.

### 4. Content depth A smaller complaint: a promised program (e.g. Tai Chi) turns out thinner than advertised, or workouts repeat. (unit)

1. Create six Workout rows with intensityLevel values from 1 through 5 and distinct workoutId values.
2. Create three completed WorkoutSession rows for workoutIds A, B, and C with difficulty ratings 3, 3, and 3.
3. Run the routine generator for the next planned date with targetIntensity 3.
4. Inspect the selected workoutId.
5. Inspect the stored RoutinePlan.reason.

**Expected:** The selected workoutId is not A, B, or C when another eligible workout exists, and RoutinePlan.reason is a non-empty explanation shown to the user.

### 5. Keep the routine library growing so a paying subscriber doesn't run out of new content after the first month. (unit)

1. Create Workout rows where one eligible workout has isNewInCurrentCatalog=true and another eligible workout has isNewInCurrentCatalog=false.
2. Create a UserProfile with regular_walker and moderate pace.
3. Create three completed WorkoutSession rows with difficulty ratings 4, 4, and 5.
4. Run the adaptive routine generator for future dates.
5. Read the first future RoutinePlan.

**Expected:** The first future RoutinePlan targetIntensity is one lower than the profile-derived intensity, clamped to 1..5, and the generator prefers the new catalog workout when it is otherwise eligible.

### 6. baseline parity (instrumented)

1. Launch OnboardingQuiz with an active premium entitlement and no UserProfile row.
2. Select goal "build habit".
3. Select experience "beginner".
4. Select days per week value 3.
5. Select preferred pace "gentle".
6. Tap Finish.
7. Query Room for UserProfile id 1.
8. Query Room for RoutinePlan rows with dateIso from today through the next six local dates.

**Expected:** One UserProfile row is saved and seven RoutinePlan rows are created with targetIntensity values between 1 and 5.

### 7. baseline parity (instrumented)

1. Seed Room with one Workout whose assetUri points to a bundled res/raw MP4.
2. Open route workout/{workoutId} for that workout.
3. Wait for playback_ready.
4. Assert that Complete workout is disabled at playback position 0.
5. Advance the fake/player test clock or mock player position to 80 percent of duration.
6. Assert that Complete workout is enabled.
7. Tap Complete workout.
8. Select difficulty rating 4.
9. Tap Save feedback.
10. Query Room for WorkoutSession rows for that workoutId.

**Expected:** Exactly one completed WorkoutSession is stored with difficultyRating 4 and completed=true.

### 8. baseline parity (manual)

1. Install the app on an Android device with Health Connect installed.
2. In Settings, tap Connect Health Connect.
3. Grant read permission for steps.
4. Open Health Connect or a connected step source and ensure today's steps are greater than zero.
5. Return to the app and tap Sync steps now.
6. Open TodayPlan and Progress.

**Expected:** TodayPlan shows today's non-zero step total, and Progress shows a seven-day list with HealthConnect as the source.

## Build instructions

```sh
keytool -genkeypair -v -keystore release.keystore -alias release -keyalg RSA -keysize 2048 -validity 10000 -storepass changeit -keypass changeit -dname "CN=StrideClear,O=SoloBuilder,C=US"
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
./gradlew bundleRelease -Pandroid.injected.signing.store.file=$PWD/release.keystore -Pandroid.injected.signing.store.password=changeit -Pandroid.injected.signing.key.alias=release -Pandroid.injected.signing.key.password=changeit
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
