feat: operator-specific scaffolding for mixed mastery mode
**Problem:**
In mixed mastery mode, we were merging display rules from both skills
using "least scaffolding wins", which meant advanced subtraction would
remove scaffolding needed for beginner addition problems.
**Solution:**
Store separate display rules for each operator and apply them per-problem.
**Implementation:**
1. **Schema changes (config-schemas.ts)**:
- Added optional `additionDisplayRules` to MasteryConfig
- Added optional `subtractionDisplayRules` to MasteryConfig
- These are populated only for mastery+mixed mode
2. **Validation changes (validation.ts)**:
- For mastery+mixed: Query both skills and store their recommendedScaffolding
separately as additionDisplayRules and subtractionDisplayRules
- For single operator: Continue using displayRules as before
3. **Rendering changes (typstGenerator.ts)**:
- Check if operator-specific rules exist for mastery mode
- Addition problems (+): Use additionDisplayRules if available
- Subtraction problems (-): Use subtractionDisplayRules if available
- Fallback to displayRules if operator-specific rules not present
**Result:**
- Addition problems get scaffolding from addition skill level
- Subtraction problems get scaffolding from subtraction skill level
- Each problem type has appropriate support for its difficulty
**Example:**
- Single-digit addition (needs carry boxes) + Five-digit subtraction mastery (no scaffolding)
- Addition problems: Show carry boxes whenRegrouping
- Subtraction problems: Never show scaffolding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -65,10 +65,21 @@ function generatePageTypst(
|
||||
p.operator === '+'
|
||||
? analyzeProblem(p.a, p.b)
|
||||
: analyzeSubtractionProblem(p.minuend, p.subtrahend)
|
||||
const displayOptions = resolveDisplayForProblem(
|
||||
config.displayRules as any, // Cast for backward compatibility with configs missing new fields
|
||||
meta
|
||||
)
|
||||
|
||||
// Choose display rules based on operator (for mastery+mixed mode)
|
||||
let rulesForProblem = config.displayRules as any
|
||||
|
||||
if (config.mode === 'mastery') {
|
||||
const masteryConfig = config as any
|
||||
// If we have operator-specific rules (mastery+mixed), use them
|
||||
if (p.operator === '+' && masteryConfig.additionDisplayRules) {
|
||||
rulesForProblem = masteryConfig.additionDisplayRules
|
||||
} else if (p.operator === '-' && masteryConfig.subtractionDisplayRules) {
|
||||
rulesForProblem = masteryConfig.subtractionDisplayRules
|
||||
}
|
||||
}
|
||||
|
||||
const displayOptions = resolveDisplayForProblem(rulesForProblem, meta)
|
||||
|
||||
return {
|
||||
...p,
|
||||
@@ -91,11 +102,14 @@ function generatePageTypst(
|
||||
})
|
||||
|
||||
// DEBUG: Show first 3 problems' ten-frames status
|
||||
console.log('[TYPST DEBUG] First 3 enriched problems:', enrichedProblems.slice(0, 3).map((p, i) => ({
|
||||
index: i,
|
||||
problem: p.operator === '+' ? `${p.a} + ${p.b}` : `${p.minuend} − ${p.subtrahend}`,
|
||||
showTenFrames: p.showTenFrames,
|
||||
})))
|
||||
console.log(
|
||||
'[TYPST DEBUG] First 3 enriched problems:',
|
||||
enrichedProblems.slice(0, 3).map((p, i) => ({
|
||||
index: i,
|
||||
problem: p.operator === '+' ? `${p.a} + ${p.b}` : `${p.minuend} − ${p.subtrahend}`,
|
||||
showTenFrames: p.showTenFrames,
|
||||
}))
|
||||
)
|
||||
|
||||
// Generate Typst problem data with per-problem display flags
|
||||
const problemsTypst = enrichedProblems
|
||||
|
||||
@@ -169,7 +169,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
const operator = formState.operator ?? 'addition'
|
||||
|
||||
if (operator === 'mixed') {
|
||||
// Mixed mode: Merge scaffolding from both skills
|
||||
// Mixed mode: Store SEPARATE display rules for each operator
|
||||
// The typstGenerator will choose which rules to apply per-problem
|
||||
const addSkillId = formState.currentAdditionSkillId
|
||||
const subSkillId = formState.currentSubtractionSkillId
|
||||
|
||||
@@ -178,31 +179,8 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
const subSkill = getSkillById(subSkillId as any)
|
||||
|
||||
if (addSkill?.recommendedScaffolding && subSkill?.recommendedScaffolding) {
|
||||
// Use the LEAST scaffolding from both (most restrictive)
|
||||
// This ensures mastery-level problems have minimal scaffolding
|
||||
baseDisplayRules = {
|
||||
// Take 'never' if either skill recommends it
|
||||
carryBoxes:
|
||||
addSkill.recommendedScaffolding.carryBoxes === 'never' ||
|
||||
subSkill.recommendedScaffolding.carryBoxes === 'never'
|
||||
? 'never'
|
||||
: 'whenRegrouping',
|
||||
answerBoxes:
|
||||
addSkill.recommendedScaffolding.answerBoxes === 'never' ||
|
||||
subSkill.recommendedScaffolding.answerBoxes === 'never'
|
||||
? 'never'
|
||||
: 'always',
|
||||
placeValueColors:
|
||||
addSkill.recommendedScaffolding.placeValueColors === 'never' ||
|
||||
subSkill.recommendedScaffolding.placeValueColors === 'never'
|
||||
? 'never'
|
||||
: addSkill.recommendedScaffolding.placeValueColors,
|
||||
tenFrames: 'never', // Always off for mastery
|
||||
problemNumbers: 'always',
|
||||
cellBorders: 'always',
|
||||
borrowNotation: subSkill.recommendedScaffolding.borrowNotation,
|
||||
borrowingHints: 'never',
|
||||
}
|
||||
// Store both separately - will be used per-problem in typstGenerator
|
||||
// Note: This will be added to the config below as additionDisplayRules/subtractionDisplayRules
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -226,7 +204,9 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
...((formState.displayRules as any) ?? {}), // Override with provided rules if any
|
||||
}
|
||||
|
||||
config = {
|
||||
// Build config with operator-specific display rules for mixed mode
|
||||
const operator = formState.operator ?? 'addition'
|
||||
const baseConfig = {
|
||||
version: 4,
|
||||
mode: mode as 'smart' | 'mastery', // Preserve the actual mode
|
||||
displayRules,
|
||||
@@ -234,6 +214,31 @@ export function validateWorksheetConfig(formState: WorksheetFormState): Validati
|
||||
currentStepId: formState.currentStepId, // Mastery progression tracking
|
||||
...sharedFields,
|
||||
}
|
||||
|
||||
// Add operator-specific display rules for mastery+mixed mode
|
||||
if (mode === 'mastery' && operator === 'mixed') {
|
||||
const addSkillId = formState.currentAdditionSkillId
|
||||
const subSkillId = formState.currentSubtractionSkillId
|
||||
|
||||
if (addSkillId && subSkillId) {
|
||||
const addSkill = getSkillById(addSkillId as any)
|
||||
const subSkill = getSkillById(subSkillId as any)
|
||||
|
||||
if (addSkill?.recommendedScaffolding && subSkill?.recommendedScaffolding) {
|
||||
config = {
|
||||
...baseConfig,
|
||||
additionDisplayRules: { ...addSkill.recommendedScaffolding },
|
||||
subtractionDisplayRules: { ...subSkill.recommendedScaffolding },
|
||||
} as any
|
||||
} else {
|
||||
config = baseConfig as any
|
||||
}
|
||||
} else {
|
||||
config = baseConfig as any
|
||||
}
|
||||
} else {
|
||||
config = baseConfig as any
|
||||
}
|
||||
} else {
|
||||
// Manual mode: Use boolean flags for uniform display
|
||||
config = {
|
||||
|
||||
@@ -400,6 +400,130 @@ const additionConfigV4MasterySchema = additionConfigV4BaseSchema.extend({
|
||||
]),
|
||||
}),
|
||||
|
||||
// Optional: Separate display rules for mixed mode (operator-specific scaffolding)
|
||||
// When operator='mixed', additionDisplayRules applies to addition problems,
|
||||
// subtractionDisplayRules applies to subtraction problems
|
||||
additionDisplayRules: z
|
||||
.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
})
|
||||
.optional(),
|
||||
subtractionDisplayRules: z
|
||||
.object({
|
||||
carryBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
answerBoxes: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
placeValueColors: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
tenFrames: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
problemNumbers: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
cellBorders: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowNotation: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
borrowingHints: z.enum([
|
||||
'always',
|
||||
'never',
|
||||
'whenRegrouping',
|
||||
'whenMultipleRegroups',
|
||||
'when3PlusDigits',
|
||||
]),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
// Optional: Current step in mastery progression path
|
||||
currentStepId: z.string().optional(),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user