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:
parent
dffe305215
commit
e156e870df
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in New Issue