Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 8x 8x 8x 8x 8x 6x 6x 8x 8x 8x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x 2x | /**
* Slot Distribution Configuration
*
* Controls how problems are distributed across purposes (focus, reinforce, review, challenge)
* and how session time is divided across part types (abacus, visualization, linear).
*/
import type { SessionPartType } from '@/db/schema/session-plans'
// =============================================================================
// Problem Purpose Distribution
// =============================================================================
/**
* Base weights for slot purposes (should sum to 1.0 before challenge adjustment).
*
* These weights apply to the non-challenge portion of each part.
* Challenge slots are allocated FIRST based on challengeRatioByPartType,
* then remaining slots are distributed using these weights.
*
* - focus: Current phase skill practice (most important)
* - reinforce: Skills flagged as needing reinforcement
* - review: Spaced repetition of mastered skills
*/
export const PURPOSE_WEIGHTS = {
/** Primary practice on current curriculum focus */
focus: 0.6,
/** Reinforce struggling skills */
reinforce: 0.2,
/** Spaced repetition of mastered skills */
review: 0.15,
// Note: Challenge slots are allocated using CHALLENGE_RATIO_BY_PART_TYPE (per part type),
// not a fixed weight. See session-planner.ts for the allocation logic.
} as const
/**
* Challenge problem ratios by part type.
*
* Different part types have different challenge densities:
* - abacus: Higher ratio (25%) - physical abacus reduces cognitive load
* - visualization: Lower ratio (15%) - mental math is harder
* - linear: Medium ratio (20%) - middle ground
*
* Challenge slots are allocated FIRST, then remaining slots use PURPOSE_WEIGHTS.
*/
export const CHALLENGE_RATIO_BY_PART_TYPE: Record<SessionPartType, number> = {
/** ~2-3 challenge problems per 10 (physical abacus makes harder problems manageable) */
abacus: 0.25,
/** ~1-2 challenge problems per 10 (mental math is harder, fewer challenges) */
visualization: 0.15,
/** ~2 challenge problems per 10 (middle ground) */
linear: 0.2,
} as const
// =============================================================================
// Session Part Time Distribution
// =============================================================================
/**
* How session time is distributed across part types (should sum to 1.0).
*
* Part 1 (abacus): 50% - This is where new skills are built
* Part 2 (visualization): 30% - Mental math with visualization
* Part 3 (linear): 20% - Mental math in sentence format
*/
export const PART_TIME_WEIGHTS: Record<SessionPartType, number> = {
abacus: 0.5,
visualization: 0.3,
linear: 0.2,
} as const
// =============================================================================
// Term Count Ranges
// =============================================================================
/**
* How many terms (numbers) per problem for each part type.
* More terms = more complex multi-step problems.
*
* Example: { min: 3, max: 6 } means problems like "23 + 14 + 8" to "12 + 5 + 9 + 3 + 7 + 2"
*
* @deprecated Use `TERM_COUNT_SCALING` from `./term-count-scaling` instead.
* Static ranges are now replaced by dynamic comfort-based scaling.
* These values are still used as fallback defaults in PlanGenerationConfig.
*/
export const TERM_COUNT_RANGES: Record<SessionPartType, { min: number; max: number } | null> = {
/** Base term count - physical abacus allows more complex problems */
abacus: { min: 3, max: 6 },
/** null = derive from abacus (typically 75% of abacus range) */
visualization: null,
/** null = same as abacus by default */
linear: null,
} as const
/**
* Get effective term count range for a part type.
* Falls back to abacus range (or adjusted) if null.
*
* @deprecated Use `computeTermCountRange()` from `./term-count-scaling` instead.
* This returns static ranges; the new function uses dynamic comfort-based scaling.
*/
export function getTermCountRange(partType: SessionPartType): {
min: number
max: number
} {
const explicit = TERM_COUNT_RANGES[partType]
if (explicit) return explicit
// Fall back to abacus range
const abacusRange = TERM_COUNT_RANGES.abacus ?? { min: 3, max: 6 }
if (partType === 'visualization') {
// Visualization uses 75% of abacus range (mental math is harder)
return {
min: Math.max(2, Math.floor(abacusRange.min * 0.75)),
max: Math.max(3, Math.floor(abacusRange.max * 0.75)),
}
}
// Linear uses same as abacus
return abacusRange
}
export type PurposeWeights = typeof PURPOSE_WEIGHTS
export type PartTimeWeights = typeof PART_TIME_WEIGHTS
|