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(startrubric-v1),model_ref(provider/model),confidence(high|medium|low).lowconfidence 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_questand 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 onsession_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}andDELETE /v1/me/training-datacascade 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
overalland 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 withsource: "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, entitlementpro(both products), offeringdefault($rc_annual,$rc_monthly). The RevenueCat app user id is the serveruser_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 trustsGET /v1/me.entitlementwith a 24 h grace cache for offline.POST /v1/subscriptions/apple/transactionsand/v1/webhooks/applestay 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|nullso 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
difficulty1–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_countorword_countas appropriate,est_read_secondscomputed 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 usingAccept-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(fallbacken). 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.