# ClearRead Kids Tutor — build spec

A real conversational reading tutor for children with plainly visible $6.99/month subscription status and a direct in-app path to cancel.

## 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:** Buddy.ai: Kids Learning Games
- **Package id:** `ai.mybuddy.talkingflashcards_new`
- **Google Play:** https://play.google.com/store/apps/details?id=ai.mybuddy.talkingflashcards_new
- **appy.fyi report:** https://appy.fyi/report/ai.mybuddy.talkingflashcards_new
- **Category:** Education

## Overview

- **Working name:** ClearRead Kids Tutor (trademark cleared: no)
- **Package id:** `fyi.appy.clearreadkidstutor`
- **Min / target SDK:** 26 / 35
- **Backend:** none
- **Estimated build time:** 8 weeks
- **Pricing:** subscription, $6.99 via `revenuecat`
- **Runtime AI:** yes (ai_gateway_llm): Generate the next adaptive tutor reply from lesson context, child transcript, speech score, and recent conversation turns. ≈ $0.003/call
- **Permissions:** `INTERNET`, `RECORD_AUDIO`

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

- Do not copy Buddy.ai branding, characters, voices, lesson art, package names, or store-listing language.
- Do not claim automatic in-app cancellation outside Google Play; the app opens the exact Play subscription-management flow and then reflects cancellation status after Play confirms it.
- Do not offer a hidden or misleading free trial in v1; v1 uses one monthly subscription price shown before purchase.
- Do not build parent social features, teacher dashboards, classroom management, or cloud progress sync in v1.
- Do not make medical, developmental, dyslexia-treatment, or educational-outcome guarantee claims.

## Tech stack

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

| Purpose | Gradle coordinate |
| --- | --- |
| Compose activity host | `androidx.activity:activity-compose:1.9.3` |
| Compose Navigation graph | `androidx.navigation:navigation-compose:2.8.3` |
| Lifecycle-aware Compose ViewModels | `androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6` |
| Lifecycle runtime for coroutine collection in Compose | `androidx.lifecycle:lifecycle-runtime-compose:2.8.6` |
| Material 3 Compose components | `androidx.compose.material3:material3:1.3.0` |
| Room local database runtime for lessons, progress, tutor turns, and subscription snapshots | `androidx.room:room-runtime:2.6.1` |
| Room Kotlin coroutine extensions | `androidx.room:room-ktx:2.6.1` |
| Room annotation processor | `androidx.room:room-compiler:2.6.1` |
| KSP Gradle plugin for Room code generation | `com.google.devtools.ksp:symbol-processing-gradle-plugin:2.0.21-1.0.25` |
| RevenueCat subscription purchase and entitlement status | `com.revenuecat.purchases:purchases:8.10.0` |
| HTTPS client for AI gateway calls | `com.squareup.okhttp3:okhttp:4.12.0` |
| JSON encoding and decoding for AI gateway requests and responses | `org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3` |
| Android coroutine dispatchers | `org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0` |
| Unit test assertions | `junit:junit:4.13.2` |
| AndroidX instrumented test runner assertions | `androidx.test.ext:junit:1.2.1` |
| Compose UI instrumented tests | `androidx.compose.ui:ui-test-junit4:1.7.4` |
| Mock AI gateway server for tests | `com.squareup.okhttp3:mockwebserver:4.12.0` |

## Design system

- **Primary color:** `#2F6FDD`
- **Background color:** `#FFF8E7`
- **Error color:** `#B3261E`
- **Typography:** Material 3 default type scale, no custom font
- **Launcher icon glyph:** Phosphor `book-open-user` (regular weight)
- **Theme notes:** Use a warm light theme by default with large rounded cards, 24dp corner radius on child-facing lesson cards, and high-contrast dark theme generated from the same primary color for system dark mode. Parent/account screens use denser Material 3 lists; child screens use large 56dp minimum tap targets and simple Compose scale/fade animations only.

## Screens

