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>
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user