# Old Scheduler Logic (Before Focus Set Redesign)

This document captures the original problem picking logic before the Focus Set model redesign.

## Overview

The old scheduler used a group-based progression system with complex state management.

## Problem Status Flow

```
new → learning → review → relearning → mastered
                   ↑___________|
```

**Statuses:**
- `new`: Never seen
- `learning`: Early stage, < 2 successes
- `review`: In spaced repetition review
- `relearning`: Failed after being in review
- `mastered`: Fully learned (memoryStrength >= 0.85, consecutiveCorrect >= 4, difficulty <= 0.35)

## Group Management

Groups had their own states:
- `locked`: Not yet accessible
- `active`: Currently learning
- `unlocked`: Available but not primary
- `completed`: Mastered

**Group Unlock Conditions:**
- All problems introduced (introducedCount === totalProblems)
- Mastery score >= 0.78
- Stable fraction >= 0.8

**Mastery Score Calculation:**
```js
masteryScore = 0.7 * (stableCount / introducedCount) + 0.3 * avgMemoryStrength
```

**Stability Criteria:**
- consecutiveCorrect >= 2
- memoryStrength >= 0.6
- p_correct >= 0.8
- lastResult !== "wrong"

## p_correct Calculation

Used a sigmoid function with multiple factors:

```js
rawScore =
  2.2 * memoryStrength +
  -1.4 * difficulty +
  -0.35 * log(1 + hoursAgo) +
  0.22 * min(consecutiveCorrect, 5) +
  -0.45 * recentFailureFlag +
  -0.18 * sameDayRepeatPenalty +
  -0.1 * speedPenalty

p_correct = sigmoid(rawScore)
```

Special cases:
- `status === "new"`: p_correct = 0.35
- `status === "learning" && successCount === 0`: p_correct = 0.45

## Session Building

### Candidate Pools

1. **Due Review** - Problems with dueAt <= now, not new/mastered
2. **Retry** - Recently wrong (within retryDelayMinutes), consecutiveWrong <= 2
3. **New** - From active group, never seen
4. **Booster** - Not due yet but weak (p_correct between 0.55 and 0.80)

### Quota Calculation

```js
baseNewFraction = mode.newFraction  // 0.15-0.25 depending on mode
reviewBacklog = duePool.length + retryPool.length

if (reviewBacklog >= 0.8 * sessionSize) newQuota = 0
else if (reviewBacklog >= 0.5 * sessionSize) newQuota = 0.5 * baseNewFraction * sessionSize
else newQuota = baseNewFraction * sessionSize

retryQuota = min(retryPool.length, 0.15 * sessionSize)
boosterQuota = 0.05 * sessionSize
reviewQuota = sessionSize - newQuota - retryQuota - boosterQuota
```

### Scoring Formula

```js
finalScore =
  0.3 * urgency +
  0.3 * learningGain +
  0.25 * moraleFit -
  0.25 * frustrationRisk +
  0.1 * diversityBonus +
  retryBonus +
  noveltyBonus
```

**Component Calculations:**

Urgency:
```js
hoursOverdue = (now - dueAt) / 3600000  // if dueAt < now
overdueFactor = 1 - exp(-hoursOverdue / 24)
urgency = 0.6 * (1 - p_correct) + 0.4 * overdueFactor
```

Learning Gain (bell curve, peaks at 0.65):
```js
diff = p_correct - 0.65
learningGain = exp(-(diff * diff) / (2 * 0.18 * 0.18))
```

Frustration Risk:
```js
raw = (0.55 - p_correct) / 0.55
frustrationRisk = clamp(raw, 0, 1)^2
```

Morale Fit (bell curve around target accuracy):
```js
diff = p_correct - targetAccuracy
moraleFit = exp(-(diff * diff) / (2 * 0.12 * 0.12))
```

## Mode Configuration

| Mode | Target Accuracy | Min p_correct | Max Hard Fraction | New Fraction |
|------|----------------|---------------|-------------------|--------------|
| confidence | 0.88 | 0.55 | 0.1 | 0.15 |
| standard | 0.78 | 0.4 | 0.2 | 0.2 |
| cram | 0.68 | 0.3 | 0.3 | 0.25 |

## State Update After Answer

### Grade Determination
- wrong → "again"
- correct, < 2500ms → "easy"
- correct, < 5000ms → "good"
- correct, >= 5000ms → "hard"

### Grade Effects
```js
again: { deltaMemory: -0.18, deltaDifficulty: +0.10, next: 10 minutes }
hard:  { deltaMemory: +0.05, deltaDifficulty: +0.03, next: 12 hours }
good:  { deltaMemory: +0.12, deltaDifficulty: -0.02, next: interval * 1.8 }
easy:  { deltaMemory: +0.18, deltaDifficulty: -0.05, next: interval * 2.5 }
```

### Memory Strength Update
```js
newMemoryStrength = clamp(
  memoryStrength + deltaMemory + 0.03 * min(consecutiveCorrect, 3) - 0.05 * consecutiveWrong,
  0.05,
  0.98
)
```

### Next Interval Calculation (for correct answers)

Early learning (successCount < 2):
- hard → 12 hours
- good → 1 day
- easy → 2 days

Review stage:
```js
baseIntervalDays = 1 + 6 * memoryStrength * (1 - 0.6 * difficulty)
multiplier = { hard: 1.2, good: 1.8, easy: 2.5 }
nextIntervalDays = max(0.5, baseIntervalDays * multiplier)
```

Wrong answer:
```js
minutes = min(30, 5 * consecutiveWrong)
dueAt = now + max(10, minutes) minutes
```

## API Endpoints

### POST /api/next
Returns next problem based on:
1. Retry pool (recently wrong, after delay)
2. Easy review (if consecutiveWrong >= 2)
3. Due review items
4. New items from active group
5. Scored selection with 70% preference for due reviews

### POST /api/answer
Records answer, updates state, logs review, updates group progress.

### GET /api/stats
Returns total, introduced, mastered, due counts and today's accuracy.

### POST /api/session
Creates a new session record.