### Launch
- **Route:** `launch`
- **Purpose:** Decide the first destination after checking whether local onboarding exists and whether the lesson database has been seeded.
- **Reached via:** app launch
- **Key UI elements:** Centered app logo glyph; Loading text: Preparing your tutor; Retry button when database initialization fails
- **States:** loading, error

### Parent Onboarding
- **Route:** `onboarding`
- **Purpose:** Let a parent create the local parent/child profile and show the app's subscription promise before the child uses lessons.
- **Reached via:** Launch when no ParentProfile exists
- **Key UI elements:** Parent-facing explanation card stating $6.99/month subscription is optional and visible in Account; Child nickname text field; Reading level selector with values 1, 2, and 3; Continue button; Privacy note explaining microphone and AI tutor use
- **States:** empty, validation_error, saving

### Kid Home
- **Route:** `home`
- **Purpose:** Child-friendly hub for starting lessons while keeping the parent account entry visible.
- **Reached via:** Launch after onboarding; back navigation from Lesson List; back navigation from Progress; back navigation from Account Subscription
- **Key UI elements:** Large Start Reading button; Lessons button; Progress button; Always-visible Parent Account button in the top-right labeled Parent Account; Premium status chip showing Active, Cancelled, Expired, or Free
- **States:** loading, empty, error, populated

### Parent Gate
- **Route:** `parent-gate/{destination}`
- **Purpose:** Block child access to subscription and account actions with a simple parent-only arithmetic gate.
- **Reached via:** tap Parent Account on Kid Home; tap Subscribe from Paywall when child-facing context is active
- **Key UI elements:** Prompt: Parent check; Arithmetic question text, for example 7 + 5 = ?; Numeric answer field; Continue button; Cancel button; Inline error text for wrong answer
- **States:** empty, validation_error

### Account Subscription
- **Route:** `account`
- **Purpose:** Show exactly what subscription is active and provide a direct cancel/manage action.
- **Reached via:** Parent Gate destination account; tap Account from Paywall; return from Google Play subscription-management intent
- **Key UI elements:** Subscription status card; Current product row showing clearread_monthly_699; Price row showing $6.99/month; Renewal or access-until date row; Will renew yes/no row; Cancel subscription button when active; Manage in Google Play button; Restore purchases button; Back to child home button
- **States:** loading, not_subscribed, active_renews, active_cancelled_access_until_date, expired, error

### Paywall
- **Route:** `paywall`
- **Purpose:** Sell the single monthly subscription with clear price, renewal, and cancellation language.
- **Reached via:** tap locked lesson from Lesson List; tap Subscribe from Account Subscription when expired or not subscribed
- **Key UI elements:** Headline: Full tutor access; Price text: $6.99/month; Bullets: real conversation, reading practice, visible cancel button; Subscribe button; Not now button; Account and cancellation link; Purchase error banner
- **States:** loading_offerings, ready, purchasing, purchase_error, already_active

### Lesson List
- **Route:** `lessons`
- **Purpose:** Show seeded reading lessons, lock state, and completion state.
- **Reached via:** tap Lessons on Kid Home; tap Start Reading on Kid Home
- **Key UI elements:** LazyColumn of lesson cards; Lesson title; Target words preview; Completion checkmark; Locked badge for premium-only lessons; Back button
- **States:** loading, empty, error, populated

### Lesson Tutor
- **Route:** `lesson/{lessonId}`
- **Purpose:** Run one adaptive reading lesson with microphone capture, speech scoring, and LLM-backed tutor replies.
- **Reached via:** tap unlocked lesson card on Lesson List; tap Start Reading on Kid Home when next lesson exists
- **Key UI elements:** Tutor message bubble; Child transcript bubble; Current word or sentence card; Hold to Talk microphone button; Pronunciation score ring; Try Again button; Next button; Finish Lesson button; AI/network error retry banner
- **States:** loading_lesson, ready, listening, scoring, waiting_for_ai, speech_error, ai_error, completed

