From 59c691dcfca169eb2f8d3ff03b1ecfdf03bc8955 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 20 Jun 2026 12:09:17 +0300 Subject: [PATCH] =?UTF-8?q?feat(ctmath):=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0?= =?UTF-8?q?=D0=BD=D1=82=20116=20=E2=80=94=20=D0=A6=D0=A2-2020=20(32=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D1=8F,=20=D1=84=D0=BE?= =?UTF-8?q?=D1=80=D0=BC=D0=B0=D1=82=20=D0=901-=D0=9020)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пробник ЦТ по математике 2020, Вариант 1. ⚠️ Новый формат: 32 задания (А1–А20 + В1–В12), В1 на соответствие, В2 множ.выбор. Машинерия параметризована N_TASKS=32. Источник: чистый PDF ЦТ 2020.pdf. ВСЕ 32 ответа решены и сверены с официальной таблицей (стр.44, столбец Вариант 1) — полное совпадение, включая A20=37√13/3, B5=-335, B8=-320, B9=160, B10=577, B11=-16, B12=336 (сфера через 4 точки куба). Фигурные A9/A11 реконструированы; без авторских ссылок (политика «все учебники наши»). VARIANT_LABEL 116 -> 'ЦТ-2020'. DRY-RUN 32/32, self-check и структурный KaTeX — зелёные. Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2020_v1.js --apply Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/scripts/seed_ctmath_ct2020_v1.js | 363 +++++++++++++++++++++++ backend/src/routes/exam-prep.js | 1 + 2 files changed, 364 insertions(+) create mode 100644 backend/scripts/seed_ctmath_ct2020_v1.js 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}`;