diff --git a/backend/scripts/seed_ctmath_ct2021_v1.js b/backend/scripts/seed_ctmath_ct2021_v1.js new file mode 100644 index 0000000..65e3cf7 --- /dev/null +++ b/backend/scripts/seed_ctmath_ct2021_v1.js @@ -0,0 +1,363 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + seed_ctmath_ct2021_v1.js + Чистый вариант-пробник для трека exam-prep `ctmath`. + + Источник: Централизованное тестирование (ЦТ) по математике, 2021, Вариант 1. + Формат 2021: Часть А = А1–А18, Часть В = В1–В14. Всего **32 задания**. + ⚠️ А12 и А16 — с НЕСКОЛЬКИМИ верными ответами; В2,В3 — множественный выбор; В1 — + на установление соответствия. Перенабрано вручную в KaTeX по PDF: + F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2021.pdf (10 вариантов, табл. ответов стр.45). + + ⚠️ Ответы решены самостоятельно и СВЕРЕНЫ с официальной таблицей (стр.45, столбец + «Вариант 1»): ВСЕ 32 совпали, включая B9=324, B11=960, B13=460, B14=1375. variant=117. + + Реконструкции/адаптации заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка): + • А7 (график) → множество $f(x)\le-3$ задано промежутками ($8$ целых $x$); + • А17 (медиана на сетке) → координаты вершин заданы (медиана из B: $7y=4x-3$); + • В1 (диаграмма посещений) → данные в figure_html-таблице; + • В4 (загон на пастбище) → размеры $a,2a$ и сторона $a+140$ заданы текстом ($800$); + • В2/В3 — утверждения текстом (как в оригинале). + ⚠️ В5: в скане `∛(-7)` — на деле `∛(-343)=-7` (иначе ответ нецелый), официальный ответ -98. + А12/А16 (несколько верных) → тип open, ответ = номера в порядке возрастания ('12','15'). + Без авторских ссылок (политика «все учебники наши»). + + Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx). + Запуск: + node backend/scripts/seed_ctmath_ct2021_v1.js # DRY-RUN (по умолчанию) + node backend/scripts/seed_ctmath_ct2021_v1.js --apply # запись в БД + + ⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется. + ─────────────────────────────────────────────────────────────────────────── */ + +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const APPLY = process.argv.includes('--apply'); +const EXAM = 'ctmath'; +const VARIANT = 117; +const N_TASKS = 32; +const PROV = 'ЦТ–2021, Вариант 1'; +const R = String.raw; + +const L = ['а', 'б', 'в', 'г', 'д']; +const mc = (...html) => html.map((h, i) => [L[i], h]); +const TD = 'style="border:1px solid #99a;padding:3px 10px"'; + +/* ── 32 задания ─────────────────────────────────────────────────────────── */ +const TASKS = [ + // ── Часть A: А1–А18 ────────────────────────────────────────────────────── + { idx: 1, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1, + text: R`Треугольник $ABC$ — равнобедренный с основанием $AB$, угол при вершине $C$ равен $56^\circ$. Найдите градусную меру угла $BAC$.`, + opts: mc('$62^\circ$', '$68^\circ$', '$34^\circ$', '$64^\circ$', '$28^\circ$'), + answer: 'а', + sol: R`Углы при основании равны: $\angle BAC=\dfrac{180^\circ-56^\circ}{2}=62^\circ$.` }, + + { idx: 2, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1, + text: R`Среди дробей $\dfrac{13}{7}$; $\dfrac{15}{7}$; $\dfrac{30}{7}$; $\dfrac{27}{7}$; $\dfrac{18}{7}$ укажите ту, которая равна дроби $4\tfrac27$.`, + opts: mc('$\dfrac{13}{7}$', '$\dfrac{15}{7}$', '$\dfrac{30}{7}$', '$\dfrac{27}{7}$', '$\dfrac{18}{7}$'), + answer: 'в', + sol: R`$4\tfrac27=\dfrac{4\cdot7+2}{7}=\dfrac{30}{7}$.` }, + + { idx: 3, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1, + text: R`Даны пары значений переменных $x$ и $y$: $(3;9)$, $(-15;3)$, $(0;12)$, $(14;-2)$, $(6;6)$. Укажите пару, которая НЕ является решением уравнения $x+y=12$.`, + opts: mc('$(3;9)$', '$(-15;3)$', '$(0;12)$', '$(14;-2)$', '$(6;6)$'), + answer: 'б', + sol: R`$(-15)+3=-12\ne12$, поэтому пара $(-15;3)$ не является решением.` }, + + { idx: 4, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1, + text: R`Среди чисел $-7$; $-11$; $11$; $-1$; $0$ укажите то, которое не меньше $-9$ и не больше $-2$.`, + opts: mc('$-7$', '$-11$', '$11$', '$-1$', '$0$'), + answer: 'а', + sol: R`Условие $-9\le x\le-2$ выполнено только для $-7$.` }, + + { idx: 5, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1, + text: R`Точка $C$ делит отрезок $AB$ в отношении $5:3$, считая от точки $A$. Если длина отрезка $AB$ равна $24$, то длина отрезка $CB$ равна:`, + opts: mc('$14{,}4$', '$9{,}6$', '$6$', '$9$', '$15$'), + answer: 'г', + sol: R`$AC:CB=5:3$, поэтому $CB=24\cdot\dfrac{3}{8}=9$.` }, + + { idx: 6, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2, + text: R`В магазин поступило $43$ коробки с маслом по $110$ пачек масла в каждой. Какое наименьшее количество пачек масла необходимо продавать ежедневно, чтобы масло было распродано не более чем за $60$ дней?`, + opts: mc('$78$', '$81$', '$79$', '$83$', '$77$'), + answer: 'в', + sol: R`Всего $43\cdot110=4730$ пачек. $\dfrac{4730}{60}=78{,}8\ldots$, поэтому ежедневно нужно продавать не менее $79$ пачек.` }, + + { idx: 7, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2, + text: R`На промежутке $[-6;6]$ функция $y=f(x)$ удовлетворяет неравенству $f(x)\le-3$ ровно при $x\in[-5;-2]\cup[1;4]$. Найдите количество целых значений $x$, при которых $f(x)\le-3$.`, + opts: mc('$7$', '$6$', '$5$', '$9$', '$8$'), + answer: 'д', + sol: R`Целые $x$ из $[-5;-2]$: $-5,-4,-3,-2$ (четыре); из $[1;4]$: $1,2,3,4$ (четыре). Всего $8$.` }, + + { idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2, + text: R`Результат упрощения выражения $|a-6|-|a|$ при $\dfrac16$1)\ y=0{,}2x^{2}$; $\ 2)\ y=8^{\frac{x^{4}-16}{2|x|}}$; $\ 3)\ y=-\dfrac3x$; $\ 4)\ y=x^{2}-x+2$; $\ 5)\ y=\sin2x$.`, + answer: '12', + sol: R`Чётны функции $1$ ($y=0{,}2x^{2}$) и $2$ (показатель $\frac{x^{4}-16}{2|x|}$ — чётная функция). Функции $3$ и $5$ нечётны, $4$ — общего вида.` }, + + { idx: 13, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3, + text: R`Площадь прямоугольного треугольника равна $2$, а радиус описанной около него окружности равен $R$. Укажите номер формулы, которой может выражаться сумма катетов $a$ и $b$.
$1)\ a+b=\dfrac{R^{2}+4}{R}$; $\ 2)\ a+b=\sqrt{R^{2}+2}$; $\ 3)\ a+b=2\sqrt{R^{2}+4}$; $\ 4)\ a+b=\dfrac{R^{2}+2}{R}$; $\ 5)\ a+b=2\sqrt{R^{2}+2}$.`, + opts: mc('$\dfrac{R^{2}+4}{R}$', '$\sqrt{R^{2}+2}$', '$2\sqrt{R^{2}+4}$', '$\dfrac{R^{2}+2}{R}$', '$2\sqrt{R^{2}+2}$'), + answer: 'д', + sol: R`Гипотенуза $c=2R$, площадь $\tfrac12 ab=2$, значит $ab=4$. $(a+b)^{2}=a^{2}+b^{2}+2ab=4R^{2}+8$, поэтому $a+b=2\sqrt{R^{2}+2}$.` }, + + { idx: 14, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3, + text: R`Основанием прямой треугольной призмы $ABCA_1B_1C_1$ является треугольник $ABC$, в котором $\angle A=20^\circ$, $\angle C=25^\circ$, а радиус описанной около него окружности равен $\sqrt7$. Найдите длину диагонали грани $AA_1C_1C$, если площадь этой грани равна $2\sqrt{35}$.`, + opts: mc('$3\sqrt3$', '$\sqrt5$', '$2\sqrt6$', '$4\sqrt6$', '$9\sqrt3$'), + answer: 'в', + sol: R`$\angle B=135^\circ$, $AC=2R\sin B=2\sqrt7\cdot\dfrac{\sqrt2}{2}=\sqrt{14}$. Из $AC\cdot AA_1=2\sqrt{35}$: $AA_1=\sqrt{10}$. Диагональ грани $\sqrt{AC^{2}+AA_1^{2}}=\sqrt{14+10}=2\sqrt6$.` }, + + { idx: 15, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2, + text: R`Парабола $y=2x^{2}+bx+c$ пересекает ось абсцисс в точках с абсциссами $3$ и $4$. Найдите сумму $b+c$.`, + opts: mc('$12$', '$5$', '$20$', '$10$', '$14$'), + answer: 'г', + sol: R`$y=2(x-3)(x-4)=2x^{2}-14x+24$, поэтому $b=-14$, $c=24$ и $b+c=10$.` }, + + { idx: 16, type: 'open', topic: 'equations', subtopic: 'eq-quadratic', diff: 3, + text: R`Укажите номера уравнений, которые являются равносильными (запишите цифрами в порядке возрастания).
$1)\ (x-6)(x+6)=0$; $\ 2)\ \sqrt{x+10}=2$; $\ 3)\ x^{2}+36=0$; $\ 4)\ \dfrac{x-x^{2}-5}{4}+\dfrac{x^{2}-x-3}{3}=\dfrac14$; $\ 5)\ |x|-6=0$.`, + answer: '15', + sol: R`Множества решений: 1) $\{-6;6\}$; 2) $\{-6\}$; 3) корней нет; 4) $\{-5;6\}$; 5) $\{-6;6\}$. Совпадают решения у уравнений $1$ и $5$.` }, + + { idx: 17, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3, + text: R`Треугольник $ABC$ имеет вершины $A(0;2)$, $B(6;3)$, $C(-2;-4)$ (узлы сетки). Укажите номер уравнения прямой, содержащей медиану, проведённую из вершины $B$.
$1)\ 7y=3x+3$; $\ 2)\ 5y=4x-1$; $\ 3)\ y=3$; $\ 4)\ y=5x+4$; $\ 5)\ 7y=4x-3$.`, + opts: mc('$7y=3x+3$', '$5y=4x-1$', '$y=3$', '$y=5x+4$', '$7y=4x-3$'), + answer: 'д', + sol: R`Медиана из $B$ идёт в середину $AC$ — точку $M(-1;-1)$. Прямая через $B(6;3)$ и $M(-1;-1)$ имеет угловой коэффициент $\dfrac{3-(-1)}{6-(-1)}=\dfrac47$ и уравнение $7y=4x-3$.` }, + + { idx: 18, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5, + text: R`$SABCD$ — правильная четырёхугольная пирамида, все рёбра которой равны $48$. Точка $M$ — середина ребра $SD$, точка $N$ на ребре $SC$ такова, что $CN:NS=1:3$. Найдите длину отрезка, по которому плоскость, проходящая через точки $M$ и $N$ параллельно ребру $SA$, пересекает основание $ABCD$.`, + opts: mc('$16\sqrt{13}$', '$16\sqrt{10}$', '$8\sqrt{37}$', '$12\sqrt{17}$', '$56$'), + answer: 'б', + sol: R`В координатах с центром основания секущая плоскость пересекает основание по прямой $x-3y=-24$. Внутри квадрата отрезок идёт от стороны $AD$ (точка $(-24;0)$) до стороны $BC$ (точка $(24;16)$); его длина $\sqrt{48^{2}+16^{2}}=\sqrt{2560}=16\sqrt{10}$.` }, + + // ── Часть B: В1–В14 ────────────────────────────────────────────────────── + { idx: 19, type: 'long', topic: 'word-sequences', subtopic: 'word-problems', diff: 3, + text: R`На диаграмме (см. таблицу) показано количество посещений сайта по дням недели. Для начала каждого из предложений А–В подберите его окончание $1$–$6$.
А) В какой день количество посещений было на $20$ больше, чем в предыдущий?
Б) В какой день количество посещений было на 35 % меньше, чем во вторник?
В) В какой день количество посещений было на 10 % больше, чем в предыдущий?
Окончания: 1) вторник; 2) среда; 3) четверг; 4) пятница; 5) суббота; 6) воскресенье.`, + fig: R`
Деньвтсрчтптсбвс
Посещений400440260300640660
`, + answer: 'А6Б3В2', + ansShow: 'А6Б3В2', + sol: R`А) на $20$ больше предыдущего — воскресенье ($660-640=20$), окончание 6. Б) на 35 % меньше вторника — $400\cdot0{,}65=260$ — четверг, окончание 3. В) на 10 % больше предыдущего — среда ($400\cdot1{,}1=440$), окончание 2. Ответ: А6Б3В2.` }, + + { idx: 20, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 4, + text: R`Выберите номера трёх верных утверждений (запишите цифрами в порядке возрастания).
$1)$ если $\cos(\arccos a)=\cos\left(\arccos\tfrac1{18}\right)$, то $a=\tfrac1{18}$;
$2)$ если $\cos\alpha=-\cos\tfrac{\pi}{18}$, то $\arccos(\cos\alpha)=-\tfrac{\pi}{18}$;
$3)$ если $\sin\alpha=\sin\tfrac{17\pi}{18}$, то $\arcsin(\sin\alpha)=\tfrac{17\pi}{18}$;
$4)$ если $\arccos a=\tfrac{\pi}{18}$, то $a=\cos\tfrac{\pi}{18}$;
$5)$ если $\sin\alpha=\sin\tfrac{\pi}{18}$, то $\alpha=-\tfrac{\pi}{18}$;
$6)$ если $\sin\alpha=\sin\tfrac{\pi}{18}$, то $\arcsin(\sin\alpha)=\tfrac{\pi}{18}$.`, + answer: '146', + sol: R`1) $\cos(\arccos a)=a$, значит $a=\tfrac1{18}$ — верно. 4) $\arccos a=\tfrac{\pi}{18}\Rightarrow a=\cos\tfrac{\pi}{18}$ — верно. 6) $\arcsin\left(\sin\tfrac{\pi}{18}\right)=\tfrac{\pi}{18}$ — верно. Утверждения $2,3,5$ неверны (значения арккосинуса/арксинуса лежат в своих главных промежутках). Верны $1,4,6$.` }, + + { idx: 21, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 4, + text: R`Две перпендикулярные плоскости $\alpha$ и $\beta$ пересекаются по прямой $a$, точка $A$ принадлежит плоскости $\beta$. Выберите номера трёх верных утверждений (запишите цифрами в порядке возрастания).
$1)$ любая прямая, проходящая через $A$ и пересекающая $\alpha$, пересекает прямую $a$;
$2)$ существует единственная прямая, проходящая через $A$ и перпендикулярная плоскости $\alpha$;
$3)$ прямая, проходящая через $A$ и перпендикулярная $\beta$, перпендикулярна $\alpha$;
$4)$ любая точка прямой $a$ лежит в плоскостях $\alpha$ и $\beta$;
$5)$ любая прямая, лежащая в $\alpha$ и перпендикулярная прямой $a$, перпендикулярна $\beta$;
$6)$ любая прямая, перпендикулярная прямой $a$, принадлежит плоскости $\beta$.`, + answer: '245', + sol: R`Верны: $2$ (через точку — единственная прямая, перпендикулярная плоскости), $4$ (прямая $a$ — линия пересечения), $5$ (характеристическое свойство перпендикулярных плоскостей). Утверждения $1,3,6$ неверны. Верны $2,4,5$.` }, + + { idx: 22, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3, + text: R`На пастбище квадратной формы огорожен загон — прямоугольник со сторонами $a$ и $2a$ (в метрах); сторона квадратного пастбища равна $a+140$. Найдите площадь загона (в м²), если площадь пастбища в $32$ раза больше площади загона.`, + answer: '800', + sol: R`Площадь загона $2a^{2}$, площадь пастбища $(a+140)^{2}$. Из $(a+140)^{2}=32\cdot2a^{2}=64a^{2}$ следует $a+140=8a$, $a=20$. Площадь загона $2\cdot20^{2}=800$ м².` }, + + { idx: 23, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3, + text: R`Найдите значение выражения $\sqrt8\cdot\sqrt[3]{-343}\cdot\sqrt{32}-7\cdot\dfrac{\sqrt[5]{64}}{\sqrt[5]{-2}}$.`, + answer: '-98', + sol: R`$\sqrt8\cdot\sqrt{32}=\sqrt{256}=16$, $\sqrt[3]{-343}=-7$, поэтому первое слагаемое $16\cdot(-7)=-112$. $\dfrac{\sqrt[5]{64}}{\sqrt[5]{-2}}=\sqrt[5]{-32}=-2$, поэтому $7\cdot(-2)=-14$. Значение $-112-(-14)=-98$.` }, + + { idx: 24, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 4, + text: R`Площадь боковой поверхности цилиндра равна $15\pi$. Найдите объём $V$ цилиндра, если радиус его основания больше высоты на $3{,}5$. В ответ запишите значение выражения $\dfrac{6V}{\pi}$.`, + answer: '225', + sol: R`$2\pi rh=15\pi\Rightarrow rh=7{,}5$, $r=h+3{,}5$. Тогда $(h+3{,}5)h=7{,}5$, $h=1{,}5$, $r=5$. $V=\pi r^{2}h=37{,}5\pi$, и $\dfrac{6V}{\pi}=225$.` }, + + { idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4, + text: R`Решите уравнение $\sqrt3\cos\left(\dfrac{5\pi}{18}+\pi x\right)=-1{,}5$. В ответ запишите увеличенное в $3$ раза произведение наибольшего корня (в радианах) на количество корней этого уравнения на промежутке $[3;9]$.`, + answer: '160', + sol: R`$\cos\left(\tfrac{5\pi}{18}+\pi x\right)=-\tfrac{\sqrt3}{2}$, откуда $x=\tfrac59+2k$ или $x=-\tfrac{10}{9}+2k$. На $[3;9]$ корни $\tfrac{41}{9},\tfrac{59}{9},\tfrac{77}{9},\tfrac{44}{9},\tfrac{62}{9},\tfrac{80}{9}$ — всего $6$; наибольший $\tfrac{80}{9}$. Тогда $3\cdot\tfrac{80}{9}\cdot6=160$.` }, + + { idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4, + text: R`Найдите сумму всех целых решений неравенства $\log_{0{,}3}\log_{4{,}7}\left(2^{x+9{,}1}-1\right)\ge0$.`, + answer: '-15', + sol: R`Основание $0{,}3<1$, поэтому $0<\log_{4{,}7}\left(2^{x+9{,}1}-1\right)\le1$, откуда $1<2^{x+9{,}1}-1\le4{,}7$, то есть $2<2^{x+9{,}1}\le5{,}7$. Значит $-8{,}1b$ (с $b\mid a$, $a=bq$) сумма результатов равна $q(b+1)^{2}=1521=3^{2}\cdot13^{2}$. Получаем пары $(338;2)$ и $(108;12)$. Сумма всех чисел $338+2+108+12=460$.` }, + + { idx: 32, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5, + text: R`Основанием пирамиды $SABCD$ является выпуклый четырёхугольник $ABCD$, диагонали $AC$ и $BD$ которого перпендикулярны и пересекаются в точке $O$, причём $AO=9$, $OC=16$, $BO=OD=12$. Вершина $S$ удалена на расстояние $\dfrac{61}{7}$ от каждой из прямых $AB$, $BC$, $CD$ и $AD$. Через середину высоты пирамиды параллельно её основанию проведена секущая плоскость, делящая пирамиду на две части. Найдите значение выражения $10\cdot V$, где $V$ — объём большей из частей.`, + answer: '1375', + sol: R`Стороны $AB=AD=15$, $BC=CD=20$; $AB+CD=BC+AD$, значит четырёхугольник описанный. Площадь основания $\tfrac12\cdot25\cdot24=300$, полупериметр $35$, радиус вписанной окружности $r=\tfrac{300}{35}=\tfrac{60}{7}$. Высота $h=\sqrt{\left(\tfrac{61}{7}\right)^{2}-\left(\tfrac{60}{7}\right)^{2}}=\tfrac{11}{7}$. Объём пирамиды $\tfrac13\cdot300\cdot\tfrac{11}{7}=\tfrac{1100}{7}$. Сечение на половине высоты отсекает сверху подобную пирамиду объёмом $\tfrac18$; большая часть $\tfrac78\cdot\tfrac{1100}{7}=137{,}5$. Тогда $10V=1375$.` }, +]; + +/* ── Сборка 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_ct2021_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_ct2021_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 → «Варианты» → «ЦТ-2021».\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 9fad96f..cb3a37d 100644 --- a/backend/src/routes/exam-prep.js +++ b/backend/src/routes/exam-prep.js @@ -51,6 +51,7 @@ const VARIANT_LABEL = { 114: 'ЦТ-2018', 115: 'ЦТ-2019', 116: 'ЦТ-2020', + 117: 'ЦТ-2021', }, }; const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;