### Progress
- **Route:** `progress`
- **Purpose:** Show parent-readable and child-readable lesson completion without any cloud dashboard.
- **Reached via:** tap Progress on Kid Home; tap View Progress after completing a lesson
- **Key UI elements:** Completed lessons count; Average score card; Recent lesson list; No progress yet empty illustration; Back button
- **States:** loading, empty, error, populated

## Data model

### ParentProfile (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; use constant value local_parent |
| displayEmail | `String?` | nullable; optional parent-entered label only, not used for login |
| createdAtMillis | `Long` | Unix epoch milliseconds |
| revenueCatAppUserId | `String` | anonymous RevenueCat app user id generated on first launch and persisted |

### ChildProfile (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; UUID string |
| nickname | `String` | parent-entered child display name |
| readingLevel | `Int` | allowed values 1, 2, or 3 |
| createdAtMillis | `Long` | Unix epoch milliseconds |
| active | `Boolean` | true for the single v1 child profile |

### Lesson (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; stable seed id such as level1_lesson01 |
| sequence | `Int` | sort order starting at 1 |
| readingLevel | `Int` | allowed values 1, 2, or 3 |
| title | `String` | child-facing lesson title |
| targetGraphemes | `String` | comma-separated letter sounds or phonics focus |
| introPrompt | `String` | opening tutor line for this lesson |
| premiumRequired | `Boolean` | false for first two lessons, true for later lessons |

### Exercise (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; stable seed id |
| lessonId | `String` | foreign key to Lesson.id |
| sequence | `Int` | sort order inside lesson starting at 1 |
| type | `String` | one of repeat_word, read_sentence, conversation_prompt |
| promptText | `String` | text shown to the child |
| expectedAnswer | `String` | word or sentence used for speech scoring |
| passingScorePercent | `Int` | minimum score to auto-advance; default 70 |

### LessonProgress (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; childId + ':' + lessonId |
| childId | `String` | foreign key to ChildProfile.id |
| lessonId | `String` | foreign key to Lesson.id |
| completedExerciseCount | `Int` | number of exercises completed transactionally |
| totalExerciseCount | `Int` | copied from seeded exercise count for stable progress display |
| bestScorePercent | `Int` | highest average lesson score, 0 to 100 |
| status | `String` | one of not_started, in_progress, completed |
| updatedAtMillis | `Long` | Unix epoch milliseconds |

### TutorTurn (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `Long` | primary key, autogenerate |
| childId | `String` | foreign key to ChildProfile.id |
| lessonId | `String` | foreign key to Lesson.id |
| role | `String` | one of system, child, tutor |
| text | `String` | spoken transcript or tutor reply |
| pronunciationScorePercent | `Int?` | nullable; only set for child speech turns |
| createdAtMillis | `Long` | Unix epoch milliseconds |

### SubscriptionSnapshot (`room_local`)

| Field | Type | Notes |
| --- | --- | --- |
| id | `String` | primary key; use constant value current |
| entitlementId | `String` | RevenueCat entitlement id premium_tutor |
| productId | `String` | Google Play product id clearread_monthly_699 |
| status | `String` | one of none, active, active_cancelled, expired, error |
| expirationAtMillis | `Long?` | nullable when there is no known entitlement expiration |
| willRenew | `Boolean` | false after Play/RevenueCat reports cancellation |
| managementUrl | `String?` | nullable; RevenueCat customerInfo.managementURL or Play subscriptions URL fallback |
| updatedAtMillis | `Long` | Unix epoch milliseconds |

## Features

### Visible parent account and subscription screen

Keep a clearly labeled Parent Account entry visible from the child home screen and show current subscription status in plain language.

- **Answers complaint:** No discoverable account/settings screen

- **Screens:** Kid Home, Parent Gate, Account Subscription

- **Estimated hours:** 36

