From c86d5b9ad44b0420f241b14a1ab40daa4cdb09d3 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Sat, 20 Jun 2026 10:37:13 +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=20113=20=E2=80=94=20=D0=A6=D0=A2-2016=20(30=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пробник ЦТ по математике 2016, Вариант 1 (А1–А18 + В1–В12) для трека exam-prep ctmath. Источник: чистый PDF ЦТ 2016.pdf. ВСЕ 30 ответов решены и сверены с официальной таблицей (стр.35, столбец Вариант 1) — полное совпадение, включая B5=-22, B9=712, B11=56, B12=724. Фигурные задания (А2 угол через MN||BC, А3 числа на прямой, А6 таблица, А7 площадь по координатам, А8 область значений, А11 круговая диаграмма) реконструированы/адаптированы в самодостаточные авто-проверяемые формы. VARIANT_LABEL 113 -> 'ЦТ-2016'. DRY-RUN 30/30, self-check и структурный KaTeX — зелёные. Запись в БД — пользователь: node backend/scripts/seed_ctmath_ct2016_v1.js --apply Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/scripts/seed_ctmath_ct2016_v1.js | 384 +++++++++++++++++++++++ backend/src/routes/exam-prep.js | 1 + 2 files changed, 385 insertions(+) create mode 100644 backend/scripts/seed_ctmath_ct2016_v1.js diff --git a/backend/scripts/seed_ctmath_ct2016_v1.js b/backend/scripts/seed_ctmath_ct2016_v1.js new file mode 100644 index 0000000..5ed9e94 --- /dev/null +++ b/backend/scripts/seed_ctmath_ct2016_v1.js @@ -0,0 +1,384 @@ +'use strict'; +/* ─────────────────────────────────────────────────────────────────────────── + seed_ctmath_ct2016_v1.js + Чистый вариант-пробник для трека exam-prep `ctmath`. + + Источник: Централизованное тестирование (ЦТ) по математике, 2016, Вариант 1. + Формат: Часть А = А1–А18 (закрытые, 5 вариантов), Часть В = В1–В12 (открытые). + Всего 30 заданий. Перенабрано вручную в KaTeX по PDF: + F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2016.pdf (10 вариантов, табл. ответов стр.35). + + ⚠️ Ответы решены самостоятельно и СВЕРЕНЫ с официальной таблицей ответов + (стр. 35, столбец «Вариант 1»): ВСЕ 30 совпали, включая B5=-22, B9=712, B11=56, + B12=724. variant=113 (после ЦТ-2015 = 112). + + Адаптации/реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка): + • А2 (∠ по рисунку треугольника) → та же задача с явным условием $MN\parallel BC$ + (даёт официальный ответ $33^\circ$); + • А3 (числа $0,k,t$ на прямой) → явно $0 html.map((h, i) => [L[i], h]); + +const TD = 'style="border:1px solid #99a;padding:3px 12px"'; + +/* ── 30 заданий ─────────────────────────────────────────────────────────── */ +const TASKS = [ + // ── Часть A: А1–А18 ────────────────────────────────────────────────────── + { idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1, + text: R`Определите наименьшее натуральное число, кратное $2$, которое при делении на $15$ даёт неполное частное, равное $3$.`, + opts: mc('$44$', '$50$', '$48$', '$18$', '$46$'), + answer: 'д', + sol: R`Число имеет вид $15\cdot3+r=45+r$, где $0\le r<15$. Наименьшее чётное получается при $r=1$: это $46$.`, + ref: 'Герасимов «Математика, 5 кл.», ч. 1, гл. 3' }, + + { idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2, + text: R`В треугольнике $ABC$ точки $M$ и $N$ лежат на сторонах $AB$ и $AC$ соответственно, причём $MN\parallel BC$. Известно, что $\angle ACB=38^\circ$ и $\angle AMN=109^\circ$. Найдите градусную меру угла $BAC$.`, + opts: mc('$33^\circ$', '$52^\circ$', '$26^\circ$', '$30^\circ$', '$60^\circ$'), + answer: 'а', + sol: R`Так как $MN\parallel BC$, то $\angle ABC=\angle AMN=109^\circ$ (соответственные углы). Тогда $\angle BAC=180^\circ-\angle ABC-\angle ACB=180^\circ-109^\circ-38^\circ=33^\circ$.`, + ref: 'Казаков «Геометрия, 7 кл.», гл. 3' }, + + { idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1, + text: R`На координатной прямой отмечены числа $0$, $k$, $t$, причём $0\dfrac1k$', '$3k>3t$', '$\dfrac{k}{-3}>\dfrac{t}{-3}$', '$k>t$'), + answer: 'г', + sol: R`При делении неравенства $k\dfrac{t}{-3}$. Остальные утверждения неверны.`, + ref: 'Арефьева «Алгебра, 8 кл.», гл. 1' }, + + { idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2, + text: R`Значение выражения $3^{-5}:\left(5\tfrac25\right)^{-3}$ равно:`, + opts: mc('$\dfrac{27}{125}$', '$\dfrac{4}{5}$', '$\dfrac{125}{81}$', '$\dfrac{81}{125}$', '$\dfrac{125}{243}$'), + answer: 'г', + sol: R`$5\tfrac25=\dfrac{27}{5}$. Тогда $3^{-5}:\left(\dfrac{27}{5}\right)^{-3}=3^{-5}\cdot\dfrac{3^{9}}{5^{3}}=\dfrac{3^{4}}{5^{3}}=\dfrac{81}{125}$.`, + ref: 'Арефьева «Алгебра, 7 кл.», гл. 1, § 4' }, + + { idx: 5, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 1, + text: R`Укажите формулу $n$-го члена арифметической прогрессии $(a_n)$, если $a_1=2$, $a_2=5$.`, + opts: mc('$a_n=-3n+5$', '$a_n=3n+5$', '$a_n=3n-1$', '$a_n=2n+5$', '$a_n=5n+2$'), + answer: 'в', + sol: R`$d=a_2-a_1=3$, поэтому $a_n=a_1+(n-1)d=2+3(n-1)=3n-1$.`, + ref: 'Арефьева «Алгебра, 9 кл.», гл. 4' }, + + { idx: 6, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 1, + text: R`Величины $a$ и $b$ прямо пропорциональны. Используя данные таблицы, найдите неизвестное значение величины $a$.`, + fig: R`
a1,9
b1087,6
`, + opts: mc('$32$', '$27$', '$22$', '$14$', '$56$'), + answer: 'б', + sol: R`При прямой пропорциональности $\dfrac{a}{b}$ постоянно: $\dfrac{a}{108}=\dfrac{1{,}9}{7{,}6}=0{,}25$, откуда $a=27$.`, + ref: 'Герасимов «Математика, 6 кл.», гл. 2' }, + + { idx: 7, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2, + text: R`Найдите площадь (в см²) многоугольника с вершинами $A(2;2)$, $B(9;2)$, $C(9;7)$, $D(3;7)$, $E(2;8)$ (координаты — в сантиметрах).`, + opts: mc('$35{,}5$', '$28$', '$36$', '$49$', '$35$'), + answer: 'а', + sol: R`По формуле площади многоугольника по координатам вершин (формула шнуровки) $2S=\bigl|{-}14+45+42+10-12\bigr|=71$, поэтому $S=35{,}5$ см².`, + ref: 'Казаков «Геометрия, 8 кл.», гл. 4' }, + + { idx: 8, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2, + text: R`Областью значений функции $y=f(x)$ на промежутке $(-5;5)$ является отрезок $[-4;6]$. Найдите сумму всех целых значений, которые принимает функция.`, + opts: mc('$12$', '$14$', '$7$', '$10$', '$11$'), + answer: 'д', + sol: R`Функция принимает все целые значения от $-4$ до $6$. Их сумма $-4-3-2-1+0+1+2+3+4+5+6=11$.`, + ref: 'Арефьева «Алгебра, 9 кл.», гл. 2' }, + + { idx: 9, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1, + text: R`Найдите значение выражения НОК$(12;18;36)$ + НОД$(39;52)$.`, + opts: mc('$26$', '$50$', '$48$', '$72$', '$49$'), + answer: 'д', + sol: R`НОК$(12;18;36)=36$, НОД$(39;52)=13$. Сумма $36+13=49$.`, + ref: 'Герасимов «Математика, 6 кл.», гл. 1' }, + + { idx: 10, type: 'mc', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 2, + text: R`Прямая $a$ пересекает плоскость $\alpha$ в точке $A$ и образует с плоскостью угол $60^\circ$. Точка $B$ лежит на прямой $a$, причём $AB=6\sqrt2$. Найдите расстояние от точки $B$ до плоскости $\alpha$.`, + opts: mc('$3\sqrt2$', '$3\sqrt6$', '$3\sqrt3$', '$6\sqrt6$', '$6\sqrt3$'), + answer: 'б', + sol: R`Расстояние от $B$ до плоскости равно $AB\sin60^\circ=6\sqrt2\cdot\dfrac{\sqrt3}{2}=3\sqrt6$.`, + ref: 'Латотин «Геометрия, 10 кл.», разд. 3' }, + + { idx: 11, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2, + text: R`На круговой диаграмме распределения посевных площадей секторам отвечают: ячмень — $63^\circ$, пшеница — $108^\circ$, гречиха — $36^\circ$, рожь — $18^\circ$, остальное — овёс. Сколько гектаров отведено под гречиху, если овсом засеяно на $390$ га больше, чем рожью?`, + opts: mc('$110$ га', '$150$ га', '$120$ га', '$160$ га', '$180$ га'), + answer: 'в', + sol: R`Овёс: $360^\circ-63^\circ-108^\circ-36^\circ-18^\circ=135^\circ$. Разность «овёс минус рожь» $=135^\circ-18^\circ=117^\circ$ отвечает $390$ га, поэтому $1^\circ\to\dfrac{390}{117}=\dfrac{10}{3}$ га. Гречиха: $36^\circ\cdot\dfrac{10}{3}=120$ га.`, + ref: 'Герасимов «Математика, 6 кл.», гл. 2' }, + + { idx: 12, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2, + text: R`Длины всех сторон треугольника — целые числа. Если длина одной стороны равна $1$, а другой — $3$, то периметр треугольника равен:`, + opts: mc('$7$', '$14$', '$21$', '$6$', '$8$'), + answer: 'а', + sol: R`По неравенству треугольника третья сторона $c$ удовлетворяет $|3-1|0$.`, + answer: '60', + sol: R`Основание $\dfrac{1}{15}<1$, поэтому $0<\log_{2}\log_{9}(x+15)<1$, откуда $1<\log_{9}(x+15)<2$, то есть $9Ответ: ${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_ct2016_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_ct2016_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 → «Варианты» → «ЦТ-2016».\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 b7bdbc5..c5a5679 100644 --- a/backend/src/routes/exam-prep.js +++ b/backend/src/routes/exam-prep.js @@ -47,6 +47,7 @@ const VARIANT_LABEL = { 110: 'ЦТ-2014', 111: 'ЦЭ-2024', 112: 'ЦТ-2015', + 113: 'ЦТ-2016', }, }; const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;