fix: remove redundant 'Teens minus singles' subtraction skill

Remove the sd-sub-borrow skill as it was redundant with the existing
"Two-digit with ones place borrowing" skill which naturally covers
problems like 52-17, 43-18, etc.

The skill was problematic because:
- digitRange: { min: 1, max: 1 } constrained both operands to single digits
- But the description "13-7, 15-8" implied 2-digit minus 1-digit
- This contradiction made it impossible to generate appropriate problems
- Students were seeing either 0% or 100% of the intended pattern

Rather than fix the complex asymmetric digit range logic, we're removing
the skill entirely. The progression now flows:
- sd-sub-no-borrow (single-digit without borrowing)
- td-sub-no-borrow (two-digit without borrowing)
- td-sub-ones-borrow (two-digit with ones place borrowing)

This provides a cleaner, more natural progression.

Changes:
- Remove sd-sub-borrow skill definition from skills.ts
- Remove 'sd-sub-borrow' from SkillId type union
- Update td-sub-no-borrow prerequisites to reference sd-sub-no-borrow
- Remove sd-sub-borrow from skillMigration.ts mapping
- Remove generateTeensMinusSingles() function from problemGenerator.ts
- Revert generateOnesOnlyBorrow() to standard logic

Also includes previous fixes:
- Fix AllSkillsModal tab button types to prevent modal closing
- Add operator-specific display rules for mixed mode
- Add borrowNotation and borrowingHints to displayRules schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-10 15:47:04 -06:00
parent dffe305215
commit e156e870df
8 changed files with 291 additions and 35 deletions

View File

@ -40,11 +40,15 @@
"mcp__sqlite__read_query",
"Bash(cat:*)",
"Bash(npm run lint:*)",
"Bash(git reset:*)"
"Bash(git reset:*)",
"Bash(npx tsx:*)",
"Bash(npx tsc:*)"
],
"deny": [],
"ask": []
},
"enableAllProjectMcpServers": true,
"enabledMcpjsonServers": ["sqlite"]
"enabledMcpjsonServers": [
"sqlite"
]
}

View File

@ -32,6 +32,8 @@ const examples = [
tenFrames: 'always' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
},
},
@ -49,6 +51,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
},
},
@ -66,6 +70,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
},
},
@ -83,6 +89,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
},
},

View File

@ -32,6 +32,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@ -55,6 +57,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@ -79,6 +83,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@ -103,6 +109,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@ -127,6 +135,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: false,
showBorrowingHints: false,
@ -151,6 +161,8 @@ const examples = [
tenFrames: 'never' as const,
problemNumbers: 'always' as const,
cellBorders: 'always' as const,
borrowNotation: 'never' as const,
borrowingHints: 'never' as const,
},
showBorrowNotation: true,
showBorrowingHints: false,

View File

