'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 : 'нет строки'})`); } // 2b. Срезать провенанс-префикс [ЦТ YYYY · XN] из начала текста задания // (в чистых вариантах 101+ его нет; для консистентности убираем из год-пачек). // Паттерн узкий: [ + ЦТ|ЦЭ|РТ|ДРТ + год + … + ]; математические скобки внутри $…$ не задеваются. const reTag = /^\s*\[(?:ЦТ|ЦЭ|РТ|ДРТ)\s+\d{4}[^\]]*\]\s*/; const prefixed = db.prepare(`SELECT id, text_html FROM exam_tasks WHERE exam_key=? AND TRIM(text_html) LIKE '[%'`).all(EXAM) .filter(r => reTag.test(r.text_html)) .map(r => ({ id: r.id, clean: r.text_html.replace(reTag, '') })) .filter(p => p.clean.trim().length > 0); // не обнуляем задачу if (prefixed.length) { actions.push({ desc: `срезать провенанс-префикс [ЦТ … ] у ${prefixed.length} заданий`, run: () => { const upd = db.prepare(`UPDATE exam_tasks SET text_html=? WHERE id=?`); for (const p of prefixed) upd.run(p.clean, p.id); } }); } else { console.log('• провенанс-префиксы: пропуск (не найдено)'); } // 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();