**Implementation notes:** On Kid Home, place a Material3 TextButton with text and contentDescription exactly 'Parent Account' in the top-right app bar. Tapping it navigates to parent-gate/account. Parent Gate generates a deterministic arithmetic challenge from the current minute, accepts only the numeric result, and then navigates to Account Subscription. Account Subscription loads SubscriptionSnapshot from Room immediately, then calls Purchases.sharedInstance.getCustomerInfo; map entitlement premium_tutor to status active if active and willRenew true, active_cancelled if active and willRenew false, expired if absent with prior expiration in the past, otherwise none. Render rows for productId clearread_monthly_699, price '$6.99/month', status, willRenew, and expiration date formatted as local yyyy-MM-dd.

**Acceptance criteria:**
- Kid Home always shows a tappable control labeled Parent Account without opening any overflow menu.
- A correct Parent Gate answer routes to Account Subscription; a wrong answer stays on Parent Gate and shows inline error text.
- Account Subscription shows one of Not subscribed, Active and renews, Cancelled with access until date, Expired, or Error.
- The subscription screen shows $6.99/month and product id clearread_monthly_699 when RevenueCat offerings load.

### Honest monthly subscription purchase, restore, and cancel path

Sell one $6.99/month subscription and provide a direct Google Play cancellation/manage action from inside the app.

- **Answers complaint:** Billing continues after cancellation

- **Screens:** Paywall, Account Subscription, Kid Home

- **Estimated hours:** 24

**Implementation notes:** Configure RevenueCat with entitlement premium_tutor and Google Play subscription product clearread_monthly_699. Paywall fetches current Offering and selects the monthly Package whose store product id is clearread_monthly_699; the purchase button calls Purchases.sharedInstance.purchasePackage from the current Activity and disables itself while purchasing. Restore purchases calls Purchases.sharedInstance.restorePurchases. The Cancel subscription button first opens customerInfo.managementURL when non-null; otherwise open Intent.ACTION_VIEW with URI 'https://play.google.com/store/account/subscriptions?sku=clearread_monthly_699&package=' + BuildConfig.APPLICATION_ID. On Activity onResume after returning from Play, call getCustomerInfo again and update SubscriptionSnapshot. The app must not create an app-managed trial timer or charge cards directly; Google Play and RevenueCat are the only billing source of truth.

**Acceptance criteria:**
- Paywall displays exactly '$6.99/month' before purchase.
- Tapping Subscribe starts a RevenueCat purchase for product clearread_monthly_699.
- Tapping Cancel subscription opens Google Play subscription management for this app and SKU, or the RevenueCat management URL when supplied.
- After Play reports cancellation, Account Subscription displays Cancelled with access until date and willRenew false.
- The app never hides the Account Subscription screen behind an unlabelled icon or overflow menu.

### Real LLM-backed conversational tutor

Generate tutor replies from the child's transcript, lesson target, score, and recent conversation instead of playing fixed recordings.

- **Answers complaint:** Overstated "AI"

- **Screens:** Lesson Tutor

- **Estimated hours:** 72

**Implementation notes:** Create TutorGateway with OkHttp POST to BuildConfig.AI_GATEWAY_URL over HTTPS. Send JSON with model class 'small-low-latency', a system message: 'You are a kind reading tutor for children age 5-8. Keep replies under 35 words. Ask one reading-focused follow-up. Do not mention billing.', the active lesson title, targetGraphemes, current Exercise.promptText, expectedAnswer, child's transcript, pronunciation score, and the last six TutorTurn rows for this child and lesson. Use kotlinx.serialization data classes for request/response. On a successful response, insert a TutorTurn role=tutor and render the returned text. On timeout or non-2xx, show the AI error banner with Retry and do not advance the exercise. Never choose a tutor reply from a prewritten list except for the initial seeded lesson intro before any child utterance.

**Acceptance criteria:**
- After a child transcript is produced, the app sends that exact transcript and the active lesson id to the AI gateway.
- The displayed tutor reply equals the AI gateway response body text, not a hardcoded local phrase.
- If the AI gateway returns HTTP 500, Lesson Tutor remains on the same exercise and shows Retry.
- Tutor replies are persisted as TutorTurn rows with role tutor.

