diff --git a/backend/scripts/seed_ctmath_ce2024_v1.js b/backend/scripts/seed_ctmath_ce2024_v1.js
new file mode 100644
index 0000000..ce6dfa5
--- /dev/null
+++ b/backend/scripts/seed_ctmath_ce2024_v1.js
@@ -0,0 +1,369 @@
+'use strict';
+/* ───────────────────────────────────────────────────────────────────────────
+ seed_ctmath_ce2024_v1.js
+ Чистый вариант-пробник для трека exam-prep `ctmath`.
+
+ Источник: Централизованный экзамен / ЦТ по математике, 2024 (сборник тестов
+ РИКЗ / «Аверсэв»), Вариант 1. АКТУАЛЬНЫЙ формат: Часть А = А1–А10 (закрытые,
+ 5 вариантов), Часть В = В1–В20 (открытые). Всего 30 заданий. Перенабрано
+ вручную в KaTeX по PDF:
+ F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦЭ-ЦТ-2024 МАТ.pdf
+
+ ⚠️ В PDF РЕШЕНИЙ НЕТ (только задания). Решения и ответы получены вручную и
+ СВЕРЕНЫ с официальной таблицей ответов в конце сборника (стр. 35, столбец
+ «Вариант 1»). Все 30 ответов совпали. variant=111.
+
+ Адаптации заданий-«с-картинкой» (исходный ответ/идея сохранены):
+ • А1 (точки на координатной прямой) → равные промежутки заданы в тексте
+ (та же точка-ответ);
+ • А6 (изображённый числовой промежуток) → промежуток $(-6;9]$ описан
+ словами (ответ 24 сохранён).
+ Остальная геометрия закодирована текстом (как у РТ-вариантов 101–109).
+
+ Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
+ Запуск:
+ node backend/scripts/seed_ctmath_ce2024_v1.js # DRY-RUN (по умолчанию)
+ node backend/scripts/seed_ctmath_ce2024_v1.js --apply # запись в БД
+
+ ⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
+ ─────────────────────────────────────────────────────────────────────────── */
+
+const { DatabaseSync } = require('node:sqlite');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const EXAM = 'ctmath';
+const VARIANT = 111;
+const PROV = 'ЦЭ–2024, Вариант 1';
+const R = String.raw;
+
+const L = ['а', 'б', 'в', 'г', 'д'];
+const mc = (...html) => html.map((h, i) => [L[i], h]);
+
+/* ── 30 заданий ─────────────────────────────────────────────────────────── */
+const TASKS = [
+ // ── Часть A: А1–А10 ──────────────────────────────────────────────────────
+ { idx: 1, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
+ text: R`На координатной прямой точки $D$, $O$, $C$, $B$, $A$, $E$ идут в указанном порядке через равные промежутки; $O$ — начало отсчёта, а соседние точки отличаются по координате на $0{,}8$. Какой точке соответствует число $1{,}6$?`,
+ opts: mc('$A$', '$B$', '$C$', '$D$', '$E$'),
+ answer: 'б',
+ sol: R`От $O$ вправо: $C=0{,}8$, $B=1{,}6$, $A=2{,}4$. Числу $1{,}6$ соответствует точка $B$.`,
+ ref: 'Латотин «Математика, 6 кл.», гл. 5' },
+
+ { idx: 2, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 2,
+ text: R`На рисунке изображена правильная четырёхугольная пирамида $SABCD$, точка $O$ — точка пересечения диагоналей основания $ABCD$. Среди прямых $BC$, $BD$, $SO$, $SB$, $SD$ укажите прямую, по которой пересекаются плоскости $DSO$ и $SCB$.`,
+ opts: mc('$BC$', '$BD$', '$SO$', '$SB$', '$SD$'),
+ answer: 'г',
+ sol: R`Точка $O$ лежит на диагонали $BD$, поэтому плоскость $DSO$ совпадает с плоскостью $SBD$. Плоскости $SBD$ и $SCB$ имеют общие точки $S$ и $B$, поэтому пересекаются по прямой $SB$.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 1, § 2' },
+
+ { idx: 3, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 1,
+ text: R`Среди значений аргумента, равных $-\dfrac{\pi}{6}$, $\dfrac{\pi}{4}$, $\dfrac{\pi}{3}$, $-\dfrac{3\pi}{2}$, $-6\pi$, укажите то, при котором значение функции $y=\sin x$ равно нулю.`,
+ opts: mc('$-\dfrac{\pi}{6}$', '$\dfrac{\pi}{4}$', '$\dfrac{\pi}{3}$', '$-\dfrac{3\pi}{2}$', '$-6\pi$'),
+ answer: 'д',
+ sol: R`$\sin x=0$ при $x=\pi n$, $n\in\mathbb{Z}$. Из данных значений этому условию удовлетворяет только $-6\pi$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 2' },
+
+ { idx: 4, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
+ text: R`Укажите номер формулы, по которой можно найти делимое $n$ при делении с остатком, если делитель $15$, неполное частное $k$, остаток $7$ ($n$ — натуральное число).`,
+ opts: mc('$n=15(k+7)$', '$n=k+22$', '$n=15k+7$', '$n=7k+15$', '$n=7(k+15)$'),
+ answer: 'в',
+ sol: R`При делении с остатком $n=q\cdot b+r$, где $b$ — делитель, $q$ — неполное частное, $r$ — остаток. Значит, $n=15k+7$.`,
+ ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1, § 11' },
+
+ { idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
+ text: R`Укажите номер квадратного уравнения, произведение действительных корней которого равно $5$.`,
+ opts: mc('$x^{2}-6x+5=0$', '$x^{2}-4x+5=0$', '$x^{2}-5x+6=0$', '$x^{2}+5x=0$', '$x^{2}-5=0$'),
+ answer: 'а',
+ sol: R`По теореме Виета произведение корней приведённого уравнения равно свободному члену. Произведение $5$ имеют уравнения 1 и 2, но у уравнения $x^{2}-4x+5=0$ дискриминант отрицателен (действительных корней нет). Значит, подходит $x^{2}-6x+5=0$ ($D=16>0$).`,
+ ref: 'Арефьева «Алгебра, 8 кл.», гл. 2' },
+
+ { idx: 6, type: 'open', topic: 'numbers', subtopic: 'num-real', diff: 2,
+ text: R`На координатной прямой изображён промежуток $(-6;9]$ (точка $-6$ не входит, точка $9$ входит). Укажите номера пар промежутков, объединением которых является этот промежуток.
1) $(-6;+\infty)$ и $(-6;9)$;
2) $(-6;0)$ и $[0;9]$;
3) $(-\infty;-6)$ и $(-\infty;9)$;
4) $(-6;9]$ и $(0;4)$;
5) $(-\infty;9]$ и $(-6;+\infty)$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '24', ansShow: '2, 4',
+ sol: R`$2)$ $(-6;0)\cup[0;9]=(-6;9]$ — верно. $\ 4)$ $(-6;9]\cup(0;4)=(-6;9]$ (так как $(0;4)\subset(-6;9]$) — верно. Остальные дают другие множества: 1) $(-6;+\infty)$; 3) $(-\infty;9)$; 5) $\mathbb{R}$. Подходят пары 2 и 4.`,
+ ref: 'Арефьева «Алгебра, 8 кл.», гл. 1, § 5' },
+
+ { idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
+ text: R`Толя купил $3$ альбома и $5$ карандашей. Стоимость одного альбома равна $1$ р. $20$ к., а стоимость одного карандаша равна $25$ к. Какая сумма (в копейках) осталась у Толи после покупки альбомов и карандашей, если всего у него было $6$ р.?`,
+ opts: mc('$115$ к.', '$145$ к.', '$110$ к.', '$125$ к.', '$275$ к.'),
+ answer: 'а',
+ sol: R`Потрачено $3\cdot120+5\cdot25=360+125=485$ (к.). Осталось $600-485=115$ (к.).`,
+ ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 2' },
+
+ { idx: 8, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 2,
+ text: R`Найдите значение выражения $\dfrac{38}{\pi}\cdot\arcsin(-1)-\left|-7\right|$.`,
+ opts: mc('$-16$', '$-12$', '$12$', '$26$', '$-26$'),
+ answer: 'д',
+ sol: R`$\arcsin(-1)=-\dfrac{\pi}{2}$, поэтому $\dfrac{38}{\pi}\cdot\left(-\dfrac{\pi}{2}\right)-7=-19-7=-26$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 7' },
+
+ { idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
+ text: R`Квадрат, длина диагонали которого равна $8$, лежит в плоскости $\alpha$. Сфера касается плоскости $\alpha$ в точке пересечения диагоналей квадрата. Найдите площадь сферы, если расстояние от центра сферы до вершины квадрата равно $4\sqrt2$.`,
+ opts: mc('$8\pi$', '$16\pi$', '$64\pi$', '$32\sqrt2\,\pi$', '$32\pi$'),
+ answer: 'в',
+ sol: R`Точка касания — центр квадрата; радиус сферы $R$ перпендикулярен плоскости. Расстояние от центра квадрата до вершины равно половине диагонали — $4$. Тогда $R^{2}+4^{2}=(4\sqrt2)^{2}=32$, $R^{2}=16$, $R=4$. Площадь сферы $S=4\pi R^{2}=64\pi$.`,
+ ref: 'Латотин «Геометрия, 11 кл.», разд. 3' },
+
+ { idx: 10, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
+ text: R`Укажите номера выражений, которые имеют смысл при $a=-6$.
1) $\dfrac{1}{\sqrt[5]{a-6}}$;
2) $\sqrt{a^{5}}$;
3) $\sqrt[5]{a}$;
4) $\dfrac{1}{\sqrt[6]{a-6}}$;
5) $\sqrt[6]{a}$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '13', ansShow: '1, 3',
+ sol: R`При $a=-6$: $\ 1)$ $\sqrt[5]{-12}$ определён (нечётный корень) и не равен нулю — смысл есть. $\ 2)$ $\sqrt{(-6)^{5}}$ — корень чётной степени из отрицательного числа — нет смысла. $\ 3)$ $\sqrt[5]{-6}$ определён — смысл есть. $\ 4)$ $\sqrt[6]{-12}$ — нет смысла. $\ 5)$ $\sqrt[6]{-6}$ — нет смысла. Подходят 1 и 3.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 2, § 14' },
+
+ // ── Часть B: В1–В20 ──────────────────────────────────────────────────────
+ { idx: 11, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
+ text: R`Дана прямая треугольная призма $ABCA_1B_1C_1$. Точка $M$ — середина ребра $AB$, $\angle ABC=90^\circ$. Выберите верные утверждения.
1) расстояние от точки $C_1$ до прямой $AB$ равно длине отрезка $BC_1$;
2) расстояние от точки $C_1$ до прямой $AB$ равно длине отрезка $C_1M$;
3) расстояние от точки $A$ до прямой $BC$ равно длине отрезка $AB$;
4) расстояние между прямыми $BB_1$ и $CC_1$ равно длине отрезка $BC_1$;
5) расстояние между прямыми $A_1B_1$ и $AB$ равно длине отрезка $AA_1$;
6) расстояние от точки $B$ до прямой $AC$ равно длине отрезка $BC$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`,
+ answer: '135', ansShow: '1, 3, 5',
+ sol: R`$1)$ верно: $AB\perp BC$ и $AB\perp BB_1$, поэтому $AB$ перпендикулярна плоскости $BB_1C_1C$, и расстояние от $C_1$ до $AB$ равно $BC_1$. $\ 2)$ неверно. $\ 3)$ верно: $BC\perp AB$, расстояние от $A$ до $BC$ равно $AB$. $\ 4)$ неверно: расстояние между $BB_1$ и $CC_1$ равно $BC$. $\ 5)$ верно: $A_1B_1\parallel AB$, расстояние равно боковому ребру $AA_1$. $\ 6)$ неверно. Подходят 1, 3, 5.`,
+ ref: 'Латотин «Геометрия, 10 кл.», разд. 3' },
+
+ { idx: 12, type: 'long', topic: 'functions', subtopic: 'fn-properties', diff: 2,
+ text: R`Функция задана формулой $f(x)=x^{2}+4x-5$ на множестве действительных чисел. Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.
Начало:
А) Сумма координат точки пересечения графика данной функции с осью ординат равна …
Б) Сумма нулей данной функции равна …
В) Наименьшее значение данной функции на области определения равно …
Окончание:
1) $9$; 2) $-4$; 3) $5$; 4) $-9$; 5) $-5$; 6) $4$.
Ответ запишите сочетанием букв и цифр, например: А1Б1В4.`,
+ answer: 'А5Б2В4', ansShow: 'А5Б2В4',
+ sol: R`А) График пересекает ось ординат в точке $(0;f(0))=(0;-5)$; сумма координат $0+(-5)=-5$ — окончание 5. Б) Нули: $x^{2}+4x-5=0$, $x=1$ и $x=-5$; их сумма $-4$ — окончание 2. В) Наименьшее значение $f(-2)=4-8-5=-9$ — окончание 4.`,
+ ref: 'Арефьева «Алгебра, 8 кл.», гл. 3' },
+
+ { idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
+ text: R`Найдите сумму всех натуральных чисел, которые кратны $9$ и больше $141$, но меньше $170$.`,
+ answer: '459',
+ sol: R`Кратные $9$ в промежутке $(141;170)$: $144$, $153$, $162$. Их сумма $144+153+162=459$.`,
+ ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1' },
+
+ { idx: 14, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
+ text: R`Найдите значение выражения $\operatorname{ctg}^{2}\alpha$, если $\sin\alpha=\dfrac15$.`,
+ answer: '24',
+ sol: R`$\cos^{2}\alpha=1-\sin^{2}\alpha=1-\dfrac{1}{25}=\dfrac{24}{25}$. Тогда $\operatorname{ctg}^{2}\alpha=\dfrac{\cos^{2}\alpha}{\sin^{2}\alpha}=\dfrac{24/25}{1/25}=24$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 4' },
+
+ { idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
+ text: R`Радиус окружности, описанной около прямоугольного треугольника $ABC$ ($\angle ABC=90^\circ$), равен $18\sqrt2$. Найдите значение выражения $90\cdot\cos\angle ACB$, если $BC=6\sqrt2$.`,
+ answer: '15',
+ sol: R`Гипотенуза $AC=2R=36\sqrt2$. $\cos\angle ACB=\dfrac{BC}{AC}=\dfrac{6\sqrt2}{36\sqrt2}=\dfrac16$. Тогда $90\cdot\dfrac16=15$.`,
+ ref: 'Казаков «Геометрия, 9 кл.», гл. 2' },
+
+ { idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
+ text: R`Пятый член геометрической прогрессии равен $48$, а шестой её член равен $96$. Найдите сумму четырёх первых членов этой прогрессии.`,
+ answer: '45',
+ sol: R`Знаменатель $q=\dfrac{96}{48}=2$. Из $b_5=b_1 q^{4}=48$: $b_1=\dfrac{48}{16}=3$. Сумма $S_4=\dfrac{b_1(q^{4}-1)}{q-1}=\dfrac{3(16-1)}{1}=45$.`,
+ ref: 'Арефьева «Алгебра, 9 кл.», гл. 4, § 18' },
+
+ { idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
+ text: R`Проездной билет на автобус на месяц стоит $39$ р., а стоимость билета на одну поездку на автобусе равна $80$ к. Сколько поездок на автобусе совершила Маша за месяц, покупая только билеты на одну поездку, если известно, что $75\%$ от суммы денег, которую она потратила за месяц на оплату поездок, равны стоимости проездного билета на месяц?`,
+ answer: '65',
+ sol: R`Пусть $n$ — число поездок. Потрачено $80n$ копеек; $39$ р. $=3900$ к. По условию $0{,}75\cdot80n=3900$, $60n=3900$, $n=65$.`,
+ ref: 'Герасимов «Математика, 6 кл.», гл. 2' },
+
+ { idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
+ text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-3\le2-\dfrac{3x-2}{2}<27$.`,
+ answer: '-11',
+ sol: R`Вычтем $2$: $-5\le-\dfrac{3x-2}{2}<25$. Умножим на $-2$ (знаки меняются): $10\ge3x-2>-50$, то есть $-48<3x\le12$, $-1656$.`,
+ answer: '17',
+ sol: R`$8^{\,2x-32}=2^{\,6x-96}$, $4^{\,3x-49}=2^{\,6x-98}$. Неравенство: $2^{\,6x-98}(2^{2}+10)>56$, $14\cdot2^{\,6x-98}>56$, $2^{\,6x-98}>4=2^{2}$, $6x-98>2$, $x>\dfrac{100}{6}$. Наименьшее целое решение $17$.`,
+ ref: 'Арефьева «Алгебра, 11 кл.», гл. 2, § 6' },
+
+ { idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
+ text: R`Найдите (в градусах) сумму различных корней уравнения $2\sin3x\cos3x-\sin6x\sin10x=0$ на промежутке $(-150^\circ;-55^\circ)$.`,
+ answer: '-567',
+ sol: R`$2\sin3x\cos3x=\sin6x$, поэтому $\sin6x(1-\sin10x)=0$. Из $\sin6x=0$: $x=30^\circ n$ — на промежутке корни $-120^\circ,-90^\circ,-60^\circ$. Из $\sin10x=1$: $x=9^\circ+36^\circ n$ — корни $-135^\circ,-99^\circ,-63^\circ$. Сумма всех различных корней: $-120-90-60-135-99-63=-567$.`,
+ ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 8' },
+
+ { idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
+ text: R`Найдите произведение наименьшего целого решения на наибольшее целое решение неравенства $\log_3^{2}(x+12)-\log_3(x+12)-6<0$.`,
+ answer: '-154',
+ sol: R`Пусть $u=\log_3(x+12)$: $u^{2}-u-6<0$, $(u-3)(u+2)<0$, $-2Ответ: ${ans}`;
+ if (t.ref) html += `Учебник: ${t.ref}
`;
+ return html;
+}
+
+/* ── Самопроверка (повтор логики checkAnswerServer из exam-prep.js) ────────── */
+const EPS = 1e-6;
+function srvToNumber(s) {
+ if (s == null) return NaN;
+ let t = String(s).trim().replace(/\$/g, '').replace(/\s+/g, '').replace(',', '.');
+ const f = t.match(/^(-?\d+(?:\.\d+)?)\s*\/\s*(-?\d+(?:\.\d+)?)$/);
+ if (f) { const n = Number(f[1]), d = Number(f[2]); return d === 0 ? NaN : n / d; }
+ const n = Number(t); return Number.isFinite(n) ? n : NaN;
+}
+function checkAnswerServer(userInput, canonical) {
+ if (userInput == null || canonical == null) return false;
+ const c = String(canonical).trim();
+ if (/^[а-д]$/.test(c)) return String(userInput).trim().toLowerCase() === c.toLowerCase();
+ if (/^[^;]+;[^;]+$/.test(c)) return false;
+ const cn = srvToNumber(c), un = srvToNumber(userInput);
+ if (Number.isNaN(cn) || Number.isNaN(un)) return false;
+ return Math.abs(cn - un) < EPS;
+}
+
+/* ── Валидация набора ──────────────────────────────────────────────────────── */
+const problems = [];
+if (TASKS.length !== 30) problems.push(`Ожидалось 30 заданий, получено ${TASKS.length}`);
+const seen = new Set();
+for (const t of TASKS) {
+ if (seen.has(t.idx)) problems.push(`Дубль task_idx=${t.idx}`); seen.add(t.idx);
+ if (t.idx < 1 || t.idx > 30) problems.push(`task_idx вне 1..30: ${t.idx}`);
+ if (!['mc', 'open', 'long'].includes(t.type)) problems.push(`#${t.idx}: тип ${t.type}`);
+ if (t.type === 'mc') {
+ if (!Array.isArray(t.opts) || t.opts.length !== 5) problems.push(`#${t.idx}: mc должен иметь 5 вариантов`);
+ if (!t.opts.some(o => o[0] === t.answer)) problems.push(`#${t.idx}: answer "${t.answer}" не среди меток`);
+ }
+ if (!t.text || !t.sol) problems.push(`#${t.idx}: пустой text/sol`);
+ if (t.type !== 'long' && !checkAnswerServer(t.answer, t.answer))
+ problems.push(`#${t.idx}: answer "${t.answer}" не проходит self-check (Unicode-минус? пробел?)`);
+ if (/−/.test(String(t.answer))) problems.push(`#${t.idx}: Unicode-минус в answer`);
+}
+
+/* ── Экспорт для тестов/тиража (без запуска main при require) ──────────────── */
+module.exports = { TASKS, buildSolution, ansShowOf, checkAnswerServer, EXAM, VARIANT, PROV };
+if (require.main !== module) return;
+
+/* ── Открытие БД ───────────────────────────────────────────────────────────── */
+const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
+const db = new DatabaseSync(DB);
+
+const track = db.prepare(`SELECT exam_key, variants_count FROM exam_tracks WHERE exam_key=?`).get(EXAM);
+if (!track) { console.error(`✗ Трек '${EXAM}' не найден в exam_tracks. Прерывание.`); process.exit(1); }
+
+/* ── DRY-RUN сводка ────────────────────────────────────────────────────────── */
+console.log(`\n=== seed_ctmath_ce2024_v1 (${PROV}) variant=${VARIANT} ===`);
+console.log(`Режим: ${APPLY ? 'APPLY (запись)' : 'DRY-RUN (только проверка)'}\n`);
+
+const byType = TASKS.reduce((a, t) => (a[t.type] = (a[t.type] || 0) + 1, a), {});
+console.log('Типы:', JSON.stringify(byType), '\n');
+
+console.log('idx | type | subtopic | d | answer');
+console.log('----+------+-----------------------+---+----------');
+for (const t of TASKS) {
+ console.log(`${String(t.idx).padStart(3)} | ${t.type.padEnd(4)} | ${String(t.subtopic).padEnd(21)} | ${t.diff} | ${String(t.answer)}`);
+}
+
+if (problems.length) {
+ console.error(`\n✗ ПРОБЛЕМЫ (${problems.length}):`);
+ problems.forEach(p => console.error(' - ' + p));
+ console.error('\nЗапись отменена из-за ошибок валидации.');
+ db.close();
+ process.exit(1);
+}
+console.log('\n✓ Валидация и self-check ответов пройдены (30/30).');
+
+/* ── APPLY: upsert ─────────────────────────────────────────────────────────── */
+if (!APPLY) {
+ console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_ce2024_v1.js --apply\n');
+ db.close();
+ process.exit(0);
+}
+
+const upsert = db.prepare(`
+ INSERT INTO exam_tasks
+ (exam_key, variant, task_idx, task_type, text_html, figure_html,
+ opts_json, answer, solution_html, topic, subtopic, difficulty)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ON CONFLICT(exam_key, variant, task_idx) DO UPDATE SET
+ task_type = excluded.task_type,
+ text_html = excluded.text_html,
+ figure_html = excluded.figure_html,
+ opts_json = excluded.opts_json,
+ answer = excluded.answer,
+ solution_html = excluded.solution_html,
+ topic = excluded.topic,
+ subtopic = excluded.subtopic,
+ difficulty = excluded.difficulty
+`);
+
+let n = 0;
+db.exec('BEGIN');
+try {
+ for (const t of TASKS) {
+ upsert.run(
+ EXAM, VARIANT, t.idx, t.type,
+ t.text,
+ t.fig || null,
+ t.type === 'mc' ? JSON.stringify(t.opts) : null,
+ t.answer,
+ buildSolution(t),
+ t.topic, t.subtopic, t.diff
+ );
+ n++;
+ }
+ const distinct = db.prepare(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN 101 AND 1999`).get(EXAM).c;
+ db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(distinct, EXAM);
+ db.exec('COMMIT');
+ console.log(`\n✓ Записано/обновлено ${n} заданий (variant=${VARIANT}).`);
+ console.log(`✓ exam_tracks.variants_count = ${distinct} (различных вариантов).`);
+ console.log(`\nПробник доступен: /exam-prep/ctmath → «Варианты» → «ЦЭ-2024».\n`);
+} catch (e) {
+ db.exec('ROLLBACK');
+ console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
+ process.exitCode = 1;
+}
+db.close();
diff --git a/backend/src/routes/exam-prep.js b/backend/src/routes/exam-prep.js
index 9107a93..8c708e5 100644
--- a/backend/src/routes/exam-prep.js
+++ b/backend/src/routes/exam-prep.js
@@ -45,6 +45,7 @@ const VARIANT_LABEL = {
108: 'РТ-2022/23 · этап II',
109: 'РТ-2022/23 · этап III',
110: 'ЦТ-2014',
+ 111: 'ЦЭ-2024',
},
};
const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;