diff --git a/apps/web/.claude/PROBLEM_GENERATION.md b/apps/web/.claude/PROBLEM_GENERATION.md
new file mode 100644
index 00000000..02b65d6d
--- /dev/null
+++ b/apps/web/.claude/PROBLEM_GENERATION.md
@@ -0,0 +1,353 @@
+# Problem Generation System - Claude Code Reference
+
+## Quick Reference for AI Development
+
+This document provides quick-reference information about the worksheet problem generation system for Claude Code and developers working on this codebase.
+
+---
+
+## File Locations
+
+### Core Logic
+- **`src/app/create/worksheets/problemGenerator.ts`** - All generation algorithms (addition, subtraction, mixed)
+- **`src/app/create/worksheets/utils/validateProblemSpace.ts`** - Space estimation and validation
+- **`src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md`** - Complete technical documentation
+
+### UI Components
+- **`components/worksheet-preview/WorksheetPreviewContext.tsx`** - Validation triggering and state
+- **`components/worksheet-preview/DuplicateWarningBanner.tsx`** - Warning display UI
+
+---
+
+## Two Generation Strategies
+
+### When to Use Each
+
+```typescript
+const estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, operator)
+
+if (estimatedSpace < 10000) {
+ // STRATEGY 1: Generate-All + Shuffle
+ // Zero retries, guaranteed coverage, deterministic
+} else {
+ // STRATEGY 2: Retry-Based
+ // Random generation, allows some duplicates after 100 retries
+}
+```
+
+### Strategy 1: Generate-All (Small Spaces)
+
+**Examples:**
+- 1-digit 100% regrouping: 45 unique problems
+- 2-digit mixed regrouping: ~4,000 unique problems
+
+**Key behavior:**
+```typescript
+// Non-interpolate: Shuffle and cycle
+problems[0-44] = first shuffle
+problems[45-89] = second shuffle (same order)
+problems[90+] = third shuffle...
+
+// Interpolate: Sort by difficulty, then cycle maintaining progression
+seen.clear() when exhausted // Start new cycle
+```
+
+**Location:** `problemGenerator.ts:381-503`
+
+### Strategy 2: Retry-Based (Large Spaces)
+
+**Examples:**
+- 3-digit problems: ~400,000 unique problems
+- 4-5 digit problems: millions of unique problems
+
+**Key behavior:**
+```typescript
+let tries = 0
+while (tries++ < 100 && !unique) {
+ problem = generate()
+ if (!seen.has(key)) {
+ seen.add(key)
+ break
+ }
+}
+// Allow duplicate if still not unique after 100 tries
+```
+
+**Location:** `problemGenerator.ts:506-595`
+
+---
+
+## Critical Edge Cases
+
+### 1. Single-Digit 100% Regrouping
+
+**Problem:** Only 45 unique problems exist!
+
+```typescript
+// 1-digit addition where a + b >= 10
+// a=0: none, a=1: 9 (1+9), a=2: 8 (2+8,2+9), ..., a=9: 1 (9+1)
+// Total: 0+9+8+7+6+5+4+3+2+1 = 45
+```
+
+**User impact:**
+- Requesting 100 problems → 55 duplicates guaranteed
+- Warning banner shown: "Single-digit problems (1-9) with 100% regrouping have very few unique combinations!"
+
+**Code:** `validateProblemSpace.ts:56-64` (exact count), `validateProblemSpace.ts:175-179` (warning)
+
+### 2. Mixed Mode with Mastery
+
+**Problem:** Cannot validate combined space with separate configs
+
+```typescript
+// Addition skill: {digitRange: {min:2, max:2}, pAnyStart: 0.3}
+// Subtraction skill: {digitRange: {min:1, max:2}, pAnyStart: 0.7}
+// Combined estimation is complex and potentially misleading
+```
+
+**Solution:** Skip validation entirely
+
+```typescript
+// WorksheetPreviewContext.tsx:53-56
+if (mode === 'mastery' && operator === 'mixed') {
+ setWarnings([])
+ return
+}
+```
+
+### 3. Subtraction Multiple Borrowing Impossibility
+
+**Mathematical fact:** 1-2 digit subtraction cannot have 2+ borrows
+
+```typescript
+// generateBothBorrow() - problemGenerator.ts:802-804
+if (maxDigits <= 2) {
+ return generateOnesOnlyBorrow(rand, minDigits, maxDigits) // Fallback
+}
+```
+
+### 4. Borrowing Across Zeros
+
+**Example:** `1000 - 1` requires 3 borrow operations
+
+```
+ones: 0 < 1, borrow from tens
+tens: 0 (zero!), borrow from hundreds (+1 for crossing zero)
+hundreds: 0, borrow from thousands (+1 for crossing zero)
+thousands: 1, decrement
+Total: 3 borrows
+```
+
+**Code:** `countBorrows()` - `problemGenerator.ts:740-782`
+
+---
+
+## Debugging Commands
+
+### Check Server Logs for Generation
+
+```bash
+# Look for these log patterns:
+[ADD GEN] Starting: 100 problems, digitRange: 1-1, pAnyStart: 1, pAllStart: 0
+[ADD GEN] Estimated unique problem space: 45 (requesting 100)
+[ADD GEN] Using generate-all + shuffle (space < 10000, interpolate=true)
+[ADD GEN] Exhausted all 45 unique problems at position 45. Starting cycle 2.
+[ADD GEN] Complete: 100 problems in 8ms (0 retries, generate-all with progressive difficulty, 1 cycles)
+```
+
+### Test Problem Space Estimation
+
+```typescript
+import { estimateUniqueProblemSpace } from './utils/validateProblemSpace'
+
+// 1-digit 100% regrouping
+const space1 = estimateUniqueProblemSpace({min: 1, max: 1}, 1.0, 'addition')
+console.log(space1) // Expected: 45
+
+// 2-digit mixed
+const space2 = estimateUniqueProblemSpace({min: 2, max: 2}, 0.5, 'addition')
+console.log(space2) // Expected: ~4000
+```
+
+### Verify Cycling Behavior
+
+```typescript
+// Generate 100 problems from 45-problem space
+const problems = generateProblems(100, 1.0, 0, false, 12345, {min: 1, max: 1})
+
+// Check that problems 0-44 and 45-89 are identical
+const cycle1 = problems.slice(0, 45)
+const cycle2 = problems.slice(45, 90)
+console.log('Cycles match:',
+ cycle1.every((p, i) => p.a === cycle2[i].a && p.b === cycle2[i].b)
+) // Expected: true
+```
+
+---
+
+## Common Modifications
+
+### Adding a New Difficulty Category
+
+**Current categories:** `non`, `onesOnly`, `both`
+
+**To add a new category:**
+
+1. Add category type to `ProblemCategory` in `types.ts`
+2. Create generator function (e.g., `generateThreePlus()`)
+3. Update probability sampling in retry-based strategy
+4. Update `countRegroupingOperations()` or `countBorrows()` for difficulty scoring
+5. Update `generateAllAdditionProblems()` or `generateAllSubtractionProblems()` filtering
+
+### Changing Strategy Threshold
+
+**Current:** 10,000 unique problems
+
+```typescript
+const THRESHOLD = 10000
+if (estimatedSpace < THRESHOLD) {
+ // Generate-all
+} else {
+ // Retry-based
+}
+```
+
+**To change:**
+- Increase for better uniqueness guarantees (more generate-all usage)
+- Decrease for better performance on larger spaces (more retry-based usage)
+
+**Trade-off:** Generate-all is O(n²) enumeration, slow for large spaces
+
+### Adjusting Retry Limit
+
+**Current:** 100 retries per problem
+
+```typescript
+let tries = 0
+while (tries++ < 100 && !ok) {
+ // Generate and check uniqueness
+}
+```
+
+**To change:**
+- Increase for better uniqueness (slower generation)
+- Decrease for faster generation (more duplicates)
+
+**Historical note:** Was 3000 retries, reduced to 100 for performance
+- 100 problems × 3000 retries = 300,000 iterations (seconds)
+- 100 problems × 100 retries = 10,000 iterations (milliseconds)
+
+---
+
+## Problem Space Estimation Formulas
+
+### Exact Counting (1-Digit)
+
+```typescript
+// Addition regrouping (a + b >= 10)
+for (let a = 0; a <= 9; a++) {
+ for (let b = 0; b <= 9; b++) {
+ if (a + b >= 10) count++
+ }
+}
+// Result: 45
+
+// Addition non-regrouping
+// Result: 100 - 45 = 55
+```
+
+### Heuristic Estimation (2+ Digits)
+
+```typescript
+numbersPerDigitCount = digits === 1 ? 10 : 9 * 10^(digits-1)
+
+// Addition
+pairsForDigits = numbersPerDigitCount * numbersPerDigitCount
+regroupFactor = pAnyStart > 0.8 ? 0.45 : pAnyStart > 0.5 ? 0.5 : 0.7
+totalSpace += pairsForDigits * regroupFactor
+
+// Subtraction (only minuend >= subtrahend valid)
+pairsForDigits = (numbersPerDigitCount * numbersPerDigitCount) / 2
+borrowFactor = pAnyStart > 0.8 ? 0.35 : pAnyStart > 0.5 ? 0.5 : 0.7
+totalSpace += pairsForDigits * borrowFactor
+```
+
+**Why these factors?**
+- High regrouping requirement → Fewer valid problems
+- Medium regrouping → About half
+- Low/mixed → Most problems valid
+
+---
+
+## User Warning Levels
+
+```typescript
+const ratio = requestedProblems / estimatedSpace
+
+if (ratio < 0.3) {
+ duplicateRisk = 'none' // No warning shown
+} else if (ratio < 0.5) {
+ duplicateRisk = 'low' // "Some duplicates may occur"
+} else if (ratio < 0.8) {
+ duplicateRisk = 'medium' // "Expect moderate duplicates" + suggestions
+} else if (ratio < 1.5) {
+ duplicateRisk = 'high' // "High duplicate risk!" + recommendations
+} else {
+ duplicateRisk = 'extreme' // "Mostly duplicate problems" + strong warnings
+}
+```
+
+**Location:** `validateProblemSpace.ts:130-172`
+
+---
+
+## Testing Checklist
+
+When modifying problem generation:
+
+- [ ] Test 1-digit 100% regrouping (45 unique)
+- [ ] Test 2-digit mixed (should use generate-all)
+- [ ] Test 3-digit (should use retry-based)
+- [ ] Test cycling: Request 100 from 45-problem space
+- [ ] Test progressive difficulty (interpolate=true)
+- [ ] Test constant difficulty (interpolate=false)
+- [ ] Test mixed mode (manual)
+- [ ] Test mixed mode (mastery with separate configs)
+- [ ] Test subtraction borrowing across zeros
+- [ ] Test subtraction 2-digit multiple borrowing (should fallback)
+- [ ] Verify server logs show correct strategy selection
+- [ ] Verify warnings appear at correct thresholds
+
+---
+
+## Related Documentation
+
+- **`PROBLEM_GENERATION_ARCHITECTURE.md`** - Complete technical documentation (read this first for deep understanding)
+- **`CONFIG_SCHEMA_GUIDE.md`** - Worksheet configuration schema
+- **`SMART_DIFFICULTY_SPEC.md`** - Smart mode difficulty progression
+- **`SUBTRACTION_AND_OPERATOR_PLAN.md`** - Subtraction implementation plan
+
+---
+
+## Quick Answers
+
+**Q: Why am I seeing duplicate problems?**
+A: Check estimated space vs requested problems. If ratio > 1.0, duplicates are inevitable.
+
+**Q: Why is generation slow?**
+A: If using retry-based with constrained space (e.g., 2-digit 100% regrouping), switch to generate-all by increasing THRESHOLD.
+
+**Q: Why does interpolate mode show different problems on second cycle?**
+A: By design! It clears the "seen" set to allow re-sampling while maintaining difficulty progression.
+
+**Q: Why is mastery+mixed mode not showing warnings?**
+A: Validation is skipped because addition and subtraction have separate configs, making combined estimation complex.
+
+**Q: Can I have 3+ borrows in 2-digit subtraction?**
+A: No, mathematically impossible. `generateBothBorrow()` falls back to ones-only for maxDigits ≤ 2.
+
+**Q: How do I test if a configuration will have many duplicates?**
+A: Use `validateProblemSpace()` - it returns `duplicateRisk` level and `warnings` array.
+
+**Q: What's the difference between addition and subtraction uniqueness?**
+A: Addition is commutative (2+3 = 3+2, same problem). Subtraction is not (5-3 ≠ 3-5, different problems).
diff --git a/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md b/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md
new file mode 100644
index 00000000..4cbc5616
--- /dev/null
+++ b/apps/web/src/app/create/worksheets/PROBLEM_GENERATION_ARCHITECTURE.md
@@ -0,0 +1,556 @@
+# Problem Generation Architecture
+
+## Overview
+
+This document describes the complete architecture for generating addition, subtraction, and mixed worksheets with configurable difficulty and uniqueness constraints.
+
+## Table of Contents
+
+1. [Core Concepts](#core-concepts)
+2. [Generation Strategies](#generation-strategies)
+3. [Problem Space Estimation](#problem-space-estimation)
+4. [Edge Cases](#edge-cases)
+5. [Performance Considerations](#performance-considerations)
+6. [User-Facing Warnings](#user-facing-warnings)
+
+---
+
+## Core Concepts
+
+### Problem Types
+
+**Addition Problems**
+- Two addends: `a + b = ?`
+- Regrouping ("carrying"): When column sum ≥ 10
+- Categories:
+ - **Non-regrouping**: No carries in any column (e.g., 23 + 45)
+ - **Ones-only**: Carry from ones to tens only (e.g., 27 + 58)
+ - **Multiple regrouping**: Carries in 2+ columns (e.g., 67 + 48)
+
+**Subtraction Problems**
+- Minuend - Subtrahend: `a - b = ?` where `a >= b`
+- Borrowing ("regrouping"): When minuend digit < subtrahend digit
+- Categories:
+ - **Non-borrowing**: No borrows needed (e.g., 89 - 34)
+ - **Ones-only**: Borrow in ones place only (e.g., 52 - 17)
+ - **Multiple borrowing**: Borrows in 2+ positions (e.g., 534 - 178)
+
+**Mixed Problems**
+- Two modes:
+ - **Manual mode**: 50/50 random mix using same constraints for both operators
+ - **Mastery mode**: Separate skill-based configs for addition and subtraction
+
+### Difficulty Parameters
+
+**pAnyStart** (0.0 - 1.0)
+- Probability that ANY regrouping/borrowing occurs
+- `0.0` = No regrouping/borrowing allowed
+- `1.0` = ALL problems must have regrouping/borrowing
+- `0.5` = Mixed (about half the problems will have regrouping/borrowing)
+
+**pAllStart** (0.0 - 1.0)
+- Probability of MULTIPLE regrouping/borrowing operations
+- Must be ≤ pAnyStart
+- Only meaningful when `pAnyStart > 0`
+
+**interpolate** (boolean)
+- `false` = All problems at same difficulty (uses pAnyStart as constant)
+- `true` = Progressive difficulty from easy to hard
+ - Early problems: `pAny = pAnyStart × 0.0`
+ - Late problems: `pAny = pAnyStart × 1.0`
+
+**digitRange** (`{min: 1-5, max: 1-5}`)
+- Range of digits for each operand
+- `{min: 1, max: 1}` = Single-digit problems (0-9 for addition, 1-9 for subtraction)
+- `{min: 2, max: 2}` = Two-digit problems (10-99)
+- `{min: 1, max: 3}` = Mixed 1-3 digit problems
+
+---
+
+## Generation Strategies
+
+The system uses **two different strategies** based on problem space size:
+
+### Strategy 1: Generate-All + Shuffle (Small Spaces)
+
+**When used:** Estimated unique problems < 10,000
+
+**Process:**
+1. **Enumerate all valid problems** within constraints
+ - For addition: All pairs `(a, b)` in digit range matching regrouping requirements
+ - For subtraction: All pairs `(m, s)` where `m ≥ s` matching borrowing requirements
+2. **Filter by difficulty constraints**
+ - `pAnyStart = 1.0` → Keep only regrouping/borrowing problems
+ - `pAnyStart = 0.0` → Keep only non-regrouping/non-borrowing problems
+ - `0.0 < pAnyStart < 1.0` → Keep all problems (will sample later)
+3. **Handle interpolation**
+ - **If interpolate = false**: Shuffle deterministically, take first N problems
+ - **If interpolate = true**: Sort by difficulty (carry/borrow count), sample based on difficulty curve
+
+**Cycling behavior** (when requesting > available):
+- **Non-interpolate**: Repeat shuffled sequence indefinitely
+ - Problems 0-44: First shuffle
+ - Problems 45-89: Second shuffle (same order)
+ - Problems 90+: Third shuffle, etc.
+- **Interpolate**: Clear "seen" set after exhausting all unique problems, maintain difficulty curve
+ - Each cycle: Easy→Medium→Hard progression
+ - Cycle boundary logged: `"Exhausted all 45 unique problems at position 45. Starting cycle 2"`
+
+**Advantages:**
+- **Zero retries** - No random generation needed
+- **Guaranteed coverage** - Every unique problem appears once before repeating
+- **Deterministic** - Same seed always produces same worksheet
+
+**Code location:** `problemGenerator.ts:365-470`
+
+### Strategy 2: Retry-Based Generation (Large Spaces)
+
+**When used:** Estimated unique problems ≥ 10,000
+
+**Process:**
+1. **For each problem position** (i = 0 to total-1):
+ - Calculate difficulty fraction: `frac = i / (total - 1)` (0.0 → 1.0)
+ - Interpolate difficulty: `pAny = pAnyStart × frac` (if interpolate=true)
+ - Sample problem category based on probabilities
+ - Generate random problem in that category
+ - Retry up to 100 times if duplicate
+ - If still duplicate after 100 tries, allow it (prevents infinite loops)
+
+**Uniqueness tracking:**
+- Addition: Key = `"min(a,b)+max(a,b)"` (commutative)
+- Subtraction: Key = `"minuend-subtrahend"` (not commutative)
+
+**Retry limits:**
+- Old limit: 3000 retries per problem (millions of iterations for large worksheets!)
+- New limit: 100 retries per problem (allow some duplicates to prevent performance issues)
+
+**Why allow duplicates?**
+- For constrained spaces (e.g., 100 problems from 2-digit 100% regrouping), uniqueness is impossible
+- Better to have a few duplicates than hang for minutes generating one worksheet
+- Duplicates are rare in practice for large spaces
+
+**Code location:** `problemGenerator.ts:473-557`
+
+---
+
+## Problem Space Estimation
+
+### Purpose
+Estimate how many unique problems exist given constraints to:
+1. Choose generation strategy (enumerate vs retry)
+2. Warn users about duplicate risk
+3. Display problem space in UI
+
+### Exact Counting (Small Spaces)
+
+**1-digit addition (0-9):**
+- Total pairs: 10 × 10 = 100
+- Regrouping pairs: 45 (where a + b ≥ 10)
+ - Calculation: `sum from a=0 to 9 of (9-a+1 if a+b≥10 else 0)`
+ - a=0: none, a=1: 9, a=2: 8, ..., a=9: 1
+ - Total: 0+9+8+7+6+5+4+3+2+1 = 45
+- Non-regrouping: 100 - 45 = 55
+
+**1-digit subtraction (0-9):**
+- Valid pairs: 55 (where m ≥ s, including 0-0)
+- Borrowing: ~36 (where m < s at ones place)
+- Non-borrowing: ~19
+
+**2-digit addition (10-99):**
+- Total pairs: 90 × 90 = 8,100
+- Use generate-all for exact count based on pAnyStart
+
+### Heuristic Estimation (Large Spaces)
+
+For digit ranges ≥ 2-digit or mixed ranges:
+
+```typescript
+numbersPerDigitCount = digits === 1 ? 10 : 9 × 10^(digits-1)
+
+// Addition
+pairsForDigits = numbersPerDigitCount × numbersPerDigitCount
+regroupFactor = pAnyStart > 0.8 ? 0.45 : pAnyStart > 0.5 ? 0.5 : 0.7
+totalSpace += pairsForDigits × regroupFactor
+
+// Subtraction
+pairsForDigits = (numbersPerDigitCount × numbersPerDigitCount) / 2 // Only m ≥ s
+borrowFactor = pAnyStart > 0.8 ? 0.35 : pAnyStart > 0.5 ? 0.5 : 0.7
+totalSpace += pairsForDigits × borrowFactor
+```
+
+**Why these factors?**
+- High regrouping requirement (pAnyStart > 0.8) → Fewer valid problems
+- Medium regrouping (pAnyStart > 0.5) → About half the space
+- Low regrouping → Most problems are valid
+
+**Code location:** `utils/validateProblemSpace.ts:18-104`
+
+---
+
+## Edge Cases
+
+### 1. Single-Digit High Regrouping (CRITICAL)
+
+**Problem:** 1-digit addition with 100% regrouping
+**Space size:** Only 45 unique problems!
+
+**User impact:**
+- Requesting 100 problems → 55 duplicates guaranteed
+- Warning banner: "Single-digit problems (1-9) with 100% regrouping have very few unique combinations!"
+
+**Mitigation:**
+- Clear warning in UI
+- Suggest increasing digit range to 2
+- Suggestion: Reduce to 1 page (20 problems) or lower regrouping to 50%
+
+**Code:** `validateProblemSpace.ts:175-179`
+
+### 2. Mixed Mode with Mastery
+
+**Problem:** Addition and subtraction use different skill configs
+**Space estimation:** Cannot easily estimate combined space
+
+**Solution:**
+- Skip validation entirely for `mode=mastery && operator=mixed`
+- Code: `WorksheetPreviewContext.tsx:53-56`
+
+**Reason:** Each operator has its own digitRange, pAnyStart, making combined estimation complex and potentially misleading
+
+### 3. Subtraction Multiple Borrowing Impossibility
+
+**Problem:** 1-2 digit subtraction cannot have 2+ borrows
+
+**Mathematical proof:**
+- 1-digit: Only 1 column, max 1 borrow
+- 2-digit: Max minuend = 99, if ones requires borrow, tens can only have 0-1 borrows
+
+**Solution:**
+- `generateBothBorrow()` falls back to `generateOnesOnlyBorrow()` when `maxDigits ≤ 2`
+- Code: `problemGenerator.ts:802-804`
+
+### 4. Borrowing Across Zeros
+
+**Problem:** `1000 - 1` requires 3 borrows (hundreds→tens→ones)
+
+**Counting logic:**
+```typescript
+100 - 1:
+ ones: 0 < 1, borrow from tens
+ tens: 0 (zero!), borrow from hundreds // +1 borrow for crossing zero
+ hundreds: 1, decrement to 0
+ Total: 2 borrows (ones + crossing zero)
+
+1000 - 1:
+ ones: 0 < 1, borrow from tens
+ tens: 0, borrow from hundreds // +1 borrow
+ hundreds: 0, borrow from thousands // +1 borrow
+ thousands: 1, decrement to 0
+ Total: 3 borrows
+```
+
+**Code:** `problemGenerator.ts:740-782` (`countBorrows` function)
+
+### 5. Duplicate Detection
+
+**Addition is commutative:**
+- `23 + 45` = `45 + 23` → Same problem
+- Key: `"min(a,b)+max(a,b)"` = `"23+45"`
+
+**Subtraction is NOT:**
+- `45 - 23` ≠ `23 - 45`
+- Key: `"45-23"` (order matters)
+
+**Mixed mode:**
+- `23 + 45` and `45 - 23` are considered different (different operators)
+
+---
+
+## Performance Considerations
+
+### Strategy Selection Threshold
+
+**Why 10,000?**
+- Generate-all is O(n²) where n = numbers in range
+- For 2-digit: 90² = 8,100 pairs (under threshold) → Use generate-all
+- For 3-digit: 900² = 810,000 pairs (over threshold) → Use retry-based
+- Enumeration is fast for < 10K, slow for > 100K
+
+### Retry Limit Reduction
+
+**Old:** 3000 retries per problem
+**New:** 100 retries per problem
+
+**Why reduced?**
+- 100-problem worksheet × 3000 retries = 300,000 iterations (seconds to generate)
+- 100-problem worksheet × 100 retries = 10,000 iterations (milliseconds to generate)
+- For large problem spaces, duplicates are rare anyway
+- For small problem spaces, we use generate-all (zero retries)
+
+**When duplicates occur:**
+- Constrained space + retry strategy = Some duplicates allowed
+- Example: 2-digit 100% regrouping, 500 problems requested
+ - Unique space: ~3,700 problems
+ - After 3,700 unique: Retries start failing, duplicates appear
+ - Alternative: Use generate-all + cycle (better!)
+
+### Logging
+
+**Addition generation:**
+```
+[ADD GEN] Starting: 100 problems, digitRange: 1-1, pAnyStart: 1, pAllStart: 0
+[ADD GEN] Estimated unique problem space: 45 (requesting 100)
+[ADD GEN] Using generate-all + shuffle (space < 10000, interpolate=true)
+[ADD GEN] Generated 45 unique problems
+[ADD GEN] Sorting problems by difficulty for progressive difficulty
+[ADD GEN] Exhausted all 45 unique problems at position 45. Starting cycle 2.
+[ADD GEN] Complete: 100 problems in 8ms (0 retries, generate-all with progressive difficulty, 1 cycles)
+```
+
+**Subtraction generation:**
+```
+[SUB GEN] Starting: 50 problems, digitRange: 2-2, pAnyBorrow: 0.5, pAllBorrow: 0
+[SUB GEN] Estimated unique problem space: 4050 (requesting 50)
+[SUB GEN] Using generate-all + shuffle (space < 10000)
+[SUB GEN] Generated 4050 unique problems
+[SUB GEN] Complete: 50 problems in 112ms (0 retries, generate-all method)
+```
+
+**Mixed mastery generation:**
+```
+[MASTERY MIXED] Generating 100 mixed problems (50/50 split)...
+[MASTERY MIXED] Step 1: Generating 50 addition problems...
+[ADD GEN] Starting: 50 problems, digitRange: 2-2, pAnyStart: 0.3, pAllStart: 0
+[MASTERY MIXED] Step 1: ✓ Generated 50 addition problems in 45ms
+[MASTERY MIXED] Step 2: Generating 50 subtraction problems...
+[SUB GEN] Starting: 50 problems, digitRange: 1-2, pAnyBorrow: 0.7, pAllBorrow: 0
+[MASTERY MIXED] Step 2: ✓ Generated 50 subtraction problems in 23ms
+[MASTERY MIXED] Step 3: Shuffling 100 problems...
+[MASTERY MIXED] Step 3: ✓ Shuffled in 0ms
+```
+
+---
+
+## User-Facing Warnings
+
+### Current Implementation
+
+**Where shown:** `DuplicateWarningBanner` component (preview pane, centered)
+
+**Trigger conditions:**
+- `duplicateRisk !== 'none'` (ratio ≥ 0.3)
+- Not dismissed by user
+- Not in mastery + mixed mode
+
+**Warning levels:**
+
+**Low risk** (0.3 ≤ ratio < 0.5):
+```
+You're requesting 50 problems, but only ~45 unique problems are possible
+with these constraints. Some duplicates may occur.
+```
+
+**Medium risk** (0.5 ≤ ratio < 0.8):
+```
+Warning: Only ~45 unique problems possible, but you're requesting 100.
+Expect moderate duplicates.
+
+Suggestion: Reduce pages to 1 or increase digit range to 3
+```
+
+**High risk** (0.8 ≤ ratio < 1.5):
+```
+High duplicate risk! Only ~45 unique problems possible for 100 requested.
+
+Recommendations:
+ • Reduce to 1 pages (50% of available space)
+ • Increase digit range to 2-2
+ • Lower regrouping probability from 100% to 50%
+```
+
+**Extreme risk** (ratio ≥ 1.5):
+```
+Extreme duplicate risk! Requesting 200 problems but only ~45 unique problems exist.
+
+This configuration will produce mostly duplicate problems.
+
+Strong recommendations:
+ • Reduce to 1 pages maximum
+ • OR increase digit range from 1-1 to 1-2
+ • OR reduce regrouping requirement from 100%
+```
+
+**Special case - Single digit:**
+```
+Single-digit problems (1-9) with 100% regrouping have very few unique combinations!
+```
+
+### Suggested Improvements
+
+**1. Show in Config Panel (Proactive)**
+- Display estimated problem space next to pages/problemsPerPage sliders
+- Live update as user adjusts settings
+- Color-coded indicator: Green (plenty), Yellow (tight), Red (insufficient)
+
+**2. Smart Mode Suggestions**
+- When user selects high pages + constrained digit range:
+ - "This configuration has limited unique problems. Consider using Smart Mode for auto-scaled difficulty."
+
+**3. Download-Time Warning (Last Chance)**
+- If user dismisses warning and clicks Download with extreme risk:
+ - Modal: "Are you sure? This will produce mostly duplicates. Continue anyway?"
+
+**4. Mixed Mode Validation**
+- Currently skipped for mastery+mixed
+- Could estimate: `additionSpace / 2 + subtractionSpace / 2` (rough approximation)
+- Or: "Mixed mastery mode - problem space not validated"
+
+**5. Tooltip on Regrouping Slider**
+- "100% regrouping with 1-digit problems severely limits unique combinations (only 45 possible)"
+- Show when `digitRange.max === 1 && pAnyStart > 0.8`
+
+---
+
+## Code Organization
+
+### Main Files
+
+**`problemGenerator.ts`** (1,130 lines)
+- All problem generation logic (addition, subtraction, mixed)
+- Generate-all vs retry strategy selection
+- Regrouping/borrowing counting
+- Deterministic PRNG (Mulberry32)
+- Mixed mode (manual and mastery)
+
+**`utils/validateProblemSpace.ts`** (200 lines)
+- Problem space estimation
+- Duplicate risk calculation
+- Warning message generation
+- Validation logic
+
+**`components/worksheet-preview/WorksheetPreviewContext.tsx`** (85 lines)
+- Validation triggering on config changes
+- Warning state management
+- Dismiss state tracking
+
+**`components/worksheet-preview/DuplicateWarningBanner.tsx`** (147 lines)
+- Warning UI display
+- Collapsible details
+- Dismiss button
+
+### Key Functions
+
+**Addition:**
+- `generateProblems()` - Main entry point
+- `generateAllAdditionProblems()` - Enumerate all valid problems
+- `generateNonRegroup()` - No carries
+- `generateOnesOnly()` - Carry in ones only
+- `generateBoth()` - Multiple carries
+- `countRegroupingOperations()` - Difficulty scoring
+
+**Subtraction:**
+- `generateSubtractionProblems()` - Main entry point
+- `generateAllSubtractionProblems()` - Enumerate all valid problems
+- `generateNonBorrow()` - No borrows
+- `generateOnesOnlyBorrow()` - Borrow in ones only
+- `generateBothBorrow()` - Multiple borrows
+- `countBorrows()` - Simulate subtraction algorithm
+
+**Mixed:**
+- `generateMixedProblems()` - Manual mode 50/50 split
+- `generateMasteryMixedProblems()` - Separate skill-based configs
+
+**Validation:**
+- `estimateUniqueProblemSpace()` - Exact or heuristic estimation
+- `validateProblemSpace()` - Duplicate risk analysis
+
+---
+
+## Testing Considerations
+
+### Unit Tests Needed
+
+**Problem space estimation:**
+- 1-digit addition: Exactly 45 regrouping, 55 non-regrouping
+- 1-digit subtraction: Validate borrow counts
+- Mixed mode: Validate combined space estimation
+
+**Generation strategy selection:**
+- Verify generate-all used for spaces < 10K
+- Verify retry-based used for spaces ≥ 10K
+
+**Cycling behavior:**
+- Request 100 problems from 45-problem space
+- Verify first 45 are unique
+- Verify next 45 repeat the sequence (non-interpolate)
+- Verify progressive difficulty maintained across cycles (interpolate)
+
+**Edge cases:**
+- Single-digit 100% regrouping
+- Subtraction multiple borrows with 2-digit max
+- Borrowing across zeros (1000 - 1)
+- Mixed mastery mode (skip validation)
+
+### Integration Tests
+
+**End-to-end worksheet generation:**
+1. Configure: 1-digit, 100% regrouping, 100 problems
+2. Generate worksheet
+3. Verify warning shown
+4. Verify 45 unique problems + 55 repeats
+5. Download PDF, verify renders correctly
+
+---
+
+## Future Improvements
+
+### 1. Better Duplicate Handling for Interpolate Mode
+
+**Current:** Clears "seen" set and restarts cycle
+**Better:** Shuffle the sorted array between cycles to get different difficulty ordering
+
+### 2. Subtraction Generate-All Missing Interpolate
+
+**Current:** Subtraction doesn't support generate-all + interpolate
+**Code:** `subtractionGenerator.ts:863` has `&& !interpolate` check
+**Fix:** Implement same difficulty sorting as addition
+
+### 3. More Granular Difficulty Levels
+
+**Current:** 3 categories (non, onesOnly, both)
+**Better:** Score by exact carry/borrow count (0, 1, 2, 3+) for finer difficulty curve
+
+### 4. Problem Space Caching
+
+**Current:** Re-estimates on every config change
+**Better:** Cache estimated spaces for common configurations
+
+### 5. User Education
+
+**Current:** Technical warnings with math jargon
+**Better:** Simpler language, visual indicators, examples
+
+---
+
+## Glossary
+
+**Regrouping (Addition):** Carrying a value to the next place value when column sum ≥ 10
+
+**Borrowing (Subtraction):** Taking from a higher place value when minuend digit < subtrahend digit
+
+**Problem Space:** The set of all unique problems possible given constraints
+
+**Generate-All:** Strategy that enumerates all valid problems upfront, then shuffles
+
+**Retry-Based:** Strategy that randomly generates problems and retries on duplicates
+
+**Interpolation:** Gradual difficulty increase from start to end of worksheet
+
+**Cycle:** Repetition of the entire problem set when requesting more problems than exist
+
+**pAnyStart:** Target probability that any regrouping/borrowing occurs
+
+**pAllStart:** Target probability of multiple regrouping/borrowing operations
+
+**Commutative:** Order doesn't matter (addition: 2+3 = 3+2)
+
+**Non-commutative:** Order matters (subtraction: 5-3 ≠ 3-5)
diff --git a/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md b/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md
new file mode 100644
index 00000000..3f8a4691
--- /dev/null
+++ b/apps/web/src/app/create/worksheets/USER_WARNING_IMPROVEMENTS.md
@@ -0,0 +1,535 @@
+# User Warning Improvements for Problem Space Constraints
+
+## Current Implementation
+
+### Warning Banner (Implemented)
+
+**Location:** Preview pane, centered overlay
+**Component:** `DuplicateWarningBanner.tsx`
+**Trigger:** When `duplicateRisk !== 'none'` (ratio ≥ 0.3)
+
+**Strengths:**
+- ✅ Visible and prominent
+- ✅ Dismissable
+- ✅ Shows in preview (where user sees the actual worksheet)
+- ✅ Provides actionable recommendations
+- ✅ Collapsible details for advanced users
+
+**Weaknesses:**
+- ❌ Reactive (shown after user has configured)
+- ❌ Can be dismissed and forgotten
+- ❌ Not shown in mastery+mixed mode
+- ❌ No visual feedback in config panel
+- ❌ Requires user to generate preview first
+
+---
+
+## Recommended Improvements
+
+### 1. Proactive Config Panel Indicator (HIGH PRIORITY)
+
+**Where:** Next to pages/problemsPerPage sliders in ConfigPanel
+**When:** Live update as user adjusts settings
+**Why:** Prevents users from creating invalid configs in the first place
+
+#### Design
+
+```typescript
+interface ProblemSpaceIndicator {
+ estimatedSpace: number
+ requestedProblems: number
+ status: 'plenty' | 'tight' | 'insufficient'
+ color: 'green' | 'yellow' | 'red'
+}
+```
+
+**Visual appearance:**
+
+```
+┌─ Problems Per Page ────────────────┐
+│ [──────●──────] 20 │
+│ │
+│ Problem Space: ~4,050 available │ ← Green text
+│ ✓ Plenty of unique problems │
+└────────────────────────────────────┘
+
+┌─ Problems Per Page ────────────────┐
+│ [──────────●──────] 50 │
+│ │
+│ Problem Space: ~45 available │ ← Yellow text
+│ ⚠ Limited unique problems │
+└────────────────────────────────────┘
+
+┌─ Problems Per Page ────────────────┐
+│ [────────────●──] 100 │
+│ │
+│ Problem Space: ~45 available │ ← Red text
+│ ✕ Insufficient - duplicates likely │
+│ → Try increasing digit range │
+└────────────────────────────────────┘
+```
+
+**Implementation:**
+
+```typescript
+// In ConfigPanel component
+const estimatedSpace = useMemo(() => {
+ return estimateUniqueProblemSpace(
+ formState.digitRange,
+ formState.pAnyStart,
+ formState.operator
+ )
+}, [formState.digitRange, formState.pAnyStart, formState.operator])
+
+const requestedProblems = formState.problemsPerPage * formState.pages
+const ratio = requestedProblems / estimatedSpace
+
+const spaceIndicator: ProblemSpaceIndicator = {
+ estimatedSpace,
+ requestedProblems,
+ status: ratio < 0.5 ? 'plenty' : ratio < 0.8 ? 'tight' : 'insufficient',
+ color: ratio < 0.5 ? 'green' : ratio < 0.8 ? 'yellow' : 'red'
+}
+```
+
+**Files to modify:**
+- `components/config-panel/ConfigPanel.tsx` (or respective sections)
+- Possibly create `components/config-panel/ProblemSpaceIndicator.tsx`
+
+---
+
+### 2. Slider Constraints with Visual Feedback (MEDIUM PRIORITY)
+
+**Where:** Pages and problemsPerPage sliders
+**When:** User drags slider past recommended limits
+**Why:** Prevents invalid configurations while allowing override
+
+#### Design
+
+**Visual feedback:**
+- Green track: Safe range (0-50% of space)
+- Yellow track: Caution range (50-80% of space)
+- Red track: Over limit (80%+ of space)
+
+**Dynamic max values:**
+- Suggest max pages based on current settings
+- Show "soft limit" vs "hard limit"
+- Allow override with confirmation
+
+**Example:**
+
+```
+┌─ Pages ────────────────────────────┐
+│ [──●──|───────] 2 pages │ ← Slider in yellow zone
+│ ↑ │
+│ Recommended max: 2 │
+│ (45 unique problems available) │
+│ │
+│ [Continue anyway] [Reduce to 1] │
+└────────────────────────────────────┘
+```
+
+**Implementation:**
+
+```typescript
+const recommendedMaxPages = Math.floor((estimatedSpace * 0.5) / problemsPerPage)
+
+// Slider shows visual zones
+
+```
+
+---
+
+### 3. Smart Mode Suggestion (MEDIUM PRIORITY)
+
+**Where:** Config panel when user selects constrained settings
+**When:** High pages + constrained digit range + manual mode
+**Why:** Educate users about Smart Mode's auto-scaling benefits
+
+#### Design
+
+```
+┌─ Mode Selection ──────────────────────────────────┐
+│ ○ Smart Mode (Recommended for varied difficulty) │
+│ ● Manual Mode │
+│ │
+│ 💡 Tip: Smart Mode automatically scales │
+│ difficulty and maximizes problem variety │
+│ [Switch to Smart Mode] │
+└───────────────────────────────────────────────────┘
+```
+
+**Trigger conditions:**
+- Manual mode selected
+- Pages > 2
+- Digit range narrow (min === max)
+- High regrouping probability (pAnyStart > 0.8)
+- Duplicate risk >= medium
+
+**Implementation:**
+
+```typescript
+const shouldSuggestSmartMode =
+ formState.mode === 'manual' &&
+ formState.pages > 2 &&
+ formState.digitRange.min === formState.digitRange.max &&
+ formState.pAnyStart > 0.8 &&
+ duplicateRisk >= 'medium'
+
+{shouldSuggestSmartMode && (
+ setMode('smart')} />
+)}
+```
+
+---
+
+### 4. Download-Time Confirmation (LOW PRIORITY)
+
+**Where:** Modal before generating PDF
+**When:** User dismissed warning AND extreme duplicate risk
+**Why:** Last chance to prevent user frustration
+
+#### Design
+
+```
+┌─ Confirm Download ─────────────────────────────┐
+│ │
+│ ⚠️ Warning: Duplicate Problems Detected │
+│ │
+│ Your configuration will produce: │
+│ • 200 requested problems │
+│ • Only 45 unique problems available │
+│ • ~155 duplicates (78% of worksheet) │
+│ │
+│ This may not provide enough practice variety. │
+│ │
+│ Recommendations: │
+│ • Reduce to 1-2 pages │
+│ • Increase digit range from 1 to 2 │
+│ • Lower regrouping to 50% │
+│ │
+│ [Go Back] [Download Anyway] │
+└─────────────────────────────────────────────────┘
+```
+
+**Trigger conditions:**
+- User clicked Download
+- Duplicate risk is extreme (ratio >= 1.5)
+- Warning was previously dismissed (or never shown)
+
+**Implementation:**
+
+```typescript
+// In PreviewCenter.tsx handleGenerate
+const handleGenerate = async () => {
+ if (duplicateRisk === 'extreme' && (isDismissed || !warningsShown)) {
+ setShowDownloadConfirmModal(true)
+ return
+ }
+
+ await onGenerate()
+}
+```
+
+---
+
+### 5. Tooltip on Regrouping Slider (LOW PRIORITY)
+
+**Where:** Regrouping probability slider
+**When:** Hover or focus
+**Why:** Contextual education about regrouping constraints
+
+#### Design
+
+```
+┌─ Regrouping Probability ──────────────────────┐
+│ [────────────────●] 100% ⓘ │ ← Hover for tooltip
+└────────────────────────────────────────────────┘
+
+Tooltip appears:
+┌────────────────────────────────────────────┐
+│ 100% Regrouping with 1-Digit Problems │
+│ │
+│ This limits unique problems to only 45. │
+│ Consider: │
+│ • Reducing to 50% regrouping │
+│ • Increasing to 2-digit problems │
+└────────────────────────────────────────────┘
+```
+
+**Conditional display:**
+- Only show warning tooltip when:
+ - `digitRange.max === 1`
+ - `pAnyStart > 0.8`
+
+**Implementation:**
+
+```typescript
+const showRegroupingWarning =
+ formState.digitRange.max === 1 && formState.pAnyStart > 0.8
+
+
+ ) : undefined}
+/>
+```
+
+---
+
+### 6. Digit Range Recommendations (MEDIUM PRIORITY)
+
+**Where:** Digit range selector
+**When:** User selects 1-digit with high pages count
+**Why:** Proactive suggestion before problem space constraint hits
+
+#### Design
+
+```
+┌─ Digit Range ─────────────────────────────────┐
+│ Min: [1▼] Max: [1▼] │
+│ │
+│ ℹ️ 1-digit problems have limited variety │
+│ For 5+ pages, consider: │
+│ • Min: 1, Max: 2 (mixed 1-2 digit) │
+│ • Min: 2, Max: 2 (all 2-digit) │
+│ │
+│ [Quick Apply: 1-2 digits] │
+└────────────────────────────────────────────────┘
+```
+
+**Trigger conditions:**
+- `digitRange.max === 1`
+- `pages >= 5`
+
+**Implementation:**
+
+```typescript
+const shouldSuggestDigitRangeIncrease =
+ formState.digitRange.max === 1 && formState.pages >= 5
+
+{shouldSuggestDigitRangeIncrease && (
+ setDigitRange({ min: 1, max: 2 })}
+ />
+)}
+```
+
+---
+
+### 7. Mixed Mode Mastery Validation (LOW PRIORITY)
+
+**Where:** Preview banner or config panel
+**When:** Mastery + mixed mode selected
+**Why:** Currently shows no validation, which could be confusing
+
+#### Design
+
+**Option A: Simple info message**
+```
+ℹ️ Mixed Mastery Mode
+Problem space not validated (uses separate skill configs for +/−)
+```
+
+**Option B: Rough estimation**
+```
+ℹ️ Mixed Mastery Mode
+~2,025 addition problems + ~550 subtraction problems available
+(Separate configs - validation approximate)
+```
+
+**Trigger conditions:**
+- `mode === 'mastery'`
+- `operator === 'mixed'`
+
+**Implementation:**
+
+Currently skipped in `WorksheetPreviewContext.tsx:53-56`.
+
+Two approaches:
+
+**Approach 1 - Info Only:**
+```typescript
+if (mode === 'mastery' && operator === 'mixed') {
+ setWarnings([
+ 'ℹ️ Mixed Mastery Mode uses separate skill-based configs for addition and subtraction. Problem space validation is disabled.'
+ ])
+ return
+}
+```
+
+**Approach 2 - Rough Estimation:**
+```typescript
+if (mode === 'mastery' && operator === 'mixed') {
+ // Get separate estimates (need to access skill configs)
+ const addSpace = estimateUniqueProblemSpace(
+ additionSkill.digitRange,
+ additionSkill.pAnyStart,
+ 'addition'
+ )
+ const subSpace = estimateUniqueProblemSpace(
+ subtractionSkill.digitRange,
+ subtractionSkill.pAnyStart,
+ 'subtraction'
+ )
+
+ const total = addSpace + subSpace
+ const requested = problemsPerPage * pages
+
+ if (requested > total * 0.8) {
+ setWarnings([
+ `Mixed Mastery Mode: ~${addSpace} addition + ~${subSpace} subtraction problems available. Validation is approximate.`
+ ])
+ }
+ return
+}
+```
+
+---
+
+## Implementation Priority
+
+### Phase 1 - High Impact, Low Effort
+1. **Config Panel Indicator** - Shows live problem space estimate
+2. **Digit Range Recommendations** - Suggests 2-digit when user selects many 1-digit pages
+
+### Phase 2 - Medium Impact, Medium Effort
+3. **Slider Visual Feedback** - Color-coded zones for safe/caution/danger
+4. **Smart Mode Suggestion** - Educates about Smart Mode benefits
+5. **Tooltip on Regrouping Slider** - Contextual help for 1-digit + 100% regrouping
+
+### Phase 3 - Nice to Have
+6. **Download Confirmation** - Last-chance warning for extreme cases
+7. **Mixed Mastery Validation** - Rough estimation or info message
+
+---
+
+## Component Structure
+
+Suggested new components to create:
+
+```
+components/config-panel/
+├── ProblemSpaceIndicator.tsx # Live space estimate with color coding
+├── SmartModeSuggestion.tsx # Suggests switching to Smart Mode
+├── DigitRangeRecommendation.tsx # Suggests increasing digit range
+└── RegroupingConstraintTooltip.tsx # Warning tooltip for constrained settings
+
+components/modals/
+└── DownloadConfirmModal.tsx # Pre-download warning for extreme risk
+```
+
+---
+
+## User Education Opportunities
+
+### Tooltips and Help Text
+
+**Regrouping Probability:**
+```
+"The percentage of problems that involve carrying (addition) or borrowing
+(subtraction). Higher percentages with limited digit ranges may result in
+fewer unique problems."
+```
+
+**Digit Range:**
+```
+"1-digit: 0-9 (very limited variety)
+2-digit: 10-99 (good variety)
+3-digit: 100-999 (excellent variety)
+
+For worksheets with many problems, use 2+ digits."
+```
+
+**Pages:**
+```
+"Each page contains {problemsPerPage} problems.
+{estimatedSpace} unique problems available with current settings."
+```
+
+### Onboarding/Tutorial
+
+Add a brief tutorial or info modal explaining:
+- What "problem space" means
+- Why digit range matters
+- How regrouping probability affects uniqueness
+- When to use Smart Mode vs Manual Mode
+
+---
+
+## Testing Plan
+
+For each improvement:
+
+1. **Visual regression:** Screenshot before/after
+2. **Interaction testing:** Verify all states (plenty/tight/insufficient)
+3. **Edge case testing:**
+ - 1-digit 100% regrouping (45 problems)
+ - 2-digit 100% regrouping (~3,700 problems)
+ - Mixed mode with mastery
+4. **Accessibility:** Keyboard navigation, screen reader labels
+5. **Mobile responsive:** Touch-friendly, readable on small screens
+
+---
+
+## Analytics (Future Consideration)
+
+Track user behavior to measure effectiveness:
+
+```typescript
+analytics.track('Warning Shown', {
+ duplicateRisk: 'high',
+ estimatedSpace: 45,
+ requestedProblems: 100,
+ digitRange: { min: 1, max: 1 },
+ pAnyStart: 1.0
+})
+
+analytics.track('Warning Dismissed', {
+ duplicateRisk: 'high'
+})
+
+analytics.track('Config Adjusted After Warning', {
+ change: 'increased_digit_range',
+ from: { min: 1, max: 1 },
+ to: { min: 1, max: 2 }
+})
+
+analytics.track('Downloaded Despite Warning', {
+ duplicateRisk: 'extreme'
+})
+```
+
+Use this data to:
+- Identify most common problematic configurations
+- Measure warning effectiveness
+- Improve recommendation accuracy
+
+---
+
+## Summary
+
+**Current state:** Reactive warning in preview pane (good, but not enough)
+
+**Ideal state:** Multi-layered approach
+1. **Proactive** - Config panel shows live feedback
+2. **Preventive** - Visual slider constraints guide users
+3. **Educational** - Tooltips and suggestions explain why
+4. **Protective** - Last-chance confirmation for extreme cases
+
+**Impact:**
+- Fewer confused users ("why so many duplicates?")
+- Better worksheet quality
+- Reduced support requests
+- Improved user confidence in the tool
diff --git a/apps/web/src/app/create/worksheets/problemGenerator.ts b/apps/web/src/app/create/worksheets/problemGenerator.ts
index d9755472..9abe3092 100644
--- a/apps/web/src/app/create/worksheets/problemGenerator.ts
+++ b/apps/web/src/app/create/worksheets/problemGenerator.ts
@@ -333,14 +333,30 @@ function uniquePush(list: AdditionProblem[], a: number, b: number, seen: Set unique problems available):
+ * - Non-interpolate mode: Repeats entire shuffled sequence (problems 0-44, 45-89, 90+...)
+ * - Interpolate mode: Clears "seen" set after exhausting problems, maintains difficulty curve
+ *
+ * EXAMPLES:
+ * - 1-digit 100% regrouping: Only 45 unique problems exist (will cycle after problem 44)
+ * - 2-digit mixed regrouping: ~4,000 unique problems (generate-all used)
+ * - 3-digit any regrouping: ~400,000 unique problems (retry-based used)
*
* @param total Total number of problems to generate
- * @param pAnyStart Starting probability of any regrouping (0-1)
- * @param pAllStart Starting probability of multiple regrouping (0-1)
- * @param interpolate If true, difficulty increases from start to end
- * @param seed Random seed for reproducible generation
- * @param digitRange Digit range for problem numbers (V4+)
+ * @param pAnyStart Target probability of ANY regrouping occurring (0-1)
+ * 0.0 = no regrouping, 1.0 = all problems must have regrouping
+ * @param pAllStart Target probability of MULTIPLE regrouping (0-1), must be ≤ pAnyStart
+ * @param interpolate If true, difficulty increases progressively from easy→hard
+ * If false, all problems at constant difficulty
+ * @param seed Random seed for deterministic reproducible generation
+ * @param digitRange Digit range for problem operands (1-5 digits)
+ * @returns Array of addition problems matching constraints
*/
export function generateProblems(
total: number,
@@ -362,7 +378,14 @@ export function generateProblems(
const estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'addition')
console.log(`[ADD GEN] Estimated unique problem space: ${estimatedSpace} (requesting ${total})`)
- // For small problem spaces, use generate-all + shuffle approach (even with interpolate)
+ // ========================================================================
+ // STRATEGY 1: Generate-All + Shuffle (Small Problem Spaces)
+ // ========================================================================
+ // Used when estimated unique problems < 10,000
+ // Advantages:
+ // - Zero retries (no random generation needed)
+ // - Guaranteed coverage (all unique problems appear before repeating)
+ // - Deterministic (same seed = same worksheet)
const THRESHOLD = 10000
if (estimatedSpace < THRESHOLD) {
console.log(
@@ -372,8 +395,9 @@ export function generateProblems(
console.log(`[ADD GEN] Generated ${allProblems.length} unique problems`)
if (interpolate) {
- // Sort problems by difficulty (number of regrouping operations)
- // This allows us to sample from easier problems early, harder problems later
+ // ===== PROGRESSIVE DIFFICULTY MODE =====
+ // Sort all problems by difficulty (carry count), then sample based on
+ // position in worksheet to create easy→medium→hard progression
console.log(`[ADD GEN] Sorting problems by difficulty for progressive difficulty`)
const sortedByDifficulty = [...allProblems].sort((a, b) => {
const diffA = countRegroupingOperations(a.a, a.b)
@@ -387,21 +411,23 @@ export function generateProblems(
let cycleCount = 0 // Track how many times we've cycled through all problems
for (let i = 0; i < total; i++) {
+ // Calculate difficulty fraction (0.0 = easy, 1.0 = hard)
const frac = total <= 1 ? 0 : i / (total - 1)
// Map frac (0 to 1) to index in sorted array
// frac=0 (start) → sample from easy problems (low index)
// frac=1 (end) → sample from hard problems (high index)
const targetIndex = Math.floor(frac * (sortedByDifficulty.length - 1))
- // Try to get a problem near the target difficulty that we haven't used
+ // Try to get a problem near the target difficulty that we haven't used yet
let problem = sortedByDifficulty[targetIndex]
const key = `${problem.a},${problem.b}`
- // If already used, search nearby for unused problem
+ // If already used, search nearby (forward then backward) for unused problem
+ // This maintains approximate difficulty while avoiding duplicates
if (seen.has(key)) {
let found = false
for (let offset = 1; offset < sortedByDifficulty.length; offset++) {
- // Try forward first, then backward
+ // Try forward first (slightly harder), then backward (slightly easier)
for (const direction of [1, -1]) {
const idx = targetIndex + direction * offset
if (idx >= 0 && idx < sortedByDifficulty.length) {
@@ -416,15 +442,16 @@ export function generateProblems(
}
if (found) break
}
- // If still not found, we've exhausted all unique problems
- // Reset the seen set and start a new cycle
+ // If still not found, we've exhausted ALL unique problems
+ // Clear the "seen" set and start a new cycle through the sorted array
+ // This maintains the difficulty progression across cycles
if (!found) {
cycleCount++
console.log(
`[ADD GEN] Exhausted all ${sortedByDifficulty.length} unique problems at position ${i}. Starting cycle ${cycleCount + 1}.`
)
seen.clear()
- // Use the target problem for this position
+ // Use the target problem for this position (beginning of new cycle)
seen.add(key)
}
} else {
@@ -440,16 +467,22 @@ export function generateProblems(
)
return result
} else {
- // No interpolation - just shuffle and take first N
+ // ===== CONSTANT DIFFICULTY MODE =====
+ // Shuffle all problems randomly, no sorting by difficulty
const shuffled = shuffleArray(allProblems, rand)
// If we need more problems than available, cycle through the shuffled array
+ // Example: 45 unique problems, requesting 100
+ // - Problems 0-44: First complete shuffle
+ // - Problems 45-89: Second complete shuffle (same order as first)
+ // - Problems 90-99: First 10 from third shuffle
if (total > shuffled.length) {
const cyclesNeeded = Math.ceil(total / shuffled.length)
console.warn(
`[ADD GEN] Warning: Requested ${total} problems but only ${shuffled.length} unique problems exist. Will cycle ${cyclesNeeded} times.`
)
// Build result by repeating the entire shuffled array as many times as needed
+ // Using modulo ensures we cycle through: problem[0], problem[1], ..., problem[N-1], problem[0], ...
const result: AdditionProblem[] = []
for (let i = 0; i < total; i++) {
result.push(shuffled[i % shuffled.length])
@@ -461,7 +494,7 @@ export function generateProblems(
return result
}
- // Take first N problems from shuffled array
+ // Take first N problems from shuffled array (typical case: enough unique problems)
const elapsed = Date.now() - startTime
console.log(
`[ADD GEN] Complete: ${total} problems in ${elapsed}ms (0 retries, generate-all method)`
@@ -470,7 +503,12 @@ export function generateProblems(
}
}
- // For large problem spaces, use retry-based approach
+ // ========================================================================
+ // STRATEGY 2: Retry-Based Generation (Large Problem Spaces)
+ // ========================================================================
+ // Used when estimated unique problems ≥ 10,000
+ // Randomly generates problems and retries on duplicates
+ // Allows some duplicates after 100 retries to prevent infinite loops
console.log(`[ADD GEN] Using retry-based approach (space >= ${THRESHOLD})`)
const problems: AdditionProblem[] = []
const seen = new Set()
@@ -998,14 +1036,28 @@ export function generateSubtractionProblems(
}
/**
- * Generate mixed addition and subtraction problems for mastery mode
- * Uses separate configs for addition and subtraction based on current skill levels
+ * Generate mixed addition and subtraction problems for MASTERY MODE
*
- * @param count Number of problems to generate
- * @param additionConfig Configuration for addition problems (from current addition skill)
- * @param subtractionConfig Configuration for subtraction problems (from current subtraction skill)
- * @param seed Random seed
- * @returns Array of mixed problems, shuffled randomly
+ * KEY DIFFERENCES from manual mixed mode:
+ * - Uses SEPARATE skill-based configs for addition vs subtraction
+ * - Addition problems based on addition skill (e.g., 2-digit 30% regrouping)
+ * - Subtraction problems based on subtraction skill (e.g., 1-2 digit 70% borrowing)
+ * - Problems can have different difficulties within same worksheet
+ *
+ * PROBLEM SPACE VALIDATION:
+ * - Validation is SKIPPED for mastery+mixed mode (too complex with separate configs)
+ * - See WorksheetPreviewContext.tsx:53-56
+ *
+ * EXAMPLE:
+ * - Addition skill: Level 5 → {digitRange: {min:2, max:2}, pAnyStart: 0.3}
+ * - Subtraction skill: Level 3 → {digitRange: {min:1, max:2}, pAnyStart: 0.7}
+ * - Result: 50 easy 2-digit additions + 50 harder 1-2 digit subtractions, shuffled
+ *
+ * @param count Total number of problems to generate (split 50/50 between operators)
+ * @param additionConfig Difficulty config from current addition skill level
+ * @param subtractionConfig Difficulty config from current subtraction skill level
+ * @param seed Random seed for deterministic generation and shuffling
+ * @returns Array of mixed problems shuffled randomly (no interpolation in mastery mode)
*/
export function generateMasteryMixedProblems(
count: number,