Algorithm Report
Civix Learning Algorithm Report
This document describes the learning algorithm in apps/mobile/lib/algorithm.ts: behavior, constants, and optimization status. It is maintained to reflect the implementation and tests in that file.
1. Algorithm version and USCIS constants
- Version:
ALGORITHM_VERSION= 5.4.0 (lib/algorithm.ts:2339) — first-question flow-zone sequencing across adaptive learning modes. - USCIS test format:
- Standard: 128 questions in pool; 20 asked at interview; 12 correct required to pass.
- 65/20 (senior): 20 designated questions; 10 asked; 6 correct required to pass.
Constants in code: USCIS.TOTAL_QUESTIONS, USCIS.EXAM_QUESTIONS, USCIS.PASSING_SCORE, USCIS.SENIOR_QUESTIONS, USCIS.SENIOR_EXAM_QUESTIONS, USCIS.SENIOR_PASSING.
1.1 Architecture — four cooperating layers
The engine is not "SM-2" alone; earlier revisions of this document described only the scheduling layer. Four layers cooperate, each answering a different question:
| Layer | Question it answers | Code |
|---|---|---|
| SM-2 scheduling | Which questions are due to enter a session | algorithm.ts:380-426 |
| Elo / 2PL flow zone | In what order to serve them, live | algorithm.ts:466-506, :699-770 |
| BKT (Bayesian Knowledge Tracing) | How well is each item known | algorithm.ts:549-570 |
| Monte-Carlo simulation | What is the probability of passing | algorithm.ts:623-648 |
Flow zone. Ordering targets an expected score of ~0.75, so difficulty tracks
the learner. Ability and item Elo are clamped to [600, 2000] (:439-441) and the
system self-stabilises: ability drifts until the learner's graded score matches
expectation.
Pass probability is free-recall, not multiple choice. The simulation uses a
guess floor of 0.03, not the MCQ 1/4 (:573-594). The real interview is oral
recall, and a 0.25 floor inflated weak learners' odds roughly twofold. This is
the single most important honesty decision in the file.
Exemption modes are simulated with the correct shape — draw 20 need 12, or
draw 10 need 6 from the designated senior pool only (:650-683).
Determinism. The pure core contains no Date.now() or Math.random(). All
shuffles take seeds, and the Monte-Carlo uses mulberry32 with a fixed seed, so a
displayed readiness number only moves when mastery moves. Session seeds are
time-based by design so sessions vary, while the pipeline stays seedable in tests.
Known characteristics (as of 2026-08-03)
Documented so they are not rediscovered as bugs. All of these lean pessimistic — displayed readiness understates rather than overstates, which is the safe direction for a paid product.
- A correct answer at exactly the learner's own average speed grades quality 3
(
:367-371); quality 4 requires beating their own EMA. Consistently-correct steady responders therefore plateau around BKT 0.75–0.85 rather than 0.99. - Ease factor does not decrease on failure (
:390-399); it only falls via the slow-correct path. Leech detection (:834-836) and the streak-0 weak boost (:944-955) catch the same items. - "New" questions outrank overdue ones (10000 vs ~5000–8000,
:901-934), so the bank is swept for coverage before first reviews come back. Deliberate for a finite exam bank; looser early spacing than strict SM-2. - BKT does not decay with time.
getPassProbabilityreads rawbkt(:679-682), so a learner returning after a long absence sees unchanged pass odds even thoughcalculateRetrievability(:2196-2217) knows better. This is the main outstanding truthfulness gap and the first planned algorithm change. - Two mastery definitions coexist: the practice grid uses
lib/stats/mastery.ts:38-47(streak ≥ 3), readiness usesstatsV2.mastered(streak ≥ 3 AND seen ≥ 5 AND ≥ 80% accuracy,:824-832).
Readiness weighting caveat
lib/home-readiness.ts:361-370 composes the headline readiness score almost
entirely from civics signals; the English test (reading/writing/speaking) enters
only through interviewReadiness, weighted at 5% and itself averaged 4-way with
civics. A learner who has never practised the English components scores a flat 60
on that component (home-readiness.ts:186). Since English is an independent way
to fail the real interview, this under-weights a genuine failure mode. Tracked as
a post-launch change.
2. Mastery levels
Each question is classified into one level via getMasteryLevel(stats):
| Level | Meaning | How it's computed |
|---|---|---|
| new | Never seen | !stats or stats.lastSeen === 0 |
| learning | Seen but not yet learned | Not mastered, stats.correct < 3 |
| reviewing | In review cycle, not yet due | Has nextReview in the future |
| due | Due for review | nextReview > 0 and now >= nextReview and within 24h of due |
| overdue | Past due | now > nextReview + 24h |
| mastered | Fully mastered | stats.mastered === true (streak ≥ 3, seen ≥ 5, correct ratio ≥ 80%) |
Usage in selection: Priority scoring favors new (base 10000+), then overdue, due, weak (streak 0), weak topics, low ease factor, and deprioritizes mastered (with exceptions when deadline is near). Review mode filters to due questions when any exist; learn mode favors new/learning; weak mode favors low streak or ease < 2.0.
2.1 Competency milestones (4-tier model)
computeCompetencySnapshot(pool, stats, options) computes a learner competency snapshot from:
- mastered percent
- rolling accuracy
- overdue ratio
- weak-topic density
- pass-risk distribution
Tiers:
- foundation
- developing
- proficient
- interview_ready
Snapshot includes score (0-100), confidence, reasons, next tier target, and points-to-next-tier.
3. Study modes
| Mode | Purpose | Filter / fallback |
|---|---|---|
| learn | New and learning questions | Filter: no stats or lastSeen === 0 or correct < 3. Fallback: full pool if fewer than count. |
| review | Due reviews | Filter: nextReview > 0 and now >= nextReview. Fallback: full pool if no due questions. |
| weak | Weak topics | Filter: no stats (count as weak) or streak < MASTERY.MIN_STREAK or easeFactor < 2.0. Fallback: full pool. |
| coverage | Ensure all seen | Reserves ~30% of slots for unseen; rest by priority. |
| category | Specific category | Filter by category or tags. |
| senior | 65/20 | Filter to senior-only pool (senior65 or SENIOR_QUESTION_IDS) and prioritize evaluatable questions (state-dependent items require stateProfile). No fallback to non-senior pool. |
| exam | Exam simulation | Handled by selectExamQuestions: category round-robin, 20 (standard) or 10 (senior) questions; senior exam uses same evaluatable-first guard. |
4. Priority factors (calculatePriority)
Order of application (conceptually):
- New questions: Base 10000; +500 easy, +250 medium; deadline critical/intensive adds 3000/1500.
- Recently answered: -10000 unless due/overdue (then -500).
- Overdue: 5000 + min(daysOverdue × 200, 2000) + 500 × urgencyMultiplier.
- Due: 3000 + 300 × urgencyMultiplier (within 24h of due).
- Weak (streak 0, wrong > 0): 2000; +500 if seen < 1 day ago; +1000 × urgencyMultiplier if deadline.
- Weak topics: Sum of (weakTopics[tag] × 100) × urgencyMultiplier.
- Low ease factor (< 2.0): (2.5 - easeFactor) × 500; +300 × urgencyMultiplier when deadline and not relaxed.
- Spaced timing: Bonus by days since last seen (e.g. 1–3 days +300; >7 days up to +400); deadline critical/intensive tightens windows.
- Difficulty progression: If doing well (correct ≥ 3, streak ≥ 2), +200 hard, +100 medium.
- Mastered: Penalty -3000 (relaxed) / -1500 (intensive) / -500 (critical); reduced if days since > 7 or deadline near and days since > 3.
- Pass-risk layer: Adds weighted risk score (wrong-rate trend, streak breaks, low ease factor, overdue days, low-quality recent answers, deadline pressure) to prioritize likely failure points.
Deadline urgency multipliers: relaxed 1.0, moderate 1.5, intensive 2.0, critical 3.0.
5. SM-2 and mastery constants
- SM2: Initial ease 2.5, min 1.3; initial interval 1 day; graduating interval 6 days; easy bonus 1.3.
- MASTERY: MIN_STREAK 3, MIN_SEEN 5, MIN_CORRECT_RATIO 0.8, TEST_READY_THRESHOLD 0.9 (90% mastered = test ready).
- Quality (0–5): Used in SM-2; correct answers get 3–5 by response speed; wrong get 0–2.
6. Optimization status
- Correctness:
MasteryProgress.learningreturns only the count of questions in the learning and reviewing levels (no double-count withdue/overdue). Unit test enforces separate counts for new, learning, due, overdue, mastered. - Selection: All study modes (learn, review, weak, coverage, category, senior) have defined filters and fallbacks. Review mode prefers due questions when present; coverage mode reserves slots for unseen; exam mode returns exactly 20 (standard) or 10 (senior) when pool is sufficient.
- Senior hardening: Senior mode now avoids unanswerable state-dependent items when
stateProfileis missing (e.g., governor/senator prompts), while still including them when profile context exists. Store fallbacks for practice and exam preserve senior-only scope. - Pass-rate optimization: Priority now includes
calculatePassRisk(...)to surface weak/overdue/high-risk items earlier with urgency-aware weights. - Deterministic scenarios:
shuffleArraySeeded(...)and optionalshuffleSeedin selection APIs enable reproducible question order for testing and simulation. - Recommendations:
getStudyRecommendations(...)accepts{ isSeniorMode?: boolean }and generates mode-aware copy when the user is in senior mode. - Performance:
weightedShufflepre-normalizes weights once (O(n)); each pick is O(remaining length). Total cost O(n × count) instead of O(n²) per call.
7. Related files
- Implementation: apps/mobile/lib/algorithm.ts
- Unit tests: apps/mobile/tests/algorithm.test.ts
- Consumers: store
getMasteryProgress,getWeakTopicsAnalysis,selectQuestions; home-readiness; results completion UI.