### Speech capture and basic reading score

Let the child speak a word or sentence and score the transcript against the expected reading text.

- **Answers complaint:** Overstated "AI"

- **Screens:** Lesson Tutor

- **Estimated hours:** 48

**Implementation notes:** Request RECORD_AUDIO at the first Hold to Talk action. Use Android SpeechRecognizer with RecognizerIntent.ACTION_RECOGNIZE_SPEECH, EXTRA_LANGUAGE_MODEL=LANGUAGE_MODEL_FREE_FORM, EXTRA_LANGUAGE='en-US', and EXTRA_PARTIAL_RESULTS=true. Normalize both expectedAnswer and transcript by lowercasing Locale.US, removing punctuation with regex '[^a-z0-9 ]', collapsing whitespace, and trimming. Compute word-level Levenshtein distance between expected word tokens and transcript tokens. Score is max(0, round(100 * (1 - distance / max(expectedTokenCount, transcriptTokenCount)))). For single-word exercises, also accept a transcript containing the expected word as 100. Insert a child TutorTurn with transcript and score. If score >= Exercise.passingScorePercent, enable Next; otherwise show Try Again and pass the transcript/score to the LLM so the tutor can respond adaptively.

**Acceptance criteria:**
- The first microphone use shows the Android runtime microphone permission prompt.
- For expected 'cat sat' and transcript 'cat sat', score is 100.
- For expected 'cat sat' and transcript 'cat', score is 50.
- A score below passingScorePercent keeps the exercise on screen and shows Try Again.
- A SpeechRecognizer error shows speech_error state with a Retry microphone action.

### Seeded reading curriculum

Provide a small local reading curriculum so the tutor has concrete lessons and exercises to run.

- **Answers complaint:** baseline parity

- **Screens:** Parent Onboarding, Kid Home, Lesson List, Lesson Tutor

- **Estimated hours:** 80

**Implementation notes:** On first database open, seed Room in a transaction with 12 lessons: 4 for readingLevel 1, 4 for readingLevel 2, and 4 for readingLevel 3. Each lesson has 5 exercises: two repeat_word, two read_sentence, and one conversation_prompt. The first two lessons overall have premiumRequired=false; lessons 3 through 12 have premiumRequired=true. Use stable ids level1_lesson01 through level3_lesson04 and exercise ids lessonId_ex01 through lessonId_ex05. Lesson List queries lessons ordered by readingLevel then sequence and filters the Start Reading recommendation to the selected child readingLevel first incomplete lesson.

**Acceptance criteria:**
- A fresh install creates exactly 12 Lesson rows and exactly 60 Exercise rows.
- The first two lessons are accessible without an active premium_tutor entitlement.
- A premium lesson tap without entitlement routes to Paywall.
- Lesson Tutor can load all five exercises for every seeded lesson id.

### Reliable lesson completion and unlock state

Use a deterministic progress state machine so completed levels do not get stuck or lose progress.

- **Answers complaint:** Glitches blocking progress

- **Screens:** Lesson Tutor, Lesson List, Progress, Kid Home

- **Estimated hours:** 28

**Implementation notes:** Represent progress with LessonProgress.status values not_started, in_progress, and completed. When Lesson Tutor opens, create LessonProgress in a Room transaction if missing with completedExerciseCount=0 and totalExerciseCount equal to the exercise query count. On Next after a passing score, update completedExerciseCount to max(existing, currentExerciseSequence). When completedExerciseCount reaches totalExerciseCount, set status=completed, bestScorePercent to the rounded average of child turn scores for the lesson, and updatedAtMillis. Never derive completion only from in-memory Compose state. After completion, the next lesson is unlocked if it is non-premium or premiumRequired is true and SubscriptionSnapshot status is active or active_cancelled with expirationAtMillis in the future.

