Files
Learn_System/backend/scripts/check_variant_dups.js
T
Maxim Dolgolyov 70cf6b3af1 tools(ctmath): check_variant_dups.js — гейт дедупликации перед добавлением варианта
Постоянный read-only инструмент: (1) без аргумента — аудит видимого пула [101;1999]
на внутренние точные дубли; (2) с seed-файлом — сверяет его TASKS с пулом ДО --apply.
Норма текста: теги/латех/пробелы убраны, ЧИСЛА сохранены (параллельные задачи дублями
не считаются). Сейчас пул = 514 задач, 514 уникальных сигнатур, 0 дублей.
Включается в конвейер тиража: новый вариант проверяется этим гейтом перед записью.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 13:31:02 +03:00

86 lines
4.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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);