@ -333,4 +333,262 @@ describe('Ten-frames rendering', () => {
expect(firstPage).toContain('showTenFrames: false')
})
})
describe('Mixed mode operator-specific scaffolding', () => {
it('should apply additionDisplayRules to addition problems in mixed mode', () => {
const config: WorksheetConfig = {
version: 4,
mode: 'mastery',
problemsPerPage: 4,
cols: 2,
pages: 1,
total: 4,
rows: 2,
orientation: 'portrait',
name: 'Test Student',
date: '2025-11-10',
seed: 12345,
fontSize: 12,
digitRange: { min: 2, max: 2 },
operator: 'mixed',
pAnyStart: 1.0,
pAllStart: 0,
// Default display rules (not used for operator-specific problems)
displayRules: {
carryBoxes: 'never',
answerBoxes: 'never',
placeValueColors: 'never',
tenFrames: 'never',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
// Operator-specific rules
additionDisplayRules: {
carryBoxes: 'whenRegrouping',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'whenRegrouping', // ← Addition should show ten-frames
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
subtractionDisplayRules: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'never',
tenFrames: 'never', // ← Subtraction should NOT show ten-frames
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'whenRegrouping',
borrowingHints: 'never',
},
interpolate: false,
page: { wIn: 8.5, hIn: 11 },
margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5 },
} as any // Cast to any to allow operator-specific rules
const problems: WorksheetProblem[] = [
{ operator: 'add', a: 45, b: 27 }, // Addition with regrouping
{ operator: 'sub', minuend: 52, subtrahend: 18 }, // Subtraction with borrowing
]
const typstPages = generateTypstSource(config, problems)
const firstPage = typstPages[0]
// Should contain both showTenFrames: true and showTenFrames: false
expect(firstPage).toContain('showTenFrames: true') // Addition
expect(firstPage).toContain('showTenFrames: false') // Subtraction
// Verify operator assignment (Typst uses "+" and "" display characters)
expect(firstPage).toContain('operator: "+"')
expect(firstPage).toContain('operator: ""')
})
it('should apply subtractionDisplayRules to subtraction problems in mixed mode', () => {
const config: WorksheetConfig = {
version: 4,
mode: 'mastery',
problemsPerPage: 2,
cols: 1,
pages: 1,
total: 2,
rows: 2,
orientation: 'portrait',
name: 'Test Student',
date: '2025-11-10',
seed: 12345,
fontSize: 12,
digitRange: { min: 2, max: 2 },
operator: 'mixed',
pAnyStart: 1.0,
pAllStart: 0,
displayRules: {
carryBoxes: 'never',
answerBoxes: 'never',
placeValueColors: 'never',
tenFrames: 'never',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
additionDisplayRules: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'never',
tenFrames: 'never', // ← Addition: no ten-frames
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
subtractionDisplayRules: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'whenRegrouping', // ← Subtraction: show ten-frames when borrowing
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'whenRegrouping',
borrowingHints: 'whenRegrouping',
},
interpolate: false,
page: { wIn: 8.5, hIn: 11 },
margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5 },
} as any
const problems: WorksheetProblem[] = [
{ operator: 'sub', minuend: 52, subtrahend: 18 }, // Subtraction with borrowing
]
const typstPages = generateTypstSource(config, problems)
const firstPage = typstPages[0]
// Subtraction with borrowing should show ten-frames
expect(firstPage).toContain('showTenFrames: true')
expect(firstPage).toContain('showBorrowNotation: true')
expect(firstPage).toContain('showBorrowingHints: true')
})
it('should handle subtraction problems with operator "sub" correctly', () => {
// This test verifies the fix for the Unicode operator bug
const config: WorksheetConfig = {
version: 4,
mode: 'mastery',
problemsPerPage: 2,
cols: 1,
pages: 1,
total: 2,
rows: 2,
orientation: 'portrait',
name: 'Test Student',
date: '2025-11-10',
seed: 12345,
fontSize: 12,
digitRange: { min: 2, max: 2 },
operator: 'mixed',
pAnyStart: 1.0,
pAllStart: 0,
displayRules: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'never',
tenFrames: 'never',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
additionDisplayRules: {
carryBoxes: 'always',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'always',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'never',
borrowingHints: 'never',
},
subtractionDisplayRules: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'always', // ← Should show for subtraction
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'always',
borrowingHints: 'always',
},
interpolate: false,
page: { wIn: 8.5, hIn: 11 },
margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5 },
} as any
const problems: WorksheetProblem[] = [
{ operator: 'sub', minuend: 52, subtrahend: 18 }, // operator: 'sub' (alphanumeric)
{ operator: 'add', a: 45, b: 27 }, // operator: 'add' (alphanumeric)
]
const typstPages = generateTypstSource(config, problems)
const firstPage = typstPages[0]
// Both problems should show scaffolding (not zero scaffolding bug)
const tenFramesTrueMatches = firstPage.match(/showTenFrames: true/g)
expect(tenFramesTrueMatches?.length).toBe(2) // Both problems
// Verify operators are correctly set (Typst uses "+" and "" display characters)
expect(firstPage).toContain('operator: ""') // Subtraction
expect(firstPage).toContain('operator: "+"') // Addition
})
it('should fallback to default displayRules when operator-specific rules are missing', () => {
const config: WorksheetConfig = {
version: 4,
mode: 'mastery',
problemsPerPage: 2,
cols: 1,
pages: 1,
total: 2,
rows: 2,
orientation: 'portrait',
name: 'Test Student',
date: '2025-11-10',
seed: 12345,
fontSize: 12,
digitRange: { min: 2, max: 2 },
operator: 'mixed',
pAnyStart: 1.0,
pAllStart: 0,
// Only default rules, no operator-specific rules
displayRules: {
carryBoxes: 'whenRegrouping',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'whenRegrouping',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'whenRegrouping',
borrowingHints: 'never',
},
interpolate: false,
page: { wIn: 8.5, hIn: 11 },
margins: { left: 0.5, right: 0.5, top: 0.5, bottom: 0.5 },
}
const problems: WorksheetProblem[] = [
{ operator: 'add', a: 45, b: 27 }, // Has regrouping
{ operator: 'sub', minuend: 52, subtrahend: 18 }, // Has borrowing
]
const typstPages = generateTypstSource(config, problems)
const firstPage = typstPages[0]
// Both should use default rules and show scaffolding
const tenFramesTrueMatches = firstPage.match(/showTenFrames: true/g)
expect(tenFramesTrueMatches?.length).toBe(2) // Both problems
})
})
})