**Acceptance criteria:**
- Closing and reopening the app after completing exercise 3 of 5 resumes at exercise 4.
- Completing exercise 5 of 5 writes status completed to Room.
- A completed lesson displays a checkmark in Lesson List after app restart.
- Progress screen average score uses persisted child TutorTurn scores, not transient UI state.

### Kid-friendly reading UI and simple animation

Make the child-facing lesson flow visually friendly and easy to operate.

- **Answers complaint:** baseline parity

- **Screens:** Kid Home, Lesson List, Lesson Tutor, Progress

- **Estimated hours:** 32

**Implementation notes:** Use Compose Material3 cards with 24dp rounded corners, 20dp padding, primary color #2F6FDD for action buttons, and background #FFF8E7. Lesson cards use animateFloatAsState to scale from 1.0 to 1.04 while pressed. Tutor message changes use AnimatedContent with fadeIn/fadeOut. The Hold to Talk button is at least 96dp square with microphone text label for accessibility. Avoid custom character art in v1; use geometric shapes, large text, and the book-open-user launcher glyph style for consistency.

**Acceptance criteria:**
- All child-facing primary tap targets are at least 56dp in both dimensions.
- Hold to Talk is at least 96dp square and has a contentDescription.
- Lesson Tutor message changes animate with fade rather than replacing abruptly.
- The app remains usable in Android system dark mode with readable text contrast.

## Store listing

- **Title:** ClearRead Kids Tutor
- **Short description:** A real AI reading tutor with clear $6.99/month billing.
- **Category:** Education
- **Keywords:** kids reading tutor, AI reading practice, phonics, speech practice, children education, reading lessons, subscription transparency
- **Icon prompt:** Create a square Android app icon for a children’s reading tutor: warm cream background, rounded blue book open in the center, simple friendly child silhouette above the pages, clean vector style, no letters, no brand names, high contrast, suitable for adaptive icon foreground.

**Long description:**

ClearRead Kids Tutor helps children practice early reading with spoken lessons, simple pronunciation scoring, and real conversational tutor replies. Parents always have a visible Parent Account screen showing whether the subscription is active, cancelled, expired, or not subscribed. The app uses one clearly shown price: $6.99/month. Cancellation and management open directly from the app through Google Play subscription management.

## Legal

- **Regulated category:** kids_coppa
- **Privacy policy URL:** https://clearread.example.com/privacy (privacy claims verified: no)
- **Data collected:** Child nickname stored locally on device; Child reading level stored locally on device; Lesson progress and pronunciation scores stored locally on device; Child spoken transcripts sent to the AI gateway for tutor replies; Recent tutor conversation text sent to the AI gateway for context; Microphone audio processed by Android speech recognition during speaking exercises; Subscription product id, entitlement status, expiration date, and purchase identifiers processed by RevenueCat

## Test plan

### 1. No discoverable account/settings screen (instrumented)

1. Install a fresh debug build and complete Parent Onboarding with child nickname 'Sam' and reading level 1.
2. Wait for Kid Home to render.
3. Find a node with text 'Parent Account' and assert it is displayed.
4. Tap 'Parent Account'.
5. On Parent Gate, enter the correct arithmetic answer shown on screen.
6. Tap Continue.

**Expected:** Account Subscription screen is displayed and contains text 'Subscription status' and '$6.99/month'.

### 2. Billing continues after cancellation (manual)

1. Use a Google Play license-test account with product clearread_monthly_699 configured in Play Console and RevenueCat entitlement premium_tutor.
2. Open Paywall and purchase the $6.99/month test subscription.
3. Open Parent Account, verify status is Active and renews.
4. Tap Cancel subscription.
5. In the Google Play subscription-management screen, cancel the test subscription.
6. Return to the app.

**Expected:** Account Subscription refreshes after resume and shows Cancelled with access until date and willRenew false; it does not show Active and renews.

### 3. Parents cannot find where the subscription lives in order to cancel it (instrumented)

