From 17c1c9249080d2f1e329e1140bec684113a7a057 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 19 Jun 2026 09:47:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(ctmath):=20=D1=8D=D1=82=D0=B0=D0=BB=D0=BE?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B9=20=D0=B2=D0=B0=D1=80=D0=B8=D0=B0=D0=BD?= =?UTF-8?q?=D1=82-=D0=BF=D1=80=D0=BE=D0=B1=D0=BD=D0=B8=D0=BA=20=D0=A0?= =?UTF-8?q?=D0=A2-2023/24=20=D0=AD=D1=82=D0=B0=D0=BF=20I=20(variant=20104)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 30 заданий (А1–А10 + В1–В20), перенабрано вручную в KaTeX по PDF РИКЗ (РТ-1 23/24 В1). Геометрия закодирована текстом — чертежи не нужны. Идемпотентный upsert, DRY-RUN по умолчанию, запись с --apply. Верификация: node --check, валидация 30/30, KaTeX-рендер 413/413 сегментов. + метки вариантов 104–106 (РТ-2023/24 этап I/II/III) в routes/exam-prep.js. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/scripts/seed_ctmath_rt2324_e1v1.js | 364 +++++++++++++++++++++ backend/src/routes/exam-prep.js | 3 + 2 files changed, 367 insertions(+) create mode 100644 backend/scripts/seed_ctmath_rt2324_e1v1.js diff --git a/backend/scripts/seed_ctmath_rt2324_e1v1.js b/backend/scripts/seed_ctmath_rt2324_e1v1.js new file mode 100644 index 0000000..5481bcb --- /dev/null +++ b/backend/scripts/seed_ctmath_rt2324_e1v1.js @@ -0,0 +1,364 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + seed_ctmath_rt2324_e1v1.js + Чистый вариант-пробник для трека exam-prep `ctmath`. + + Источник: РТ–2023/2024, Этап I, Вариант 1 (РИКЗ, «Тематическое + консультирование по математике»). 30 заданий: А1–А10 + В1–В20. + Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR): + F:\!Рабочие\ЦТ\Математика\Математика\РТ\2023-2024\МАТ РТ-1 23_24 В1.pdf + + variant=104 — следующий «чистый» вариант после РТ-2024/25 (101/102/103). + Геометрия закодирована текстом (стандартная разметка фигур / углы словами) — + отдельных чертежей не требуется (как у большинства существующих задач). + + Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx). + Запуск: + node backend/scripts/seed_ctmath_rt2324_e1v1.js # DRY-RUN (по умолчанию) + node backend/scripts/seed_ctmath_rt2324_e1v1.js --apply # запись в БД + + ⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code + блокирует продакшн-записи). Без --apply ничего не пишется. + ─────────────────────────────────────────────────────────────────────────── */ + +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const APPLY = process.argv.includes('--apply'); +const EXAM = 'ctmath'; +const VARIANT = 104; +const PROV = 'РТ–2023/2024, Этап I, Вариант 1'; +const R = String.raw; + +/* opts: метки кириллица а–д (как в существующих строках ctmath; checkAnswerServer + имеет ветку /^[а-д]$/). РТ-варианты 1..5 → а..д. */ +const L = ['а', 'б', 'в', 'г', 'д']; +const mc = (...html) => html.map((h, i) => [L[i], h]); + +/* ── 30 заданий ─────────────────────────────────────────────────────────── */ +const TASKS = [ + // ── Часть A: А1–А10 ────────────────────────────────────────────────────── + { idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1, + text: R`Наименьшее целое число, принадлежащее интервалу $(-13{,}8;-7)$, равно:`, + opts: mc('$-14$', '$-6$', '$-7$', '$-13$', '$-12$'), + answer: 'г', + sol: R`Интервалу $(-13{,}8;-7)$ принадлежат целые числа $-13,\,-12,\,-11,\,-10,\,-9,\,-8$. Наименьшее из них — число $-13$.`, + ref: 'Арефьева «Алгебра, 8 кл.», гл. 1, § 5' }, + + { idx: 2, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1, + text: R`Укажите номер выражения, которое является произведением числа $m$ и суммы чисел $1{,}7$ и $3{,}5$.`, + opts: mc('$1{,}7\cdot(3{,}5+m)$', '$1{,}7+3{,}5\cdot m$', '$1{,}7\cdot m+3{,}5$', '$3{,}5\cdot(1{,}7+m)$', '$m\cdot(1{,}7+3{,}5)$'), + answer: 'д', + sol: R`Произведение числа $m$ и суммы чисел $1{,}7$ и $3{,}5$ записывается как $m\cdot(1{,}7+3{,}5)$.`, + ref: 'Арефьева «Алгебра, 7 кл.», гл. 2, § 4' }, + + { idx: 3, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 2, + text: R`В параллелепипеде $ABCDA_1B_1C_1D_1$ среди отрезков $BD$, $BD_1$, $AB_1$, $A_1B_1$, $B_1B$ укажите тот, который является диагональю параллелепипеда.`, + opts: mc('$BD$', '$BD_1$', '$AB_1$', '$A_1B_1$', '$B_1B$'), + answer: 'б', + sol: R`Диагональю параллелепипеда называют отрезок, соединяющий две вершины, не принадлежащие одной грани. Среди перечисленных только $BD_1$ соединяет противоположные вершины параллелепипеда, поэтому $BD_1$ — диагональ.`, + ref: 'Латотин «Геометрия, 10 кл.», разд. 1, § 1' }, + + { idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1, + text: R`Определите, при каком из значений $x$, равных $4;\ 5;\ 1;\ 0{,}1;\ 8$, верно неравенство $\dfrac{320}{x}<50$.`, + opts: mc('$4$', '$5$', '$1$', '$0{,}1$', '$8$'), + answer: 'д', + sol: R`При $x>0$ неравенство $\dfrac{320}{x}<50$ равносильно $x>6{,}4$. Из данных чисел этому условию удовлетворяет только $x=8$.`, + ref: 'Арефьева «Алгебра, 7 кл.», гл. 3, § 18' }, + + { idx: 5, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1, + text: R`Укажите номер функции, график которой параллелен оси абсцисс.`, + opts: mc('$y=-4$', '$y=4-2x$', '$y=\dfrac{2}{x}$', '$y=4^{x}$', '$y=4x$'), + answer: 'а', + sol: R`График параллелен оси абсцисс у постоянной функции $y=b$ (угловой коэффициент $k=0$). Из предложенных это $y=-4$.`, + ref: 'Арефьева «Алгебра, 7 кл.», гл. 3, § 20' }, + + { idx: 6, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 2, + text: R`Функция задана формулой $f(x)=|x|-6$. Укажите номера верных утверждений.
1) областью определения функции является множество всех действительных чисел;
2) функция возрастает на промежутке $(-\infty;0]$;
3) функция является чётной;
4) $f(-5)=-11$;
5) $f(3)<0$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`, + answer: '135', ansShow: '1, 3, 5', + sol: R`$1)$ верно: $|x|-6$ определено при всех $x$. $\ 2)$ неверно: на $(-\infty;0]$ функция убывает. $\ 3)$ верно: $f(-x)=|-x|-6=|x|-6=f(x)$. $\ 4)$ неверно: $f(-5)=5-6=-1$. $\ 5)$ верно: $f(3)=3-6=-3<0$.`, + ref: 'Арефьева «Алгебра, 8 кл.», гл. 4, § 19' }, + + { idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2, + text: R`Фермер привёз на осеннюю ярмарку некоторое количество картофеля и продал из этого количества $102$ кг. Сколько всего килограммов картофеля привёз фермер, если после продажи осталось $\dfrac{5}{11}$ всего привезённого картофеля?`, + opts: mc('$224$ кг', '$204$ кг', '$192$ кг', '$187$ кг', '$169$ кг'), + answer: 'г', + sol: R`Проданная часть составляет $1-\dfrac{5}{11}=\dfrac{6}{11}$ всего картофеля и равна $102$ кг. Тогда всего привезено $102:\dfrac{6}{11}=\dfrac{102\cdot11}{6}=187$ кг.`, + ref: 'Герасимов «Математика, 6 кл.», гл. 3, § 10' }, + + { idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 1, + text: R`Значение выражения $\sqrt[3]{0{,}9}\cdot\sqrt[3]{30}$ равно:`, + opts: mc('$3$', '$\sqrt[3]{12}$', '$\sqrt3$', '$\sqrt[6]{12}$', '$6$'), + answer: 'а', + sol: R`По свойству корня $n$-й степени $\sqrt[3]{0{,}9}\cdot\sqrt[3]{30}=\sqrt[3]{0{,}9\cdot30}=\sqrt[3]{27}=3$.`, + ref: 'Арефьева «Алгебра, 10 кл.», гл. 2, § 14' }, + + { idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 2, + text: R`Треугольник $KMN$ — сечение треугольной пирамиды $SABC$ плоскостью, проходящей через точку $M$ — середину ребра $BC$ — параллельно плоскости $SAC$. Найдите периметр треугольника $KMN$, если каждое ребро пирамиды $SABC$ имеет длину $2\sqrt2$.`, + opts: mc('$\dfrac{2\sqrt2}{3}$', '$\dfrac{3\sqrt2}{2}$', '$3\sqrt2$', '$6\sqrt2$', '$4\sqrt2$'), + answer: 'в', + sol: R`Секущая плоскость параллельна $SAC$ и проходит через середину $M$ ребра $BC$, поэтому она пересекает рёбра $AB$ и $SB$ в их серединах $N$ и $K$. Отрезки $MN$, $MK$, $NK$ — средние линии граней, каждый равен $\dfrac12\cdot2\sqrt2=\sqrt2$. Значит, периметр $KMN$ равен $3\sqrt2$.`, + ref: 'Латотин «Геометрия, 10 кл.», разд. 1, § 3' }, + + { idx: 10, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2, + text: R`Укажите номера выражений, которые не имеют смысла.
1) $\arccos\dfrac{\sqrt3}{2}$;
2) $\arcsin\sqrt2$;
3) $\operatorname{ctg}\left(-\dfrac{3\pi}{2}\right)$;
4) $\operatorname{tg}\left(-\dfrac{3\pi}{2}\right)$;
5) $\operatorname{arctg}\dfrac{\sqrt3}{3}$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`, + answer: '24', ansShow: '2, 4', + sol: R`$1)$ имеет смысл: $\arccos\dfrac{\sqrt3}{2}=\dfrac{\pi}{6}$. $\ 2)$ не имеет смысла: $\sqrt2\notin[-1;1]$. $\ 3)$ имеет смысл: $\operatorname{ctg}\left(-\dfrac{3\pi}{2}\right)=0$. $\ 4)$ не имеет смысла: $\operatorname{tg}\left(-\dfrac{3\pi}{2}\right)$ не существует, так как $\cos\left(-\dfrac{3\pi}{2}\right)=0$. $\ 5)$ имеет смысл: $\operatorname{arctg}\dfrac{\sqrt3}{3}=\dfrac{\pi}{6}$.`, + ref: 'Арефьева «Алгебра, 10 кл.», гл. 1, § 3; § 7' }, + + // ── Часть B: В1–В20 ────────────────────────────────────────────────────── + { idx: 11, type: 'long', topic: 'numbers', subtopic: 'num-divisibility', diff: 2, + text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.
Начало:
А) Наибольший простой делитель числа $14$ равен …
Б) Наименьшее общее кратное чисел $5$ и $55$ равно …
В) Наибольший общий делитель чисел $16$ и $55$ равен …
Окончание:
1) $1$; 2) $110$; 3) $55$; 4) $5$; 5) $7$; 6) $2$.
Ответ запишите сочетанием букв и цифр, например: А1Б1В4.`, + answer: 'А5Б3В1', ansShow: 'А5Б3В1', + sol: R`А) Простые делители числа $14$ — это $2$ и $7$; наибольший равен $7$ — окончание 5. Б) Наименьшее общее кратное чисел $5$ и $55$ равно $55$ — окончание 3. В) Наибольший общий делитель чисел $16$ и $55$ равен $1$ — окончание 1.`, + ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1, § 12' }, + + { idx: 12, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 3, + text: R`$ABCDA_1B_1C_1D_1$ — куб. Отрезки $B_1D_1$ и $AD_1$ являются диагоналями граней $A_1B_1C_1D_1$ и $AA_1D_1D$ соответственно. Выберите верные утверждения.
1) прямая $C_1D_1$ перпендикулярна прямой $AD_1$;
2) прямая $B_1D_1$ параллельна прямой $BC$;
3) прямая $AD_1$ параллельна плоскости $BB_1C_1$;
4) прямая $B_1D_1$ перпендикулярна прямой $AD_1$;
5) прямая $AA_1$ параллельна прямой $B_1D_1$;
6) прямая $CC_1$ параллельна плоскости $BAA_1$.
Ответ запишите цифрами в порядке возрастания, без пробелов.`, + answer: '136', ansShow: '1, 3, 6', + sol: R`$1)$ верно: $C_1D_1$ перпендикулярна плоскости $AA_1D_1D$, значит $C_1D_1\perp AD_1$. $\ 2)$ неверно: $B_1D_1$ и $BC$ скрещиваются. $\ 3)$ верно: $AD_1\subset AA_1D_1D$, а эта грань параллельна грани $BB_1C_1C$. $\ 4)$ неверно: угол между $B_1D_1$ и $AD_1$ равен $60^\circ$. $\ 5)$ неверно: $AA_1\perp B_1D_1$. $\ 6)$ верно: $CC_1\subset CC_1D_1D$, а эта грань параллельна грани $BAA_1B_1$.`, + ref: 'Латотин «Геометрия, 10 кл.», разд. 1–2' }, + + { idx: 13, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1, + text: R`В треугольнике $ABC$ известно, что $\angle ABC=40^\circ$, $\angle BAC=3x$, $\angle ACB=x$. Найдите градусную меру угла $ACB$.`, + answer: '35', + sol: R`По теореме о сумме градусных мер углов треугольника $3x+x+40^\circ=180^\circ$, откуда $4x=140^\circ$, $x=35^\circ$. Значит, $\angle ACB=35^\circ$.`, + ref: 'Казаков «Геометрия, 7 кл.», гл. 4, § 19' }, + + { idx: 14, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2, + text: R`Арифметическая прогрессия $(a_n)$ задана формулой $n$-го члена $a_n=6-2n$. Найдите номер члена этой прогрессии, равного $-70$.`, + answer: '38', + sol: R`По условию $a_n=-70$, тогда $6-2n=-70$, $-2n=-76$, $n=38$.`, + ref: 'Арефьева «Алгебра, 9 кл.», гл. 4, § 15' }, + + { idx: 15, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2, + text: R`Найдите произведение наименьшего натурального двузначного простого числа и натурального числа, при делении которого на $5$ получается в неполном частном $13$ и в остатке $1$.`, + answer: '726', + sol: R`Наименьшее двузначное простое число — $11$. Натуральное число с неполным частным $13$ и остатком $1$ при делении на $5$ равно $13\cdot5+1=66$. Произведение: $11\cdot66=726$.`, + ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 1, § 11' }, + + { idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 2, + text: R`За керамическую плитку и её укладку заплатили $524$ рубля. Сколько стоит (в рублях) керамическая плитка, если стоимость её укладки составляет $31\%$ стоимости плитки?`, + answer: '400', + sol: R`Пусть стоимость плитки равна $x$ рублей, тогда стоимость укладки $0{,}31x$. Уравнение $x+0{,}31x=524$, $1{,}31x=524$, $x=400$.`, + ref: 'Герасимов «Математика, 6 кл.», гл. 2, § 1' }, + + { idx: 17, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3, + text: R`Найдите значение выражения $\dfrac{a^{5}-a}{a^{5}-a^{9}}$ при $a=\dfrac{1}{\sqrt[4]{18}}$.`, + answer: '-18', + sol: R`$\dfrac{a^{5}-a}{a^{5}-a^{9}}=\dfrac{a(a^{4}-1)}{a^{5}(1-a^{4})}=-\dfrac{1}{a^{4}}$. При $a=\dfrac{1}{\sqrt[4]{18}}$ имеем $a^{4}=\dfrac{1}{18}$, поэтому значение равно $-18$.`, + ref: 'Арефьева «Алгебра, 11 кл.», гл. 1, § 1' }, + + { idx: 18, type: 'open', topic: 'functions', subtopic: 'fn-graphs', diff: 2, + text: R`Дана функция $y=x^{2}$. График функции $y=g(x)$ получен из графика функции $y=x^{2}$ сдвигом вдоль оси абсцисс на $1$ единицу влево и вдоль оси ординат на $3$ единицы вниз. Найдите значение $g(-6)$.`, + answer: '22', + sol: R`Указанный сдвиг даёт $g(x)=(x+1)^{2}-3$. Тогда $g(-6)=(-6+1)^{2}-3=25-3=22$.`, + ref: 'Арефьева «Алгебра, 9 кл.», гл. 2, § 9' }, + + { idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2, + text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-130<\dfrac{4-3x}{0{,}5}<25$.`, + answer: '20', + sol: R`Умножив на $0{,}5$: $-65<4-3x<12{,}5$. Вычтя $4$: $-69<-3x<8{,}5$. Разделив на $-3$ (знаки меняются): $-2\dfrac560$ приводит к $v^{2}+5v-168\ge0$, решение $v\ge\dfrac{-5+\sqrt{697}}{2}\approx10{,}7$. Наименьшее целое значение $v=11$.`, + ref: 'Арефьева «Алгебра, 9 кл.», гл. 3, § 13' }, + + { idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5, + text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, все рёбра которой равны. Точка $N$ лежит на диагонали $A_1B$ грани $AA_1B_1B$ так, что $A_1N:NB=1:5$. Точки $M$ и $K$ лежат на рёбрах $CC_1$ и $CB$ соответственно так, что $CM:CC_1=1:4$, $CK:KB=1:3$. Найдите значение выражения $18\sqrt7\cdot\operatorname{tg}\varphi$, где $\varphi$ — угол между прямыми $C_1N$ и $KM$.`, + answer: '70', + sol: R`Пусть длина ребра призмы равна $a$. Прямая $BC_1\parallel KM$, поэтому угол между $C_1N$ и $KM$ равен углу $NC_1B$. Тогда $BC_1=a\sqrt2$, $A_1N=\dfrac{a\sqrt2}{6}$, $NB=\dfrac{5a\sqrt2}{6}$. Из треугольника $A_1BC_1$ по теореме косинусов $\cos\angle A_1BC_1=\dfrac34$; далее $C_1N=\dfrac{2a\sqrt2}{3}$ и $\cos\angle NC_1B=\dfrac{9}{16}$, $\sin\angle NC_1B=\dfrac{5\sqrt7}{16}$, $\operatorname{tg}\varphi=\dfrac{5\sqrt7}{9}$. Тогда $18\sqrt7\cdot\dfrac{5\sqrt7}{9}=2\cdot5\cdot7=70$.`, + ref: 'Латотин «Геометрия, 10 кл.», разд. 2, § 4' }, +]; + +/* ── Сборка 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 !== 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_rt2324_e1v1 (${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_rt2324_e1v1.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 → «Варианты» → «РТ-2023/24 · этап I».\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 079d7d7..2551805 100644 --- a/backend/src/routes/exam-prep.js +++ b/backend/src/routes/exam-prep.js @@ -38,6 +38,9 @@ const VARIANT_LABEL = { 101: 'РТ-2024/25 · этап I', 102: 'РТ-2024/25 · этап II', 103: 'РТ-2024/25 · этап III', + 104: 'РТ-2023/24 · этап I', + 105: 'РТ-2023/24 · этап II', + 106: 'РТ-2023/24 · этап III', }, }; const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;