diff --git a/backend/scripts/seed_ctmath_ct2020_v1.js b/backend/scripts/seed_ctmath_ct2020_v1.js
new file mode 100644
index 0000000..0a9a8af
--- /dev/null
+++ b/backend/scripts/seed_ctmath_ct2020_v1.js
@@ -0,0 +1,363 @@
+'use strict';
+/* ───────────────────────────────────────────────────────────────────────────
+ seed_ctmath_ct2020_v1.js
+ Чистый вариант-пробник для трека exam-prep `ctmath`.
+
+ Источник: Централизованное тестирование (ЦТ) по математике, 2020, Вариант 1.
+ Формат 2020: Часть А = А1–А20 (закрытые), Часть В = В1–В12 (открытые; В1 — на
+ соответствие, В2 — множественный выбор). Всего **32 задания** (не 30!).
+ Перенабрано вручную в KaTeX по PDF:
+ F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2020.pdf (10 вариантов, табл. ответов стр.44).
+
+ ⚠️ Ответы решены самостоятельно и СВЕРЕНЫ с официальной таблицей (стр.44, столбец
+ «Вариант 1»): ВСЕ 32 совпали, включая A20=37√13/3, B5=-335, B8=-320, B9=160, B10=577,
+ B11=-16, B12=336. variant=116 (после ЦТ-2019 = 115).
+
+ Реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
+ • А9 (точка и прямая на сетке) → A(-1;2), прямая l: y=-x; симметрия → (-2;1);
+ • А11 (графики плот/катер) → скорость плота и расстояние даны числами (→ 960 мин);
+ • А2/А7 — добавлены явные условия (точки на одной дуге; M,N — середины сторон);
+ • В1/В2 — данные предложений/утверждений приведены текстом (как в оригинале).
+
+ Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx). Без авторских ссылок
+ (политика «все учебники наши»).
+ Запуск:
+ node backend/scripts/seed_ctmath_ct2020_v1.js # DRY-RUN (по умолчанию)
+ node backend/scripts/seed_ctmath_ct2020_v1.js --apply # запись в БД
+
+ ⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
+ ─────────────────────────────────────────────────────────────────────────── */
+
+const { DatabaseSync } = require('node:sqlite');
+const path = require('path');
+
+const APPLY = process.argv.includes('--apply');
+const EXAM = 'ctmath';
+const VARIANT = 116;
+const N_TASKS = 32;
+const PROV = 'ЦТ–2020, Вариант 1';
+const R = String.raw;
+
+const L = ['а', 'б', 'в', 'г', 'д'];
+const mc = (...html) => html.map((h, i) => [L[i], h]);
+
+/* ── 32 задания ─────────────────────────────────────────────────────────── */
+const TASKS = [
+ // ── Часть A: А1–А20 ──────────────────────────────────────────────────────
+ { idx: 1, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
+ text: R`Укажите номер точки, которая принадлежит графику функции $y=5^{x}$.`,
+ opts: mc('$(25;2)$', '$(2;10)$', '$(5;25)$', '$(2;25)$', '$(1;0)$'),
+ answer: 'г',
+ sol: R`При $x=2$ имеем $y=5^{2}=25$, поэтому точка $(2;25)$ лежит на графике.` },
+
+ { idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
+ text: R`Вписанный угол $KML$ равен $38^\circ$. Точки $M$ и $N$ лежат на одной дуге окружности (по одну сторону от хорды $KL$). Найдите вписанный угол $KNL$.`,
+ opts: mc('$46^\circ$', '$38^\circ$', '$19^\circ$', '$52^\circ$', '$76^\circ$'),
+ answer: 'б',
+ sol: R`Вписанные углы, опирающиеся на одну и ту же дугу $KL$ с одной стороны, равны: $\angle KNL=\angle KML=38^\circ$.` },
+
+ { idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
+ text: R`Укажите номер выражения для натурального числа, содержащего $c$ десятков и $3$ единицы ($c$ — цифра).`,
+ opts: mc('$c+3$', '$3c$', '$3c+10$', '$10c+3$', '$30+c$'),
+ answer: 'г',
+ sol: R`$c$ десятков и $3$ единицы — это $10c+3$.` },
+
+ { idx: 4, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
+ text: R`Определите, на сколько неизвестное слагаемое меньше суммы, если $x+20=80$.`,
+ opts: mc('$80$', '$20$', '$60$', '$40$', '$100$'),
+ answer: 'б',
+ sol: R`Неизвестное слагаемое $x=80-20=60$, сумма равна $80$. Разность $80-60=20$.` },
+
+ { idx: 5, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
+ text: R`Среди точек $C(33)$, $D(24)$, $E(28)$, $F(43)$, $K(12)$ координатной прямой укажите точку, симметричную точке $A(5)$ относительно точки $B(19)$.`,
+ opts: mc('$C(33)$', '$D(24)$', '$E(28)$', '$F(43)$', '$K(12)$'),
+ answer: 'а',
+ sol: R`Симметричная точка имеет координату $2\cdot19-5=33$ — это точка $C(33)$.` },
+
+ { idx: 6, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2,
+ text: R`Найдите значение выражения $\left(3\tfrac17-2\right)\cdot\left(1+\tfrac34\right):9$.`,
+ opts: mc('$1\tfrac{41}{63}$', '$\tfrac{3}{28}$', '$1\tfrac{19}{252}$', '$-\tfrac{11}{36}$', '$\tfrac29$'),
+ answer: 'д',
+ sol: R`$\left(\tfrac{22}{7}-2\right)\cdot\tfrac74:9=\tfrac87\cdot\tfrac74:9=2:9=\tfrac29$.` },
+
+ { idx: 7, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
+ text: R`В треугольнике $ABC$ $\angle ABC=104^\circ$, $\angle ACB=29^\circ$. Точки $M$ и $N$ — середины сторон $BC$ и $AC$ соответственно. Найдите градусную меру угла $ANM$ четырёхугольника $ABMN$.`,
+ opts: mc('$151^\circ$', '$128^\circ$', '$119^\circ$', '$133^\circ$', '$104^\circ$'),
+ answer: 'г',
+ sol: R`$MN$ — средняя линия, поэтому $MN\parallel AB$ и $\angle MNC=\angle BAC=180^\circ-104^\circ-29^\circ=47^\circ$. Так как $A,N,C$ лежат на одной прямой, $\angle ANM=180^\circ-47^\circ=133^\circ$.` },
+
+ { idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
+ text: R`У Юры некоторое количество марок, а у Яна — в $2$ раза больше. Все марки поместили в один альбом. Среди чисел $26$, $38$, $20$, $37$, $39$ выберите то, которое может выражать количество марок в альбоме.`,
+ opts: mc('$26$', '$38$', '$20$', '$37$', '$39$'),
+ answer: 'д',
+ sol: R`Всего марок $x+2x=3x$ — число, кратное $3$. Из данных чисел кратно $3$ только $39$.` },
+
+ { idx: 9, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
+ text: R`Даны точка $A(-1;2)$ и прямая $l$, заданная уравнением $y=-x$. Найдите координаты точки, симметричной точке $A$ относительно прямой $l$.`,
+ opts: mc('$(1;1)$', '$(-1;0)$', '$(-2;1)$', '$(0;2)$', '$(-2;4)$'),
+ answer: 'в',
+ sol: R`Симметрия относительно прямой $y=-x$ переводит точку $(x;y)$ в $(-y;-x)$, поэтому $(-1;2)\to(-2;1)$.` },
+
+ { idx: 10, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
+ text: R`График уравнения $1{,}8x-0{,}6y=a$ проходит через точку $A(-2;9)$. Найдите число $a$.`,
+ opts: mc('$-9$', '$9$', '$7$', '$-18$', '$-2{,}4$'),
+ answer: 'а',
+ sol: R`$a=1{,}8\cdot(-2)-0{,}6\cdot9=-3{,}6-5{,}4=-9$.` },
+
+ { idx: 11, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
+ text: R`Из двух пунктов навстречу друг другу одновременно отправляются плот (по течению) и катер (против течения). По графику движения скорость плота (равная скорости течения) составляет $0{,}5$ км/ч, а расстояние между пунктами — $8$ км. За сколько минут плот придёт в пункт, из которого отправился катер?`,
+ opts: mc('$1020$ мин', '$960$ мин', '$510$ мин', '$900$ мин', '$480$ мин'),
+ answer: 'б',
+ sol: R`Плоту нужно пройти $8$ км со скоростью $0{,}5$ км/ч: $\dfrac{8}{0{,}5}=16$ ч $=960$ мин.` },
+
+ { idx: 12, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
+ text: R`Внесите множитель под знак корня в выражении $-x\cdot\sqrt[5]{2x^{2}}$.`,
+ opts: mc('$\sqrt[5]{2x^{3}}$', '$\sqrt[5]{2x^{7}}$', '$\sqrt[5]{-2x^{7}}$', '$\sqrt[5]{-2x^{3}}$', '$\sqrt[5]{-2x^{10}}$'),
+ answer: 'в',
+ sol: R`$-x\cdot\sqrt[5]{2x^{2}}=\sqrt[5]{(-x)^{5}\cdot2x^{2}}=\sqrt[5]{-2x^{7}}$.` },
+
+ { idx: 13, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
+ text: R`В окружности радиуса $13$ проведена хорда $AB$. Точка $M$ делит хорду $AB$ на отрезки длиной $10$ и $12$. Найдите расстояние от точки $M$ до центра окружности.`,
+ opts: mc('$11$', '$7$', '$5$', '$6$', '$8$'),
+ answer: 'б',
+ sol: R`По свойству хорд $AM\cdot MB=R^{2}-OM^{2}$: $10\cdot12=169-OM^{2}$, откуда $OM^{2}=49$, $OM=7$.` },
+
+ { idx: 14, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 3,
+ text: R`Для неравенства $(8-x)(x+3)\ge0$ укажите номера верных утверждений.
$1)$ число $0$ не является решением неравенства;
$2)$ неравенство равносильно неравенству $|x|\le8$;
$3)$ количество всех целых решений неравенства равно $12$;
$4)$ неравенство верно при $x\in[-2;3]$;
$5)$ решением неравенства является промежуток $[-8;3]$.`,
+ opts: mc('$2$ и $4$', '$3$ и $5$', '$3$ и $4$', '$1$ и $2$', '$1$ и $5$'),
+ answer: 'в',
+ sol: R`Решение неравенства — отрезок $[-3;8]$. Тогда: $0$ — решение (1 неверно); $|x|\le8$ даёт $[-8;8]$ (2 неверно); целых решений от $-3$ до $8$ ровно $12$ (3 верно); на $[-2;3]$ неравенство выполнено (4 верно); промежуток $[-3;8]$, не $[-8;3]$ (5 неверно). Верны $3$ и $4$.` },
+
+ { idx: 15, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
+ text: R`Длины диагоналей ромба являются корнями уравнения $0{,}1x^{2}-2{,}2x+7{,}4=0$. Найдите площадь ромба.`,
+ opts: mc('$22$', '$48$', '$74$', '$11$', '$37$'),
+ answer: 'д',
+ sol: R`Уравнение равносильно $x^{2}-22x+74=0$; по теореме Виета произведение корней-диагоналей $d_1d_2=74$. Площадь ромба $\tfrac12 d_1d_2=37$.` },
+
+ { idx: 16, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 3,
+ text: R`На одной стороне прямого угла с вершиной $O$ отмечены точки $A$ и $B$ так, что $OA=1{,}7$, $OB=a$, $OAА) Разность этой прогрессии равна …
Б) Первый член этой прогрессии равен …
В) Сумма первых восьми членов этой прогрессии равна …
Окончания: $1)\;2$; $\ 2)\;-13$; $\ 3)\;4$; $\ 4)\;-26$; $\ 5)\;-20$; $\ 6)\;3$.`,
+ answer: 'А6Б2В5',
+ ansShow: 'А6Б2В5',
+ sol: R`$a_9-a_5=4d=12$, $d=3$ (окончание 6). $a_{10}=a_1+9d=14$, $a_1=-13$ (окончание 2). $S_8=4(2a_1+7d)=4(-26+21)=-20$ (окончание 5). Ответ: А6Б2В5.` },
+
+ { idx: 22, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
+ text: R`Выберите номера трёх верных утверждений, если известно, что $\sin\alpha=\sin23^\circ$ и $\cos\alpha=-\cos23^\circ$ (запишите цифрами в порядке возрастания).
$1)$ $\sin(\alpha+23^\circ)=0$;
$2)$ $\operatorname{tg}\alpha>0$;
$3)$ $\operatorname{ctg}\alpha<0$;
$4)$ $\alpha$ — угол первой четверти;
$5)$ $\sin^{2}\alpha+\cos^{2}\alpha=1$;
$6)$ $\alpha=-23^\circ$.`,
+ answer: '135',
+ sol: R`Из условий $\alpha=157^\circ$ (вторая четверть). Тогда $\sin(157^\circ+23^\circ)=\sin180^\circ=0$ (1 верно); $\operatorname{tg}157^\circ<0$ (2 неверно); $\operatorname{ctg}157^\circ<0$ (3 верно); это вторая четверть (4 неверно); основное тождество всегда верно (5 верно); $\alpha\ne-23^\circ$ (6 неверно). Верны $1,3,5$.` },
+
+ { idx: 23, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
+ text: R`В каждую из трёх корзин положили одинаковое количество яблок. Если в одну из корзин добавить $19$ яблок, то в ней окажется меньше, чем в двух других корзинах вместе. Если же в эту корзину положить ещё $23$ яблока, то в ней их станет больше, чем было первоначально в трёх корзинах вместе. Сколько яблок было в каждой корзине первоначально?`,
+ answer: '20',
+ sol: R`Пусть в корзине $x$ яблок. Тогда $x+19<2x$, то есть $x>19$; и $x+19+23>3x$, то есть $x<21$. Значит $x=20$.` },
+
+ { idx: 24, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
+ text: R`В равнобедренную трапецию, площадь которой равна $115$, вписана окружность радиуса $5$. Найдите периметр трапеции.`,
+ answer: '46',
+ sol: R`Высота $h=2r=10$. Площадь $\tfrac12(a+b)h=5(a+b)=115$, откуда $a+b=23$. Для описанной около окружности трапеции сумма оснований равна сумме боковых сторон, поэтому периметр $=2(a+b)=46$.` },
+
+ { idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
+ text: R`Найдите произведение наименьшего корня (в градусах) на количество различных корней уравнения $\sin5x=\cos65^\circ$ на промежутке $(-90^\circ;90^\circ)$.`,
+ answer: '-335',
+ sol: R`$\cos65^\circ=\sin25^\circ$, поэтому $5x=25^\circ+360^\circ k$ или $5x=155^\circ+360^\circ k$, то есть $x=5^\circ+72^\circ k$ или $x=31^\circ+72^\circ k$. На $(-90^\circ;90^\circ)$ корни $-67^\circ,-41^\circ,5^\circ,31^\circ,77^\circ$ — всего $5$; наименьший $-67^\circ$. Произведение $-67\cdot5=-335$.` },
+
+ { idx: 26, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
+ text: R`Точки $N$ и $M$ лежат на сторонах $AB$ и $AD$ параллелограмма $ABCD$ так, что $AN:NB=1:2$ и $AM:MD=1:2$. Площадь треугольника $CMN$ равна $45$. Найдите площадь параллелограмма $ABCD$.`,
+ answer: '162',
+ sol: R`Пусть площадь параллелограмма равна $S$. Через векторы $\vec{AB}$ и $\vec{AD}$ площадь треугольника $CMN$ равна $\tfrac{5}{18}S$. Из $\tfrac{5}{18}S=45$ получаем $S=162$.` },
+
+ { idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 5,
+ text: R`Найдите произведение наибольшего целого отрицательного и наибольшего целого положительного решений неравенства $3\cdot16^{\frac{x^{2}-29}{-3x}}-10\cdot16^{\frac{x^{2}-29}{-6x}}>8$.`,
+ answer: '-32',
+ sol: R`Пусть $t=16^{\frac{x^{2}-29}{-6x}}>0$. Тогда $3t^{2}-10t-8>0$, $(3t+2)(t-4)>0$, значит $t>4$, то есть $\frac{x^{2}-29}{-6x}>\tfrac12$, что приводит к $\frac{x^{2}+3x-29}{x}<0$. Решение: $x<\frac{-3-5\sqrt5}{2}$ или $00$, $x\ne17$. Уравнение приводится к $\log_{18}\bigl(x\,|17-x|\bigr)=1$, то есть $x\,|17-x|=18$. При $x<17$: $x^{2}-17x+18=0$ (корни $p,q$ с $p+q=17$, $pq=18$); при $x>17$: $x=18$. Сумма квадратов $(17^{2}-2\cdot18)+18^{2}=253+324=577$.` },
+
+ { idx: 31, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 5,
+ text: R`Найдите все пары $(m,n)$ целых чисел, связанных соотношением $m^{2}+2m=n^{2}-6n+13$. Пусть $k$ — количество таких пар, $m_0$ — наименьшее из значений $m$. Найдите значение выражения $k\cdot m_0$.`,
+ answer: '-16',
+ sol: R`Равенство приводится к $(m+1)^{2}-(n-3)^{2}=5$. Полагая $a=m+1$, $b=n-3$, имеем $(a-b)(a+b)=5$; целые решения дают пары $(m;n)$: $(2;5),(2;1),(-4;1),(-4;5)$. Значит $k=4$, $m_0=-4$, и $k\cdot m_0=-16$.` },
+
+ { idx: 32, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
+ text: R`$ABCDA_1B_1C_1D_1$ — куб, длина ребра которого равна $4\sqrt6$. Сфера проходит через его вершины $B$ и $D_1$ и середины рёбер $BB_1$ и $CC_1$. Найдите площадь сферы $S$ и в ответ запишите значение выражения $\dfrac{S}{\pi}$.`,
+ answer: '336',
+ sol: R`В координатах с ребром $a$ центр сферы — $\left(\tfrac{a}{4};\tfrac{a}{2};\tfrac{a}{4}\right)$, а $R^{2}=\tfrac{7a^{2}}{8}$. При $a=4\sqrt6$ ($a^{2}=96$) получаем $R^{2}=84$, $S=4\pi R^{2}=336\pi$ и $\dfrac{S}{\pi}=336$.` },
+];
+
+/* ── Сборка solution_html ────────────────────────────────────────────────── */
+function ansShowOf(t) {
+ if (t.ansShow != null) return t.ansShow;
+ if (t.type === 'mc') return `${t.answer})`;
+ return `$${t.answer}$`;
+}
+function buildSolution(t) {
+ const ans = ansShowOf(t);
+ let html = `${t.sol}Ответ: ${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 !== N_TASKS) problems.push(`Ожидалось ${N_TASKS} заданий, получено ${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 > N_TASKS) problems.push(`task_idx вне 1..${N_TASKS}: ${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_ct2020_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 ответов пройдены (${N_TASKS}/${N_TASKS}).`);
+
+/* ── APPLY: upsert ─────────────────────────────────────────────────────────── */
+if (!APPLY) {
+ console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_ct2020_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 → «Варианты» → «ЦТ-2020».\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 a90a2be..9fad96f 100644
--- a/backend/src/routes/exam-prep.js
+++ b/backend/src/routes/exam-prep.js
@@ -50,6 +50,7 @@ const VARIANT_LABEL = {
113: 'ЦТ-2016',
114: 'ЦТ-2018',
115: 'ЦТ-2019',
+ 116: 'ЦТ-2020',
},
};
const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;