Decisions

ADR-001 — Product decisions for v1 (binding)

Every open decision in the PRD, resolved: training path, scoring, rewards, accounts, paywall, languages.

Status: Accepted 2026-10-17. Owner: TPM. Supersedes the "待决策" list in PRD v1.0 §11. Anything here can be changed by a PR that updates this file and the affected contract/design.

D1. Training path = 3 steps

阅读 (read) → 复述 (first retell) → 压缩重讲 (compressed retell) → 完成 (settlement). "观点迁移" (transfer) is not in v1 and is not shown anywhere in the UI. Progress indicators are 1/3, 2/3, 3/3.

D2. Recording limits and validity

Rule Round 1 Round 2
Max duration 180 s, auto-stop then confirm sheet 120 s, auto-stop then confirm sheet
Client floor 20 s of unpaused recording and level activity above noise floor same
Server validity transcript length ≥ the per-language floor (zh-Hans: 40 characters; en: 30 words) and speech ratio ≥ 0.35 and detected language = the article's language same
Outcome if invalid attempt.status = "unreliable" with reason (too_short, no_speech, language_mismatch, low_audio_quality, transcript_incomplete). No score, no XP. User may re-record for free; the session stays in the same stage. same

Pause is allowed; paused time does not count. Backgrounding, incoming call, or route change pauses and persists the partial recording to disk; on return the user chooses 继续 / 重录. Leaving the screen stops capture and the timer.

D3. Scoring

Four dimensions, each 0–100: key_info 关键信息, structure 逻辑结构, efficiency 表达效率, fluency 流畅度.

  • overall = round(0.35*key_info + 0.30*structure + 0.20*efficiency + 0.15*fluency)
  • Bands: < 50 需要再练, 50–69 基础不错, 70–84 讲清楚了, ≥ 85 出色.
  • Every assessment carries rubric_version (start rubric-v1), model_ref (provider/model), confidence (high|medium|low). low confidence is surfaced as "这次评估不太确定" with the reason, and the one-action advice is still given if available; unreliable (D2) is a different, non-scored outcome.
  • Evidence: each dimension note and the one-action must reference ≥ 1 evidence item {start_ms, end_ms, transcript_excerpt} that falls inside the attempt duration (server validates bounds). Dimension notes with no evidence are rendered as "依据不足" and do not lower the score.
  • Rubric prompts are language-aware: the assessor is instructed in the article's language and judges idiom, structure and fillers against that language's norms (e.g. 嗯/那个 vs um/like). Scores are comparable across languages only within the same rubric version.
  • Fluency is judged from audio timing (pause map from ASR word timestamps), never from transcript punctuation alone. Accent, stutter, and disability-related speech patterns are explicitly out of scope of fluency; the rubric penalises only pauses/breaks that affect comprehension.
  • The assessor is given the article's key_points (hidden from the client before assessment), and scores coverage by meaning, not by string match.

Long-term ability score (能力分): per dimension and overall, ability = EMA(alpha=0.3) over the per-session best attempt score, computed server-side at settlement with algo_version = "ability-v1". Shown only when sample_count >= 3; before that the UI shows "建立基线中 n/3". Session delta (+18 in the prototype) is not the ability delta; the completion screen shows the round-to-round delta and, separately, the ability change if any.

D4. Rewards, streaks, levels