View File

@ -463,11 +463,7 @@ export function AllSkillsModal({
{ value: 'available', label: 'Available', count: availableSkills.length },
{ value: 'locked', label: 'Locked', count: lockedSkills.length },
].map((tab) => (
<Tabs.Trigger
key={tab.value}
value={tab.value}
asChild
>
<Tabs.Trigger key={tab.value} value={tab.value} asChild>
<button
type="button"
className={css({

View File

@ -42,7 +42,6 @@ export const SKILL_TO_STEP_MIGRATION: Record<SkillId, string> = {
// Single-digit subtraction
'sd-sub-no-borrow': 'single-carry-1d-full', // Map to equivalent complexity
'sd-sub-borrow': 'single-carry-1d-full',
// Two-digit subtraction
'td-sub-no-borrow': 'single-carry-2d-full',

View File

@ -27,7 +27,6 @@ export type SkillId =
| '5d-mastery'
// Single-digit subtraction
| 'sd-sub-no-borrow'
| 'sd-sub-borrow'
// Two-digit subtraction
| 'td-sub-no-borrow'
| 'td-sub-ones-borrow'
@ -372,30 +371,6 @@ export const SKILL_DEFINITIONS: SkillDefinition[] = [
recommendedReview: [],
},
{
id: 'sd-sub-borrow',
name: 'Single-digit with borrowing',
description: 'Subtraction with borrowing like 13-7, 15-8',
operator: 'subtraction',
digitRange: { min: 1, max: 1 },
regroupingConfig: { pAnyStart: 1.0, pAllStart: 0 },
recommendedScaffolding: {
carryBoxes: 'never',
answerBoxes: 'always',
placeValueColors: 'always',
tenFrames: 'whenRegrouping',
problemNumbers: 'always',
cellBorders: 'always',
borrowNotation: 'whenRegrouping',
borrowingHints: 'whenRegrouping',
},
recommendedProblemCount: 20,
masteryThreshold: 0.9,
minimumAttempts: 20,
prerequisites: ['sd-sub-no-borrow'],
recommendedReview: ['sd-sub-no-borrow'],
},
// Two-Digit Subtraction (4 skills)
{
id: 'td-sub-no-borrow',
@ -417,8 +392,8 @@ export const SKILL_DEFINITIONS: SkillDefinition[] = [
recommendedProblemCount: 15,
masteryThreshold: 0.85,
minimumAttempts: 15,
prerequisites: ['sd-sub-borrow'],
recommendedReview: ['sd-sub-borrow'],
prerequisites: ['sd-sub-no-borrow'],
recommendedReview: ['sd-sub-no-borrow'],
},
{

View File

@ -526,6 +526,10 @@ const additionConfigV4MasterySchema = additionConfigV4BaseSchema.extend({
// Optional: Current step in mastery progression path
currentStepId: z.string().optional(),
// Optional: Current skills for mixed mode (operator-specific progression)
currentAdditionSkillId: z.string().optional(),
currentSubtractionSkillId: z.string().optional(),
})
// V4 uses discriminated union on 'mode'