diff --git a/backend/scripts/cleanup_ctmath_bank.js b/backend/scripts/cleanup_ctmath_bank.js new file mode 100644 index 0000000..79d26c2 --- /dev/null +++ b/backend/scripts/cleanup_ctmath_bank.js @@ -0,0 +1,88 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + cleanup_ctmath_bank.js — точечная чистка банка exam-prep ctmath. + + Что делает (идемпотентно): + 1. id=1248 (вычисление 5^lg2·2^lg5): дефектная задача (варианты «а» и «д» + одинаковы, верного ответа нет) — уже переведена в 'long'; чистим + литеральное answer="null" → NULL. + 2. id=1419 (var 2024, «укажите номера пар»): битый mc — сохранённый ответ «а» + («3 и 4») противоречит решению («4 и 5»), причём «4 и 5» вообще нет среди + вариантов; единственная подходящая пара — №4, ни один mc-вариант не верен. + Ретайрим в 'long' (self-check): убирается из авто-проверки тренажёра/пробника + (там берутся только mc/open), но текст и разбор сохраняются. + 3. variants_count трека ctmath → число «чистых» вариантов-пробников (variant≥101), + чтобы шапка («N вариантов») соответствовала пикеру (год-пачки скрыты роутом). + + Год-пачки (variant=год) НЕ удаляются — они остаются пулом задач для тренажёра + по темам (он отбирает по subtopic). «Указательные» opts (["1","1"]…) НЕ трогаем — + они рабочие (ученик выбирает номер). + + Запуск: node backend/scripts/cleanup_ctmath_bank.js [--apply] + ─────────────────────────────────────────────────────────────────────────── */ + +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const APPLY = process.argv.includes('--apply'); +const EXAM = 'ctmath'; +// Чистые варианты-пробники: 3-значные [101;1999]; год-пачки — 4-значные годы +// (≥2011) и 0 — исключены. Совпадает с MOCK_VARIANT_RANGE.ctmath в routes/exam-prep.js. +const MOCK_LO = 101, MOCK_HI = 1999; + +const db = new DatabaseSync(path.join(__dirname, '..', 'data', 'learnspace.db')); +const get = (sql, ...a) => db.prepare(sql).get(...a); + +console.log(`\n=== cleanup_ctmath_bank (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===\n`); + +const actions = []; + +// 1. id=1248 answer="null" → NULL +const t1248 = get(`SELECT id, task_type, answer FROM exam_tasks WHERE id=1248 AND exam_key=?`, EXAM); +if (t1248 && t1248.answer === 'null') { + actions.push({ desc: `id=1248: answer "null" → NULL (тип ${t1248.task_type})`, + run: () => db.prepare(`UPDATE exam_tasks SET answer=NULL WHERE id=1248`).run() }); +} else { + console.log(`• id=1248: пропуск (answer=${t1248 ? JSON.stringify(t1248.answer) : 'нет строки'})`); +} + +// 2. id=1419 битый mc → long, answer/opts NULL +const t1419 = get(`SELECT id, task_type FROM exam_tasks WHERE id=1419 AND exam_key=?`, EXAM); +if (t1419 && t1419.task_type === 'mc') { + actions.push({ desc: `id=1419: битый mc → 'long' (answer/opts → NULL, текст и разбор сохраняются)`, + run: () => db.prepare(`UPDATE exam_tasks SET task_type='long', answer=NULL, opts_json=NULL WHERE id=1419`).run() }); +} else { + console.log(`• id=1419: пропуск (тип ${t1419 ? t1419.task_type : 'нет строки'})`); +} + +// 3. variants_count = число чистых вариантов (≥101) +const cleanCnt = get(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN ? AND ?`, EXAM, MOCK_LO, MOCK_HI).c; +const curCnt = get(`SELECT variants_count vc FROM exam_tracks WHERE exam_key=?`, EXAM).vc; +if (curCnt !== cleanCnt) { + actions.push({ desc: `exam_tracks.variants_count: ${curCnt} → ${cleanCnt} (чистых вариантов [${MOCK_LO};${MOCK_HI}])`, + run: () => db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(cleanCnt, EXAM) }); +} else { + console.log(`• variants_count: пропуск (уже ${curCnt})`); +} + +console.log(`\nК применению (${actions.length}):`); +actions.forEach(a => console.log(' - ' + a.desc)); + +if (!actions.length) { console.log('\nНечего менять — всё уже в нужном состоянии.\n'); db.close(); process.exit(0); } + +if (!APPLY) { + console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/cleanup_ctmath_bank.js --apply\n'); + db.close(); process.exit(0); +} + +db.exec('BEGIN'); +try { + for (const a of actions) a.run(); + db.exec('COMMIT'); + console.log(`\n✓ Применено изменений: ${actions.length}.\n`); +} catch (e) { + db.exec('ROLLBACK'); + console.error('\n✗ Ошибка, откат:', e.message); + process.exitCode = 1; +} +db.close(); diff --git a/backend/scripts/seed_ctmath_rt2425_e1v1.js b/backend/scripts/seed_ctmath_rt2425_e1v1.js index 3360cd8..4245525 100644 --- a/backend/scripts/seed_ctmath_rt2425_e1v1.js +++ b/backend/scripts/seed_ctmath_rt2425_e1v1.js @@ -369,8 +369,8 @@ try { ); n++; } - // обновить variants_count = реальное число различных вариантов - const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c; + // variants_count = число «чистых» вариантов-пробников [101;1999]; год-пачки (годы≥2011, 0) скрыты роутом + const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c; db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM); db.exec('COMMIT'); console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}).`); diff --git a/backend/scripts/seed_ctmath_rt2425_e2v1.js b/backend/scripts/seed_ctmath_rt2425_e2v1.js index 05eef28..5893701 100644 --- a/backend/scripts/seed_ctmath_rt2425_e2v1.js +++ b/backend/scripts/seed_ctmath_rt2425_e2v1.js @@ -289,7 +289,7 @@ const upsert = db.prepare(` let n = 0; db.exec('BEGIN'); try { for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; } - const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c; + const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c; db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM); db.exec('COMMIT'); console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`); diff --git a/backend/scripts/seed_ctmath_rt2425_e3v1.js b/backend/scripts/seed_ctmath_rt2425_e3v1.js index 48de6e8..b3ab74f 100644 --- a/backend/scripts/seed_ctmath_rt2425_e3v1.js +++ b/backend/scripts/seed_ctmath_rt2425_e3v1.js @@ -285,7 +285,7 @@ const upsert = db.prepare(` let n = 0; db.exec('BEGIN'); try { for (const t of TASKS) { upsert.run(EXAM, VARIANT, t.idx, t.type, t.text, t.fig || null, t.type === 'mc' ? JSON.stringify(t.opts) : null, t.answer, buildSolution(t), t.topic, t.subtopic, t.diff); n++; } - const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=?`).get(EXAM).c; + const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c; db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM); db.exec('COMMIT'); console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}). variants_count=${distinct}.`); diff --git a/backend/src/routes/exam-prep.js b/backend/src/routes/exam-prep.js index 629a053..1ee459e 100644 --- a/backend/src/routes/exam-prep.js +++ b/backend/src/routes/exam-prep.js @@ -15,6 +15,21 @@ router.param('examKey', (req, res, next, examKey) => { next(); }); +/* ── Mock/variant picker: какие variant считаются «пробниками» ────── + ctmath: год-пачки (variant=год 2011–2024 и 0) — это тематический ПУЛ для + тренажёра по темам, а НЕ чистые 30-задачные варианты (у части до 114 задач). + Чистые варианты-пробники нумеруются 3-значно (101, 102, …), а год-пачки — + 4-значными годами (≥2011) и 0, поэтому фильтр — ДИАПАЗОН [101;1999], а не + просто порог (год 2024 > 101 и иначе бы прошёл!). В пикере пробников, + mock/start и просмотре вариантов показываем только чистые. Тренажёр по темам + отбирает по subtopic и этот фильтр НЕ использует — пул задач не теряется. + Для остальных треков (math9: варианты 1..80) диапазона нет — показываются все. */ +const MOCK_VARIANT_RANGE = { ctmath: [101, 1999] }; +const isMockVariant = (examKey, v) => { + const r = MOCK_VARIANT_RANGE[examKey]; + return r ? (v >= r[0] && v <= r[1]) : (v >= 1); +}; + /* ── Statements (prepared once) ────────────────────────────────── */ const SQL = { listTracks: db.prepare(` @@ -478,7 +493,7 @@ router.get('/:examKey/info', (req, res) => { router.get('/:examKey/variants', (req, res) => { const { examKey } = req.params; if (!SQL.getTrack.get(examKey)) return res.status(404).json({ error: 'Unknown exam track' }); - const rows = SQL.listVariants.all(req.user.id, examKey); + const rows = SQL.listVariants.all(req.user.id, examKey).filter(r => isMockVariant(examKey, r.variant)); const variants = rows.map(r => ({ n: r.variant, label: `Вариант ${r.variant}`, @@ -498,6 +513,7 @@ router.get('/:examKey/variants/:n/tasks', (req, res) => { const { examKey } = req.params; const n = parseInt(req.params.n, 10); if (!Number.isFinite(n) || n < 1) return res.status(400).json({ error: 'Bad variant number' }); + if (!isMockVariant(examKey, n)) return res.status(404).json({ error: 'Variant not found or empty' }); const rows = SQL.getVariantTasks.all(examKey, n); if (!rows.length) return res.status(404).json({ error: 'Variant not found or empty' }); @@ -1180,7 +1196,7 @@ router.post('/:examKey/mock/start', (req, res) => { if (source === 'variant') { variant = Number(req.body?.variant); - if (!Number.isInteger(variant) || variant < 1) { + if (!Number.isInteger(variant) || !isMockVariant(examKey, variant)) { return res.status(400).json({ error: 'Variant number required' }); } const rows = SQL.getTasksByVariant.all(examKey, variant); diff --git a/frontend/js/exam-prep/mock.js b/frontend/js/exam-prep/mock.js index b72680b..0922480 100644 --- a/frontend/js/exam-prep/mock.js +++ b/frontend/js/exam-prep/mock.js @@ -44,11 +44,16 @@ /* ════════════════════════════════════════════════════════════ PHASE 1: SETUP ════════════════════════════════════════════════════════════ */ - function renderSetup() { + async function renderSetup() { const title = EP.info?.track?.title || 'Пробный экзамен'; const dur = EP.info?.track?.duration_min || 180; const tpv = EP.info?.track?.tasks_per_variant || 10; - const vc = EP.info?.track?.variants_count || 80; + // Реальный список вариантов-пробников (бэкенд уже отфильтровал год-пачки): + // номера вариантов могут быть не подряд (ctmath: 101, 102, …), поэтому + // показываем выпадающий список реальных вариантов, а не диапазон 1..N. + let vlist = []; + try { vlist = (await EP.api.listVariants(examKey)).variants || []; } catch {} + const vOpts = vlist.map(v => ``).join(''); main.innerHTML = `