'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);