feat: fase 2+3 — expansion masiva multi-perfil + bug fixes + hardening
Fase 2 (expansion): - Francesca: +6 etapas matematicas (5-10), +7 etapas lectura (3-9), +2 ortografia - Sebastian: +8 etapas matematicas (4-11), +6 lectura, +3 historia, +3 geografia, +1 banderas - Nuevas paginas: ortografia, lectura, geografia, matematicas - ~900 ejercicios nuevos, paridad ~500 por perfil Fase 3 (bug fixes + hardening): - Fix: SessionLog auto-create en grade route - Fix: engine cache (no borrar antes de revisar) - Fix: FSRS cards actualizadas en flujo curriculum - Fix: responseMs real (no hardcodeado 3000) - Fix: options dentro de content en sebastian etapa-1 - Fix: trazo-letra consonantes crash - Security: execFileSync anti-command-injection en TTS - DB: 18 cascade deletes + indices + unique constraints - APIs debug: reset, state, seed - Docker: espeak-ng + prisma + user/group - E2E: 23 tests nuevos (curriculum-flow.spec.ts) - Code review: tailwind colors, middleware dev mode, eslint config Verificado: tsc exit 0, build exit 0, 61 E2E tests pass
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
type Prob = {
|
||||
question: string
|
||||
answer: number
|
||||
hint: string
|
||||
emoji: string
|
||||
distractors: number[]
|
||||
}
|
||||
|
||||
const problems: Prob[] = [
|
||||
{ question: "Compraste 3 caramelos a $2 cada uno. ¿Cuánto gastaste?", answer: 6, hint: "3 × 2", emoji: "🍬", distractors: [5, 8] },
|
||||
{ question: "Tenés $20 y querés comprar 4 juguetes iguales. ¿Cuánto cuesta cada uno?", answer: 5, hint: "20 ÷ 4", emoji: "🧸", distractors: [4, 6] },
|
||||
{ question: "Un libro cuesta $12 y una lapicera cuesta $3. ¿Cuánto cuestan los dos?", answer: 15, hint: "12 + 3", emoji: "📚", distractors: [14, 16] },
|
||||
{ question: "Tenés $15 y gastás $7 en un helado. ¿Cuánto te queda?", answer: 8, hint: "15 - 7", emoji: "🍦", distractors: [7, 9] },
|
||||
{ question: "Un cuaderno cuesta $8. Compraste 3 cuadernos. ¿Cuánto pagaste?", answer: 24, hint: "8 × 3", emoji: "📓", distractors: [22, 27] },
|
||||
{ question: "Una mesa mide 120 cm de alto. Una silla mide 45 cm. ¿Cuánto miden juntos?", answer: 165, hint: "120 + 45", emoji: "📏", distractors: [155, 175] },
|
||||
{ question: "Una botella tiene 1 litro de agua. Serviste 4 vasos de 250 ml cada uno. ¿Cuánto serviste?", answer: 1000, hint: "4 × 250", emoji: "💧", distractors: [750, 1250] },
|
||||
{ question: "Compraste 2 kilos de manzanas a $5 el kilo. ¿Cuánto pagaste?", answer: 10, hint: "2 × 5", emoji: "🍎", distractors: [8, 12] },
|
||||
{ question: "Un tren tiene 3 vagones. En cada vagón viajan 6 personas. ¿Cuántas personas viajan?", answer: 18, hint: "3 × 6", emoji: "🚂", distractors: [16, 20] },
|
||||
{ question: "Una pizza tiene 8 porciones. Comiste 3. ¿Cuántas quedan?", answer: 5, hint: "8 - 3", emoji: "🍕", distractors: [4, 6] },
|
||||
{ question: "Gastaste $50 en el supermercado con $100. ¿Cuánto vuelto recibís?", answer: 50, hint: "100 - 50", emoji: "🛒", distractors: [40, 60] },
|
||||
{ question: "Un lápiz cuesta $3 y una goma cuesta $2. Compraste 2 lápices y 1 goma. ¿Cuánto pagaste?", answer: 8, hint: "2 × 3 + 2", emoji: "✏️", distractors: [7, 10] },
|
||||
{ question: "Tu clase tiene 24 alumnos. Los dividís en 4 equipos iguales. ¿Cuántos alumnos por equipo?", answer: 6, hint: "24 ÷ 4", emoji: "👥", distractors: [5, 7] },
|
||||
{ question: "Una receta usa 250 gramos de harina. Hacés 4 recetas. ¿Cuántos gramos necesitás?", answer: 1000, hint: "250 × 4", emoji: "🧁", distractors: [800, 1200] },
|
||||
{ question: "Corriste 3 vueltas a la plaza. Cada vuelta es 400 metros. ¿Cuánto corriste?", answer: 1200, hint: "3 × 400", emoji: "🏃", distractors: [1000, 1400] },
|
||||
{ question: "Una caja tiene 12 huevos. Usaste 4 para cocinar. ¿Cuántos quedan?", answer: 8, hint: "12 - 4", emoji: "🥚", distractors: [7, 9] },
|
||||
{ question: "Compraste 5 paquetes de figuritas a $3 cada uno. ¿Cuánto pagaste?", answer: 15, hint: "5 × 3", emoji: "🃏", distractors: [14, 18] },
|
||||
{ question: "Son las 10:00. Pasan 3 horas. ¿Qué hora es?", answer: 13, hint: "10 + 3", emoji: "⏰", distractors: [12, 14] },
|
||||
{ question: "Una regla mide 30 cm. Tenés 4 reglas iguales puestas en fila. ¿Cuánto miden?", answer: 120, hint: "4 × 30", emoji: "📏", distractors: [110, 130] },
|
||||
{ question: "Compraste 2 docenas de facturas. ¿Cuántas facturas son?", answer: 24, hint: "2 × 12", emoji: "🥐", distractors: [22, 26] },
|
||||
{ question: "Tenés $100. Comprás un juguete de $35 y otro de $28. ¿Cuánto gastaste?", answer: 63, hint: "35 + 28", emoji: "🧸", distractors: [60, 65] },
|
||||
{ question: "En una pecera hay 15 peces. Compraste 8 más. ¿Cuántos peces hay?", answer: 23, hint: "15 + 8", emoji: "🐠", distractors: [22, 24] },
|
||||
{ question: "Tenés 24 lápices y los repartís entre 6 amigos. ¿Cuántos lápices recibe cada uno?", answer: 4, hint: "24 ÷ 6", emoji: "✏️", distractors: [3, 5] },
|
||||
{ question: "Gastaste $12 en la mañana y $18 en la tarde. ¿Cuánto gastaste en total?", answer: 30, hint: "12 + 18", emoji: "💰", distractors: [28, 32] },
|
||||
{ question: "Un camión lleva 45 cajas. Descargaron 18. ¿Cuántas cajas quedan?", answer: 27, hint: "45 - 18", emoji: "🚚", distractors: [26, 28] },
|
||||
{ question: "Una hormiga camina 5 cm por minuto. ¿Cuánto camina en 7 minutos?", answer: 35, hint: "5 × 7", emoji: "🐜", distractors: [33, 40] },
|
||||
{ question: "Tenés $50. Compras una remera de $22 y una gorra de $15. ¿Cuánto vuelto recibís?", answer: 13, hint: "50 - 22 - 15", emoji: "👕", distractors: [12, 15] },
|
||||
{ question: "En un álbum caben 9 figuritas por página. Tenés 6 páginas llenas. ¿Cuántas figuritas tenés?", answer: 54, hint: "9 × 6", emoji: "🖼️", distractors: [52, 56] },
|
||||
{ question: "Un paquete de galletitas trae 18. Comiste 5. ¿Cuántas quedan?", answer: 13, hint: "18 - 5", emoji: "🍪", distractors: [12, 14] },
|
||||
{ question: "Ahorraste $8 por semana durante 6 semanas. ¿Cuánto ahorraste?", answer: 48, hint: "8 × 6", emoji: "🏦", distractors: [46, 50] },
|
||||
]
|
||||
|
||||
const exercises: any[] = []
|
||||
for (let i = 0; i < problems.length; i++) {
|
||||
const p = problems[i]
|
||||
const ds = p.distractors.slice()
|
||||
if (ds.length < 2) {
|
||||
ds.push(p.answer + 2)
|
||||
}
|
||||
exercises.push({
|
||||
id: `fra10-prob-${i}`,
|
||||
stage: 10,
|
||||
skillCode: `prob-dinero-${i}`,
|
||||
type: "problema",
|
||||
prerequisites: i === 0 ? [] : [exercises[i - 1].id],
|
||||
content: {
|
||||
instruction: "Resolvé el problema:",
|
||||
text: p.question,
|
||||
audioText: p.question,
|
||||
emoji: p.emoji,
|
||||
options: [
|
||||
{ label: `$${p.answer}`, correct: true, id: `c-${i}` },
|
||||
{ label: `$${ds[0]}`, correct: false, id: `w1-${i}` },
|
||||
{ label: `$${ds[1]}`, correct: false, id: `w2-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: `Resolver problemas con dinero y medidas (vida práctica Montessori): ${p.hint}.`,
|
||||
errorType: "discriminacion-auditiva",
|
||||
})
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 10,
|
||||
name: "Problemas con dinero y medidas",
|
||||
description: "Aplicar operaciones en contexto de la vida real con dinero ($), medidas (cm, kg, L) y tiempo.",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
const TABLAS = [2, 5, 10] as const
|
||||
|
||||
const exercises: any[] = []
|
||||
|
||||
let idx = 0
|
||||
for (const t of TABLAS) {
|
||||
for (let m = 1; m <= 10; m++) {
|
||||
const correct = t * m
|
||||
exercises.push({
|
||||
id: `fra5-tabla${t}-${m}`,
|
||||
stage: 5,
|
||||
skillCode: `tabla-${t}-x-${m}`,
|
||||
type: "eleccion",
|
||||
prerequisites: idx === 0 ? [] : [exercises[idx - 1]?.id].filter(Boolean),
|
||||
content: {
|
||||
instruction: `¿Cuánto es ${t} × ${m}?`,
|
||||
audioText: `${t} por ${m} es ${correct}`,
|
||||
emoji: "🔢",
|
||||
options: [
|
||||
{ label: String(correct), correct: true, id: String(correct) },
|
||||
{ label: String(correct + 1), correct: false, id: `w1-${idx}` },
|
||||
{ label: String(correct + t), correct: false, id: `w2-${idx}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: `Memorizar tabla del ${t} con método Montessori (tabla de Pythagore).`,
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 5,
|
||||
name: "Tablas de multiplicar (2, 5, 10)",
|
||||
description: "Memorizar las tablas de multiplicar del 2, 5 y 10 con material Montessori (tabla de Pythagore, cuentas doradas).",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
const TABLAS = [3, 4, 6, 7, 8, 9] as const
|
||||
|
||||
const exercises: any[] = []
|
||||
|
||||
let idx = 0
|
||||
for (const t of TABLAS) {
|
||||
for (let m = 1; m <= 10; m++) {
|
||||
const correct = t * m
|
||||
exercises.push({
|
||||
id: `fra6-tabla${t}-${m}`,
|
||||
stage: 6,
|
||||
skillCode: `tabla-${t}-x-${m}`,
|
||||
type: "eleccion",
|
||||
prerequisites: idx === 0 ? [] : [exercises[idx - 1]?.id].filter(Boolean),
|
||||
content: {
|
||||
instruction: `¿Cuánto es ${t} × ${m}?`,
|
||||
audioText: `${t} por ${m} es ${correct}`,
|
||||
emoji: "✖️",
|
||||
options: [
|
||||
{ label: String(correct), correct: true, id: String(correct) },
|
||||
{ label: String(correct + t), correct: false, id: `w1-${idx}` },
|
||||
{ label: String(Math.max(correct - 1, 1)), correct: false, id: `w2-${idx}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: `Memorizar tabla del ${t} con método Montessori (tabla perforada, material de cuentas).`,
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 6,
|
||||
name: "Tablas de multiplicar (3, 4, 6, 7, 8, 9)",
|
||||
description: "Completar todas las tablas de multiplicar restantes con método Montessori.",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
const mults = [
|
||||
{ a: 12, b: 3 }, { a: 14, b: 2 }, { a: 21, b: 4 }, { a: 23, b: 3 },
|
||||
{ a: 15, b: 5 }, { a: 18, b: 2 }, { a: 24, b: 3 }, { a: 31, b: 2 },
|
||||
{ a: 34, b: 2 }, { a: 25, b: 4 }, { a: 42, b: 2 }, { a: 56, b: 3 },
|
||||
{ a: 13, b: 6 }, { a: 27, b: 3 }, { a: 32, b: 4 }, { a: 45, b: 2 },
|
||||
{ a: 11, b: 7 }, { a: 22, b: 4 }, { a: 33, b: 3 }, { a: 41, b: 2 },
|
||||
{ a: 52, b: 3 }, { a: 63, b: 2 }, { a: 71, b: 4 }, { a: 81, b: 2 },
|
||||
]
|
||||
|
||||
const exercises: any[] = []
|
||||
for (let i = 0; i < mults.length; i++) {
|
||||
const m = mults[i]
|
||||
const res = m.a * m.b
|
||||
exercises.push({
|
||||
id: `fra7-mult-${m.a}-${m.b}`,
|
||||
stage: 7,
|
||||
skillCode: `mult-${m.a}x${m.b}`,
|
||||
type: "eleccion",
|
||||
prerequisites: i === 0 ? [] : [exercises[i - 1].id],
|
||||
content: {
|
||||
instruction: `Multiplicá: ${m.a} × ${m.b}`,
|
||||
audioText: `${m.a} por ${m.b}`,
|
||||
emoji: "✖️",
|
||||
options: [
|
||||
{ label: String(res), correct: true, id: String(res) },
|
||||
{ label: String(res + m.b), correct: false, id: `w1-${i}` },
|
||||
{ label: String(res - m.a), correct: false, id: `w2-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: `Multiplicar ${m.a} × ${m.b} con método Montessori (juego de sellos, descomposición decena-unidad).`,
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 7,
|
||||
name: "Multiplicación de 2 dígitos × 1 dígito",
|
||||
description: "Resolver multiplicaciones de 2 dígitos por 1 dígito descomponiendo en decenas y unidades.",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
const divisions = [
|
||||
{ a: 12, b: 2 }, { a: 12, b: 3 }, { a: 12, b: 4 }, { a: 15, b: 3 },
|
||||
{ a: 15, b: 5 }, { a: 16, b: 4 }, { a: 18, b: 2 }, { a: 18, b: 3 },
|
||||
{ a: 20, b: 4 }, { a: 20, b: 5 }, { a: 24, b: 3 }, { a: 24, b: 4 },
|
||||
{ a: 25, b: 5 }, { a: 27, b: 3 }, { a: 28, b: 4 }, { a: 30, b: 5 },
|
||||
{ a: 30, b: 6 }, { a: 32, b: 4 }, { a: 35, b: 5 }, { a: 36, b: 4 },
|
||||
{ a: 36, b: 6 }, { a: 40, b: 5 }, { a: 42, b: 6 }, { a: 45, b: 5 },
|
||||
]
|
||||
|
||||
const exercises: any[] = []
|
||||
for (let i = 0; i < divisions.length; i++) {
|
||||
const d = divisions[i]
|
||||
const res = d.a / d.b
|
||||
exercises.push({
|
||||
id: `fra8-div-${d.a}-${d.b}`,
|
||||
stage: 8,
|
||||
skillCode: `div-${d.a}-${d.b}`,
|
||||
type: "eleccion",
|
||||
prerequisites: i === 0 ? [] : [exercises[i - 1].id],
|
||||
content: {
|
||||
instruction: `Dividí: ${d.a} ÷ ${d.b}`,
|
||||
audioText: `${d.a} dividido ${d.b}`,
|
||||
emoji: "➗",
|
||||
options: [
|
||||
{ label: String(res), correct: true, id: String(res) },
|
||||
{ label: String(res + 1), correct: false, id: `w1-${i}` },
|
||||
{ label: String(Math.max(res - 1, 1)), correct: false, id: `w2-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: `Dividir ${d.a} ÷ ${d.b} con método Montessori (cuentas doradas, reparto equitativo).`,
|
||||
errorType: "discriminacion-auditiva",
|
||||
})
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 8,
|
||||
name: "Divisiones exactas (1 dígito)",
|
||||
description: "Resolver divisiones exactas de 1 dígito con material Montessori de reparto equitativo.",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
@@ -0,0 +1,135 @@
|
||||
import type { Stage } from "../types"
|
||||
|
||||
type Frac = { num: number; den: number }
|
||||
|
||||
function randFrac(): Frac {
|
||||
const denoms = [2, 3, 4]
|
||||
const d = denoms[Math.floor(Math.random() * denoms.length)]
|
||||
const n = Math.floor(Math.random() * (d - 1)) + 1
|
||||
return { num: n, den: d }
|
||||
}
|
||||
|
||||
function fracLabel(f: Frac): string {
|
||||
return `${f.num}/${f.den}`
|
||||
}
|
||||
|
||||
// Recognize which fraction is bigger
|
||||
const compares: { a: Frac; b: Frac; correct: "a" | "b" }[] = []
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let a = randFrac()
|
||||
let b = randFrac()
|
||||
while (a.num / a.den === b.num / b.den) {
|
||||
b = randFrac()
|
||||
}
|
||||
compares.push({ a, b, correct: a.num / a.den > b.num / b.den ? "a" : "b" })
|
||||
}
|
||||
|
||||
// Identify the fraction (label + visual)
|
||||
const labels = [
|
||||
{ frac: { num: 1, den: 2 }, label: "un medio" },
|
||||
{ frac: { num: 1, den: 3 }, label: "un tercio" },
|
||||
{ frac: { num: 2, den: 3 }, label: "dos tercios" },
|
||||
{ frac: { num: 1, den: 4 }, label: "un cuarto" },
|
||||
{ frac: { num: 3, den: 4 }, label: "tres cuartos" },
|
||||
{ frac: { num: 2, den: 4 }, label: "dos cuartos" },
|
||||
]
|
||||
|
||||
const exercises: any[] = []
|
||||
|
||||
// Part 1: Compare fractions
|
||||
for (let i = 0; i < compares.length; i++) {
|
||||
const c = compares[i]
|
||||
const aVal = c.a.num / c.a.den
|
||||
const bVal = c.b.num / c.b.den
|
||||
exercises.push({
|
||||
id: `fra9-frac-compare-${i}`,
|
||||
stage: 9,
|
||||
skillCode: `frac-compare-${i}`,
|
||||
type: "conciencia",
|
||||
prerequisites: i === 0 ? [] : [exercises[i - 1].id],
|
||||
content: {
|
||||
instruction: `¿Cuál es la fracción más grande?`,
|
||||
text: `${fracLabel(c.a)} vs ${fracLabel(c.b)}`,
|
||||
audioText: `${fracLabel(c.a)} o ${fracLabel(c.b)}, cuál es más grande`,
|
||||
emoji: "🥧",
|
||||
options: [
|
||||
{ label: fracLabel(c.a), correct: c.correct === "a", id: `a-${i}` },
|
||||
{ label: fracLabel(c.b), correct: c.correct === "b", id: `b-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: "Comparar fracciones con método Montessori (círculos de fracciones rojos).",
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
}
|
||||
|
||||
// Part 2: Identify fractions by name
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
const l = labels[i]
|
||||
const idx = compares.length + i
|
||||
exercises.push({
|
||||
id: `fra9-frac-label-${i}`,
|
||||
stage: 9,
|
||||
skillCode: `frac-label-${i}`,
|
||||
prerequisites: idx === 0 ? [] : [exercises[idx - 1].id],
|
||||
type: "eleccion",
|
||||
content: {
|
||||
instruction: `¿Cuál es "${l.label}"?`,
|
||||
audioText: `${l.label}`,
|
||||
emoji: "🔵",
|
||||
options: [
|
||||
{ label: fracLabel(l.frac), correct: true, id: `c-${i}` },
|
||||
{ label: fracLabel(labels[(i + 1) % labels.length].frac), correct: false, id: `w1-${i}` },
|
||||
{ label: fracLabel(labels[(i + 2) % labels.length].frac), correct: false, id: `w2-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: "Identificar fracciones por nombre con método Montessori.",
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
}
|
||||
|
||||
// Part 3: Shade recognition (conciencia)
|
||||
const shades = [
|
||||
{ num: 1, den: 2, text: "Si dividís una pizza en 2 partes iguales y tomás 1, ¿qué fracción comiste?" },
|
||||
{ num: 1, den: 3, text: "Si dividís una pizza en 3 partes iguales y tomás 1, ¿qué fracción comiste?" },
|
||||
{ num: 2, den: 3, text: "Si dividís una pizza en 3 partes iguales y tomás 2, ¿qué fracción comiste?" },
|
||||
{ num: 1, den: 4, text: "Si dividís una pizza en 4 partes iguales y tomás 1, ¿qué fracción comiste?" },
|
||||
{ num: 3, den: 4, text: "Si dividís una pizza en 4 partes iguales y tomás 3, ¿qué fracción comiste?" },
|
||||
{ num: 2, den: 4, text: "Si dividís una pizza en 4 partes iguales y tomás 2, ¿qué fracción comiste?" },
|
||||
{ num: 0, den: 0, text: "" },
|
||||
{ num: 0, den: 0, text: "" },
|
||||
{ num: 0, den: 0, text: "" },
|
||||
]
|
||||
|
||||
for (let i = 0; i < shades.length; i++) {
|
||||
if (!shades[i].text) continue
|
||||
const s = shades[i]
|
||||
const idx = compares.length + labels.length + i
|
||||
exercises.push({
|
||||
id: `fra9-frac-shade-${i}`,
|
||||
stage: 9,
|
||||
skillCode: `frac-shade-${i}`,
|
||||
prerequisites: idx === 0 ? [] : [exercises[idx - 1].id],
|
||||
type: "conciencia",
|
||||
content: {
|
||||
instruction: s.text,
|
||||
audioText: s.text,
|
||||
emoji: "🍕",
|
||||
options: [
|
||||
{ label: `${s.num}/${s.den}`, correct: true, id: `c-${i}` },
|
||||
{ label: `${s.den}/${s.num}`, correct: false, id: `w1-${i}` },
|
||||
{ label: `${s.num + 1}/${s.den}`, correct: false, id: `w2-${i}` },
|
||||
].sort(() => Math.random() - 0.5),
|
||||
},
|
||||
functionalContext: "Reconocer fracciones en contexto de partes de un entero (círculos Montessori).",
|
||||
errorType: "confusion-letra",
|
||||
})
|
||||
}
|
||||
|
||||
const stage: Stage = {
|
||||
id: 9,
|
||||
name: "Fracciones simples (1/2, 1/3, 1/4)",
|
||||
description: "Reconocer y comparar fracciones básicas con círculos de fracciones Montessori.",
|
||||
exercises,
|
||||
}
|
||||
|
||||
export default stage
|
||||
Reference in New Issue
Block a user