Reward XP Idempotency key
Round 1 assessed (not unreliable) 15 {session_id}:round1
Round 2 assessed 20 {session_id}:round2
Session completed 10 {session_id}:complete
Improvement bonus (round 2 overall > round 1 overall, or same overall with shorter duration and key_info not lower) 10 {session_id}:improve
  • Weekly quests award XP on completion: 复述 5 次 30, 压缩 3 次 30, 做到建议 3 次 40. Weekly-quest XP does not count toward the daily cap (it is a weekly reward); it is ledgered with type weekly_quest and idempotency key {user}:{week_start}:{quest_id}.
  • Daily XP cap 165 (user's time zone) applies to session XP (round1, round2, complete, improve). Repeating an article completed in the last 7 days earns 50 % XP, rounded down. Diagnostic session earns XP once.
  • Streak: +1 on the first completed session of a local day. Multiple sessions per day do not add days. One streak freeze is granted at each 7-day milestone (max 2 banked); a missed day consumes a freeze automatically. Time-zone changes cannot create a second "first session" on the same UTC day: streak day keys are stored as the local date at completion; when the user's time zone differs from the one at the previous increment, a new increment is refused if less than 20 h have passed (same-zone consecutive local days always count, so an honest 23:00 → 08:00 pair is fine).
  • Levels (cumulative XP): L1 0, L2 100, L3 250, L4 450, L5 700, L6 1000, L7 1350, L8 1750, L9 2200, L10 2700, then +600 per level. Titles: L1–2 开口者, L3–4 讲述者, L5–6 清晰表达者, L7–8 简洁表达者, L9–10 结构大师, L11+ 影响者.
  • Settlement (POST /v1/sessions/{id}/complete) is a single transaction, idempotent on session_id; repeated calls return the original settlement.

D5. Accounts and sync

First launch creates an anonymous server account (POST /v1/auth/anonymous with a Keychain-persisted install id). Sign in with Apple upgrades the same account (merges on Apple sub). Server is authoritative for all stats; iOS keeps a read cache for offline display and a local queue for pending uploads/events. No separate "device sync" feature in v1.

D6. Audio, privacy, AI processing

  • Audio uploaded as audio/mp4 (AAC, 48 kbps mono, 16 kHz) to R2 via presigned PUT. Default retention 30 days (user setting: 7 / 30 / 365). Transcripts and assessments are kept until the user deletes the session or the account.
  • Third-party AI providers are used for ASR, assessment and TTS. This is disclosed in onboarding and in Settings. Audio is not used to train models. Minimum age 13 (App Store 4+ rating is not sought; the app is 12+ with no UGC exposure).
  • DELETE /v1/sessions/{id} and DELETE /v1/me/training-data cascade to audio, transcript, assessment, evidence, ledger (ledger rows are tombstoned, stats are recomputed). Backup purge within 30 days, stated in the privacy copy.
  • Recording only starts on an explicit tap; a persistent recording indicator stays visible; the first tap shows a purpose sheet before the system microphone prompt.

D7. Release batches

  • v1.0: onboarding, diagnostic, paywall, 3-step loop with real ASR + assessment, exemplar TTS, library with topic filter, growth, profile, weekly quests, settings and deletion, reminders.
  • v1.1: article import (text paste / URL), weekly report push, streak freeze UI.
  • v2: 观点迁移, personalised recommendations, Android.

D8. Business model — free diagnostic, hard paywall

Flow: splash → onboarding (goal, scenario, self-assessed level, weekly commitment, reminder time, notification permission) → diagnostic session (one short article, round 1 only, 90 s cap; this is the real pipeline, not a demo) → result reveal (four dimensions + one action, with evidence) → plan (14-day plan targeted at the weakest dimension) → paywall → trial.

The paywall is a hard paywall: nothing past it is usable without an active entitlement (trial or paid), including the library and growth tabs. It must be interactive and honest:

  • Shows the user's own diagnostic overall and weakest dimension.
  • A weekly-days control (3 / 5 / 7) that updates a projection: "起点 N 分的用户,每周练 K 天,14 天后平均达到 M 分(样本 S 人)". Numbers come from GET /v1/paywall → benchmarks[], computed by a nightly cohort job; until real cohorts exist, the server returns pilot values with source: "pilot_benchmark" and the UI labels them "试点数据".
  • A 14-day calendar showing the plan (day 1 = 先说结论, …), the streak milestones and when the trial converts.
  • Products: clarisay.pro.annual (default, trial 7 days), clarisay.pro.monthly. Prices are set in App Store Connect; the UI reads them from the store (via RevenueCat), never hard-codes them. Restore purchases, Terms, Privacy links.
  • Purchases go through RevenueCat (amended 2026-10-17): project "Clarisay", app com.weizhi.shuke, entitlement pro (both products), offering default ($rc_annual, $rc_monthly). The RevenueCat app user id is the server user_id.
  • Entitlement is still decided server-side: after a purchase/restore the app calls POST /v1/subscriptions/revenuecat/sync, and the RevenueCat webhook (POST /v1/webhooks/revenuecat) keeps it current; both re-read RevenueCat's subscriber record. The client trusts GET /v1/me.entitlement with a 24 h grace cache for offline. POST /v1/subscriptions/apple/transactions and /v1/webhooks/apple stay only for pre-RevenueCat builds (deprecated).

D9. Progressive improvement model

  • Micro-skills (fixed catalogue v1, micro_skill_id): conclusion_first 先说结论, three_points 三点结构, cut_fillers 删掉口头禅, keep_constraints 保留关键限制, one_example 一个例子, name_the_claim 先点明观点, drop_repeats 不重复. Every one-action in an assessment maps to exactly one micro-skill.
  • Focus skill: the user's current focus = micro-skill most recently suggested and not yet achieved twice in a row. Home shows "上次建议:先说结论". Each new assessment returns achieved_previous_action: true|false|null so the feedback screen can say "做到了 ✓ 上次的建议" before anything else.
  • Mastery per micro-skill = achieved / suggested over the last 10 sessions; shown on growth as a skill grid.
  • Difficulty tiers: articles have difficulty 1–3 (length 300–450 / 450–650 / 650–900 字, argument complexity). Tier 2 unlocks at ability ≥ 70 or 10 completed sessions; tier 3 at ability ≥ 80. Daily quest picks the highest unlocked tier in the user's preferred topics, unseen first.
  • 14-day plan: generated at onboarding from the diagnostic; each day has a theme micro-skill. The plan is advisory (the loop is the same), it changes the daily quest's one-action emphasis and the copy.

D10. "A bit addictive", within limits

Mechanics in v1: daily quest card with XP badge, streak with flame and freeze, XP bar that fills with animation at settlement, level-up moment, achievements (首战告捷, 先说结论 ×3, 连续 3 天, 连续 7 天, 10 次训练, 效率 +10, 压缩成功 ×5), weekly quests (复述 5 次 / 压缩 3 次 / 做到建议 3 次), the before/after block on every completion, a "明天解锁下一个趋势点" hook on growth, reminder at the user's chosen time with streak-at-risk copy at 20:00 local if no session yet. Not allowed: guilt copy, fake scarcity, countdowns on the paywall, decoy pricing, notification more than once per day.

D10a. Accent colour contrast

Coral #FF6030 on white is 3.0:1, which passes WCAG only for large/bold text. Rule: coral backgrounds carry text ≥ 17 pt bold (buttons, badges), never body text; coral text on light surfaces uses the darker #C4421A (accent-primary-strong; amended 2026-10-18 from #E14E21, which measures 3.75:1 on the canvas and fails body text, to the nearest coral at 4.5:1 or more); when the system Increase Contrast setting is on, the primary button background switches to #C2410C (white label 5.2:1, as design/tokens.css specifies). Tokens: accent-primary (#FF6030), accent-primary-strong (#C4421A), high-contrast accent-primary (#C2410C).

D11. Dynamic AI providers

Backend exposes three capabilities — asr, llm, tts — each with a provider chain configured at runtime (KV ai:providers, editable via /v1/admin/ai-providers, with a code default). Providers in v1: ASR = Workers AI Whisper, OpenAI whisper-1/gpt-4o-transcribe, Deepgram; LLM = Anthropic (default claude-sonnet-4-5 or newer), OpenAI, Google Gemini, Workers AI; TTS = OpenAI TTS, ElevenLabs, Workers AI. Each adapter implements the same interface and returns a normalised result plus model_ref. Fallback on provider error; never fallback on content-policy or validation failure.

D12. Analytics

Event names from the PRD §10 are canonical: training_start, reading_ready, recording_start, recording_pause, attempt_submit, assessment_success, assessment_fail, feedback_view, retry_start, training_complete, reward_granted, report_view, plus onboarding_step, diagnostic_complete, paywall_view, paywall_interact, trial_start, purchase. Common properties: anonymous user id, session_id, article_id, round, screen, ts, error_code, app_version. Never audio, article text, transcript or evidence text.

D13. Languages — the product is multilingual

  • UI locales in v1: en, zh-Hans. Follows the device locale; user can override in Settings. Every string exists in both; the UI never assumes a script (no CJK-only layout, no hard-coded 字 units).
  • Practice language (the language the user reads and speaks in) is chosen in onboarding, default = UI locale, changeable any time. v1 practice languages: en, zh-Hans. The daily quest, quick picks and library are filtered by the user's practice language(s); the diagnostic uses the practice language.
  • Content is per-article: language (BCP-47), char_count or word_count as appropriate, est_read_seconds computed with a per-language reading rate (zh-Hans 400 chars/min, en 230 words/min). Seeds ship ≥ 8 articles per language per launch.
  • AI pipeline: ASR receives the article language as a hint and the detected language must match it (else unreliable: language_mismatch); assessment prompt, one-action copy, exemplar text/TTS voice are all in the article's language; micro-skill titles and instructions are localized by the API using Accept-Language.
  • API strings: any user-facing string the API returns (band labels, verdict labels, headlines, quest titles, error messages) is localized from the request's Accept-Language (fallback en). Content strings (article body, transcript, assessment notes) are in the article's language regardless of UI locale.
  • Stats (XP, streaks, ability) are per user, not per language; ability dimensions carry the language of the sessions that produced them, and growth can filter by language.
  • Landing: English default with a zh-Hans version; legal pages exist in both.
  • Adding a language later = adding seeds, a reading rate, a validity floor, a TTS voice and the string catalog entries; no code paths may branch on language === "zh-Hans" except those tables.