feat: add problem space validation to warn about duplicate risk

Created validateProblemSpace utility to estimate unique problem count
and warn users when worksheet config will produce many duplicates.

Validation levels:
- none: < 30% of space used
- low: 30-50% (minor duplicates expected)
- medium: 50-80% (suggest reducing pages or expanding constraints)
- high: 80-150% (strong warnings with specific recommendations)
- extreme: >150% (mostly duplicates, block generation)

Provides actionable suggestions:
- Reduce page count
- Increase digit range
- Lower regrouping probability

Next: Wire this into the UI to show warnings before generation.
This commit is contained in:
Thomas Hallock 2025-11-12 07:06:07 -06:00
parent 08fef59cc5
commit 0b8c1803ff
2 changed files with 152 additions and 0 deletions

View File

@ -11,6 +11,7 @@ import {
import { getSkillById } from './skills'
import { generateTypstSource } from './typstGenerator'
import { validateWorksheetConfig } from './validation'
import { validateProblemSpace } from './utils/validateProblemSpace'
export interface PreviewResult {
success: boolean
@ -20,6 +21,7 @@ export interface PreviewResult {
endPage?: number
error?: string
details?: string
warnings?: string[] // Added for problem space validation warnings
}
/**

View File

@ -0,0 +1,150 @@
/**
* Validate that worksheet configuration has enough problem space to avoid excessive duplicates
*/
export interface ProblemSpaceValidation {
isValid: boolean
warnings: string[]
estimatedUniqueProblems: number
requestedProblems: number
duplicateRisk: 'none' | 'low' | 'medium' | 'high' | 'extreme'
}
/**
* Estimate the maximum unique problems possible given constraints
*/
function estimateUniqueProblemSpace(
digitRange: { min: number; max: number },
pAnyRegroup: number,
operator: 'addition' | 'subtraction'
): number {
const { min: minDigits, max: maxDigits } = digitRange
// Calculate approximate number space for each digit count
let totalSpace = 0
for (let digits = minDigits; digits <= maxDigits; digits++) {
// For N-digit numbers: 9 * 10^(N-1) possibilities (e.g., 2-digit = 90 numbers from 10-99)
const numbersPerDigitCount = digits === 1 ? 9 : 9 * Math.pow(10, digits - 1)
if (operator === 'addition') {
// Addition: a + b where both are in the digit range
// Rough estimate: numbersPerDigitCount^2 / 2 (since a+b = b+a, but we count both)
// Then filter by regrouping probability
const pairsForDigits = numbersPerDigitCount * numbersPerDigitCount
// If pAnyRegroup is high, only a fraction of pairs will work
// Regrouping is more common with larger digits, so this is approximate
const regroupFactor = pAnyRegroup > 0.8 ? 0.3 : pAnyRegroup > 0.5 ? 0.5 : 0.7
totalSpace += pairsForDigits * regroupFactor
} else {
// Subtraction: minuend - subtrahend where minuend > subtrahend
// About half the pairs (where minuend > subtrahend)
const pairsForDigits = (numbersPerDigitCount * numbersPerDigitCount) / 2
// Borrowing constraints reduce space similarly
const borrowFactor = pAnyRegroup > 0.8 ? 0.3 : pAnyRegroup > 0.5 ? 0.5 : 0.7
totalSpace += pairsForDigits * borrowFactor
}
}
return Math.floor(totalSpace)
}
/**
* Validate worksheet configuration for duplicate risk
* Returns warnings if configuration will likely produce many duplicates
*/
export function validateProblemSpace(
problemsPerPage: number,
pages: number,
digitRange: { min: number; max: number },
pAnyStart: number,
operator: 'addition' | 'subtraction' | 'mixed'
): ProblemSpaceValidation {
const requestedProblems = problemsPerPage * pages
const warnings: string[] = []
// For mixed mode, assume half of each
let estimatedSpace: number
if (operator === 'mixed') {
const addSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'addition')
const subSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, 'subtraction')
estimatedSpace = addSpace + subSpace
} else {
estimatedSpace = estimateUniqueProblemSpace(digitRange, pAnyStart, operator)
}
// Calculate duplicate risk
const ratio = requestedProblems / estimatedSpace
let duplicateRisk: 'none' | 'low' | 'medium' | 'high' | 'extreme'
if (ratio < 0.3) {
duplicateRisk = 'none'
} else if (ratio < 0.5) {
duplicateRisk = 'low'
warnings.push(
`You're requesting ${requestedProblems} problems, but only ~${Math.floor(estimatedSpace)} unique problems are possible with these constraints. Some duplicates may occur.`
)
} else if (ratio < 0.8) {
duplicateRisk = 'medium'
warnings.push(
`Warning: Only ~${Math.floor(estimatedSpace)} unique problems possible, but you're requesting ${requestedProblems}. Expect moderate duplicates.`
)
warnings.push(
`Suggestion: Reduce pages to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} or increase digit range to ${digitRange.max + 1}`
)
} else if (ratio < 1.5) {
duplicateRisk = 'high'
warnings.push(
`High duplicate risk! Only ~${Math.floor(estimatedSpace)} unique problems possible for ${requestedProblems} requested.`
)
warnings.push(
`Recommendations:\n` +
` • Reduce to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} pages (50% of available space)\n` +
` • Increase digit range to ${digitRange.max + 1}-${digitRange.max + 1}\n` +
` • Lower regrouping probability from ${Math.round(pAnyStart * 100)}% to 50%`
)
} else {
duplicateRisk = 'extreme'
warnings.push(
`Extreme duplicate risk! Requesting ${requestedProblems} problems but only ~${Math.floor(estimatedSpace)} unique problems exist.`
)
warnings.push(
`This configuration will produce mostly duplicate problems.`
)
warnings.push(
`Strong recommendations:\n` +
` • Reduce to ${Math.floor(estimatedSpace * 0.5 / problemsPerPage)} pages maximum\n` +
` • OR increase digit range from ${digitRange.min}-${digitRange.max} to ${digitRange.min}-${digitRange.max + 1}\n` +
` • OR reduce regrouping requirement from ${Math.round(pAnyStart * 100)}%`
)
}
// Special case: single digit with high regrouping is extremely constrained
if (digitRange.min === 1 && digitRange.max === 1 && pAnyStart > 0.8 && requestedProblems > 50) {
warnings.unshift(
`Single-digit problems (1-9) with ${Math.round(pAnyStart * 100)}% regrouping have very few unique combinations!`
)
}
return {
isValid: duplicateRisk !== 'extreme',
warnings,
estimatedUniqueProblems: Math.floor(estimatedSpace),
requestedProblems,
duplicateRisk,
}
}
/**
* Format validation result for display to user
*/
export function formatValidationWarnings(validation: ProblemSpaceValidation): string {
if (validation.warnings.length === 0) {
return ''
}
return validation.warnings.join('\n\n')
}