diff --git a/backend/scripts/check_variant_dups.js b/backend/scripts/check_variant_dups.js new file mode 100644 index 0000000..ca5f74f --- /dev/null +++ b/backend/scripts/check_variant_dups.js @@ -0,0 +1,85 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + check_variant_dups.js — проверка дубликатов задач в exam-prep (трек ctmath). + + Зачем: новые варианты-пробники не должны повторять уже имеющиеся задачи в + «общей базе» (видимый ученику пул = выверенные варианты [101;1999]). + + Режимы: + node backend/scripts/check_variant_dups.js + → аудит всего видимого пула ([101;1999]) на ВНУТРЕННИЕ точные дубли; + node backend/scripts/check_variant_dups.js seed_ctmath_ct2017_v1.js + → сверяет TASKS из seed-файла с уже имеющимся видимым пулом БД + (до --apply). Год-пачки (variant≥2011) и variant=0 в сравнении НЕ + участвуют (они скрыты из практики фильтром exam-prep.js). + + Точные дубли = совпадение нормализованного текста (теги/латех/пробелы убраны, + ЧИСЛА сохранены — параллельные задачи с другими числами дублями не считаются). + Возврат: код 0 — дублей нет; код 1 — найдены (для CI/ручной проверки). + Только ЧТЕНИЕ БД. --apply не нужен. + ─────────────────────────────────────────────────────────────────────────── */ + +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const MOCK_LO = 101, MOCK_HI = 1999; // видимый пул ctmath (как в exam-prep.js) +const EXAM = 'ctmath'; + +const norm = s => String(s || '') + .replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/gi, ' ') + .replace(/\$/g, '').replace(/\\[a-zA-Z]+/g, '') + .replace(/[^0-9a-zа-яёA-ZА-ЯЁ]+/g, '').toLowerCase(); + +const DB = path.join(__dirname, '..', 'data', 'learnspace.db'); +const db = new DatabaseSync(DB); + +// видимый пул: variant в [101;1999] +const pool = db.prepare( + `SELECT variant, task_idx, text_html FROM exam_tasks + WHERE exam_key=? AND variant BETWEEN ? AND ?`).all(EXAM, MOCK_LO, MOCK_HI); +const poolSig = new Map(); // sig -> [{variant,task_idx}] +for (const r of pool) { + const k = norm(r.text_html); if (!k) continue; + if (!poolSig.has(k)) poolSig.set(k, []); + poolSig.get(k).push({ variant: r.variant, task_idx: r.task_idx }); +} + +const arg = process.argv[2]; +let problems = 0; + +if (!arg) { + // АУДИТ внутренних дублей пула + const dups = [...poolSig.entries()].filter(([, a]) => a.length > 1); + console.log(`\n=== Аудит видимого пула ctmath [${MOCK_LO};${MOCK_HI}] ===`); + console.log(`Задач в пуле: ${pool.length}, уникальных сигнатур: ${poolSig.size}`); + if (!dups.length) console.log('✓ Точных дублей внутри пула НЕТ.'); + else { + problems = dups.length; + console.log(`✗ Точных дубль-групп: ${dups.length}`); + for (const [, a] of dups) console.log(' ' + a.map(x => `${x.variant}#${x.task_idx}`).join(' = ')); + } +} else { + // СВЕРКА seed-файла с пулом + const seedPath = path.isAbsolute(arg) ? arg : path.join(__dirname, arg); + let mod; + try { mod = require(seedPath); } catch (e) { console.error('✗ Не загрузить seed:', e.message); process.exit(2); } + const { TASKS, VARIANT } = mod; + if (!Array.isArray(TASKS)) { console.error('✗ В seed нет экспорта TASKS'); process.exit(2); } + console.log(`\n=== Сверка seed (variant=${VARIANT}, ${TASKS.length} задач) с пулом ===`); + // исключаем САМ этот вариант из пула (если уже применён — не считать самосовпадением) + const own = new Set(); + for (const t of TASKS) { + const k = norm(t.text); if (!k) continue; + const hit = (poolSig.get(k) || []).filter(x => x.variant !== VARIANT); + if (hit.length) { + problems++; + console.log(` ✗ #${t.idx} дублирует: ` + hit.map(x => `${x.variant}#${x.task_idx}`).join(', ')); + } + if (own.has(k)) { problems++; console.log(` ✗ #${t.idx} — внутренний дубль в самом варианте`); } + own.add(k); + } + if (!problems) console.log(`✓ Дублей с видимым пулом нет — variant=${VARIANT} можно добавлять.`); +} + +db.close(); +process.exit(problems ? 1 : 0);