1. Seed SubscriptionSnapshot with status active, productId clearread_monthly_699, willRenew true, and a non-null managementUrl.
2. Launch Kid Home.
3. Tap Parent Account, pass Parent Gate, and wait for Account Subscription.
4. Tap Cancel subscription.
5. Capture the outgoing intent using an intent test rule.

**Expected:** The outgoing ACTION_VIEW intent URI equals the stored managementUrl, or starts with https://play.google.com/store/account/subscriptions and contains sku=clearread_monthly_699.

### 4. Overstated "AI" (unit)

1. Create a fake TutorGateway request builder with lesson id level1_lesson01, expectedAnswer 'red hen', transcript 'I saw a red dragon', score 40, and two previous TutorTurn rows.
2. Build the JSON request.
3. Inspect the serialized messages array.

**Expected:** The JSON request contains the exact child transcript 'I saw a red dragon', the expected answer 'red hen', the numeric score 40, and the previous turn texts; no local prerecorded response is selected.

### 5. Overstated "AI" (instrumented)

1. Start MockWebServer and configure BuildConfig.AI_GATEWAY_URL for the debug test to its URL.
2. Enqueue an HTTP 200 JSON response with tutor text 'Great try! Let’s read red hen together.'
3. Launch Lesson Tutor for level1_lesson01.
4. Inject a fake speech result transcript 'red hen' with score 100.
5. Wait for AI response rendering.

**Expected:** Lesson Tutor displays exactly 'Great try! Let’s read red hen together.' and a TutorTurn with role tutor is inserted in Room.

### 6. Speech scoring baseline parity (unit)

1. Call the speech scoring function with expectedAnswer 'cat sat' and transcript 'cat sat'.
2. Call it again with expectedAnswer 'cat sat' and transcript 'cat'.
3. Call it again with expectedAnswer 'cat' and transcript 'the cat is here'.

**Expected:** The three scores are 100, 50, and 100 respectively.

### 7. Glitches blocking progress (instrumented)

1. Seed the database and open Lesson Tutor for level1_lesson01.
2. For each of the five exercises, inject a passing fake transcript and tap Next until Finish Lesson appears.
3. Tap Finish Lesson.
4. Force-stop and relaunch the app.
5. Open Lesson List and Progress.

**Expected:** Lesson List shows level1_lesson01 with a completion checkmark, Progress shows one completed lesson, and the app does not reopen the completed lesson at exercise 1.

### 8. Glitches blocking progress (unit)

1. Create LessonProgress with completedExerciseCount 3 and totalExerciseCount 5.
2. Call the progress update method for currentExerciseSequence 2.
3. Call it again for currentExerciseSequence 4.
4. Call it again for currentExerciseSequence 5.

**Expected:** The stored completedExerciseCount never decreases, becomes 4 after the second update, and status becomes completed only after sequence 5.

### 9. baseline parity (manual)

1. Install the app on a physical Android device with no microphone permission granted.
2. Complete onboarding and open the first free lesson.
3. Press Hold to Talk.
4. Grant microphone permission.
5. Say the expected word shown on screen.

**Expected:** The app enters listening state, displays the recognized transcript, shows a pronunciation score, and enables Next when the score meets the exercise passing threshold.

## Build instructions

```sh
set -e
./gradlew clean
./gradlew testDebugUnitTest
./gradlew connectedDebugAndroidTest
mkdir -p keystore
if [ ! -f keystore/release.jks ]; then keytool -genkeypair -v -keystore keystore/release.jks -storepass changeit123 -keypass changeit123 -alias release -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=ClearRead Kids Tutor,O=Solo Builder,C=US"; fi
export RELEASE_STORE_FILE=$PWD/keystore/release.jks
export RELEASE_STORE_PASSWORD=changeit123
export RELEASE_KEY_ALIAS=release
export RELEASE_KEY_PASSWORD=changeit123
./gradlew bundleRelease
```

## Human gates still required

- `trademark_and_privacy_review`
- `closed_testing_recruitment`
- `regulated_category_go_no_go`
