feat(ct-math): уроки стереометрии (44-47) + скрипт мини-фикса 866/1248

- backend/scripts/seed_ctmath_lessons_stereo.js — 4 урока блока «Стереометрия»
  по PILOT_STEREOMETRY (расположение/сечения, многогранники, тела вращения,
  координатный метод В20) в курс 13; применён (lessons.id=44-47, 60 блоков).
- backend/scripts/fix_ctmath_misc.js — точечный фикс exam_tasks id=866
  (варианты-прямые в норму) и id=1248 (битый источник → long); dry/--apply,
  идемпотентен. Запись блокируется авто-режимом — запускает пользователь.
- README: статус (уроки стерео, сайдбар, остаток).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-15 11:36:56 +03:00
parent 1bc0cc247a
commit 623fbde38b
3 changed files with 179 additions and 6 deletions
+45
View File
@@ -0,0 +1,45 @@
'use strict';
/*
* Точечная полировка 2 mc-задач ctmath:
* - id=866: варианты-прямые вшиты в середину текста, opts = цифры-указатели →
* нормальный opts_json + чистый текст (answer сохраняем = 4).
* - id=1248: битый источник (нет верного варианта, опции не сходятся) → 'long'.
* Идемпотентно (проверяет текущее состояние). dry по умолчанию, запись --apply.
*/
const db = require('../src/db/db');
const APPLY = process.argv.includes('--apply');
const t866 = db.prepare('SELECT id,task_type,answer,opts_json FROM exam_tasks WHERE id=866').get();
const t1248 = db.prepare('SELECT id,task_type FROM exam_tasks WHERE id=1248').get();
const plan = [];
if (t866 && t866.task_type === 'mc') {
// opts уже нормальные? (значения не цифры-указатели)
let o = []; try { o = JSON.parse(t866.opts_json); } catch {}
const isDigit = o.length && o.every(p => /^[1-9]$/.test(String(p[1]).trim()));
if (isDigit) {
plan.push({
id: 866,
set: {
text_html: 'A16. Какая из прямых пересекает график функции $y=x^4-3x^2+11x$ в 11 добавочных точках?',
opts_json: JSON.stringify([['1', '$y=-3$'], ['2', '$y=-1{,}5$'], ['3', '$y=0$'], ['4', '$y=4k$'], ['5', '$y=2$']]),
answer: '4',
},
});
} else console.log('id=866 уже не цифровой — пропуск');
} else console.log('id=866 нет или уже не mc — пропуск');
if (t1248 && t1248.task_type === 'mc') {
plan.push({ id: 1248, set: { task_type: 'long', answer: null } });
} else console.log('id=1248 нет или уже не mc — пропуск');
console.log(APPLY ? '[APPLY]' : '[DRY-RUN]', 'к изменению:', plan.map(p => p.id).join(', ') || '(нет)');
for (const p of plan) console.log(' id', p.id, '→', JSON.stringify(p.set).slice(0, 160));
if (!APPLY) { console.log('DRY-RUN: запись НЕ выполнялась. Запись: --apply'); process.exit(0); }
for (const p of plan) {
const cols = Object.keys(p.set);
const sql = `UPDATE exam_tasks SET ${cols.map(c => c + '=@' + c).join(', ')} WHERE id=@id`;
db.prepare(sql).run({ ...p.set, id: p.id });
}
console.log('Обновлено:', plan.length);
@@ -0,0 +1,128 @@
'use strict';
/*
* Уроки блока «Стереометрия» курса «ЦЭ/ЦТ — Математика» (по PILOT_STEREOMETRY.md).
* 4 урока: расположение/сечения → многогранники → тела вращения → углы/расстояния.
* Форматы блоков — под рендер frontend/lesson.html (text/heading/callout esc-only;
* математика $…$/$$…$$; callout.style=info|warning|success|error). Идемпотентно.
* node backend/scripts/seed_ctmath_lessons_stereo.js [--dry]
*/
const db = require('../src/db/db');
const DRY = process.argv.includes('--dry');
const COURSE_TITLE = 'ЦЭ/ЦТ — Математика', SECTION_TITLE = 'Стереометрия';
const course = db.prepare("SELECT id FROM courses WHERE subject_slug='math' AND title=?").get(COURSE_TITLE);
if (!course) { console.error('Нет курса. Сначала seed_ctmath_course.js'); process.exit(1); }
const section = db.prepare('SELECT id FROM course_sections WHERE course_id=? AND title=?').get(course.id, SECTION_TITLE);
if (!section) { console.error('Нет секции «' + SECTION_TITLE + '»'); process.exit(1); }
const H = (text, level = 2) => ['heading', { text, level }];
const P = (text) => ['text', { text }];
const F = (tex, label) => ['formula', label ? { label, tex } : { tex }];
const CI = (text) => ['callout', { style: 'info', text }];
const CW = (text) => ['callout', { style: 'warning', text }];
const CS = (text) => ['callout', { style: 'success', text }];
const SIM = (caption) => ['sim', { simId: 'stereo', caption }];
const FC = (front, back) => ['flashcard', { front, back }];
const ORD = (question, items) => ['ordering', { question, items }];
const ACC = (title, content) => ['accordion', { title, content }];
// M26 — расположение, сечения (А2, В1)
const L1 = [
H('Прямые и плоскости в пространстве'),
P('Две прямые в пространстве: пересекаются, параллельны или скрещиваются. Прямая и плоскость: прямая лежит в плоскости, параллельна ей или пересекает её. Две плоскости: параллельны или пересекаются по прямой.'),
SIM('Покрутите фигуру: найдите линию пересечения двух плоскостей и пары скрещивающихся прямых'),
CI('Линия пересечения двух плоскостей проходит через их общие точки. В правильной пирамиде плоскости, проходящие через вершину и центр основания, пересекаются по прямой через вершину (например, $SO$).'),
F('a\\parallel b,\\ b\\subset\\alpha,\\ a\\not\\subset\\alpha \\Rightarrow a\\parallel\\alpha', 'Признак параллельности прямой и плоскости'),
CW('В задании В1 (выбор верных утверждений о расстояниях) проверяйте каждое утверждение отдельно: расстояние между скрещивающимися прямыми — это длина их общего перпендикуляра, а не любого отрезка.'),
H('Разбор А2', 3),
P('Пример. В правильной четырёхугольной пирамиде $SABCD$ ($O$ — центр основания) найдите прямую пересечения плоскостей $DSO$ и $SCB$.'),
P('Решение. Обе плоскости проходят через вершину $S$, значит линия их пересечения проходит через $S$; анализом общих точек получаем прямую $SO$.'),
CS('Метод: ищем общие точки двух плоскостей — через них проходит линия пересечения.'),
FC('Расстояние между скрещивающимися прямыми', 'Длина их общего перпендикуляра'),
FC('Линия пересечения двух плоскостей', 'Проходит через все их общие точки'),
CI('Тренажёр: задания А2 и В1 по теме «Стереометрия» в практике модуля /exam-prep/ctmath. Цель: не менее 80%.'),
];
// M27 — многогранники (В13, В17)
const L2 = [
H('Многогранники: объёмы, площади, подобие'),
F('V_{\\text{призмы}}=S_{\\text{осн}}\\cdot h,\\qquad V_{\\text{пирамиды}}=\\tfrac{1}{3}S_{\\text{осн}}\\cdot h', 'Объёмы'),
P('Сечение, параллельное основанию пирамиды, отсекает подобную фигуру. Если высота делится от вершины в отношении $k$, то линейные размеры сечения относятся к основанию как $k$, а площади — как $k^2$.'),
F('\\dfrac{S_{\\text{сеч}}}{S_{\\text{осн}}}=k^2,\\quad k=\\dfrac{\\text{высота до сечения}}{\\text{вся высота}}', 'Сечение, параллельное основанию'),
SIM('Сечение пирамиды плоскостью, параллельной основанию'),
CW('В задании В17 ловят на том, что как $k^2$ относятся именно площади, а не длины. Сначала найдите $k$ из отношения высот, затем возводите в квадрат.'),
H('Разбор В17', 3),
P('Пример. Плоскость, параллельная основанию треугольной пирамиды, делит высоту в отношении $5:3$ от вершины. Площадь сечения меньше площади основания на $39$. Найдите площадь сечения.'),
P('Решение. $k=\\dfrac{5}{5+3}=\\dfrac{5}{8}$, поэтому $\\dfrac{S_{\\text{сеч}}}{S_{\\text{осн}}}=\\dfrac{25}{64}$. Пусть $S_{\\text{осн}}=x$: $x-\\dfrac{25}{64}x=39\\Rightarrow\\dfrac{39}{64}x=39\\Rightarrow x=64$. Тогда $S_{\\text{сеч}}=25$.'),
CS('Ответ: $25$.'),
FC('$V$ пирамиды', '$\\tfrac{1}{3}S_{\\text{осн}}\\cdot h$'),
FC('Отношение площадей сечения и основания (сечение $\\parallel$ основанию)', '$k^2$, где $k$ — отношение высот от вершины'),
CI('Тренажёр: В13 и В17 по теме «Стереометрия». Цель: не менее 75%.'),
];
// M28 — тела вращения (А9, В13)
const L3 = [
H('Тела вращения: цилиндр, конус, шар'),
F('S_{\\text{сферы}}=4\\pi R^2,\\qquad V_{\\text{шара}}=\\tfrac{4}{3}\\pi R^3', 'Шар и сфера'),
F('S_{\\text{бок}}=2\\pi R h,\\qquad V=\\pi R^2 h', 'Цилиндр'),
F('S_{\\text{бок}}=\\pi R l,\\qquad V=\\tfrac{1}{3}\\pi R^2 h', 'Конус'),
SIM('Сечение цилиндра плоскостью, параллельной оси'),
CI('Сфера, касающаяся плоскости: радиус в точку касания перпендикулярен плоскости. Расстояние от центра до точки плоскости и радиус образуют прямоугольный треугольник — работает теорема Пифагора.'),
H('Разбор А9', 3),
P('Пример. Квадрат с диагональю $8$ лежит в плоскости $\\alpha$; сфера касается $\\alpha$ в точке пересечения диагоналей; расстояние от центра сферы до вершины квадрата равно $4\\sqrt2$. Найдите площадь сферы.'),
P('Решение. Полудиагональ $=4$. $R^2=(4\\sqrt2)^2-4^2=32-16=16$, $R=4$. Площадь $=4\\pi R^2=64\\pi$.'),
CS('Ответ: $64\\pi$.'),
H('Разбор В13', 3),
P('Пример. Цилиндр рассечён плоскостью, параллельной оси; в сечении квадрат площади $100$; расстояние от оси до плоскости $\\sqrt{39}$. Найдите $\\dfrac{S_{\\text{бок}}}{\\pi}$.'),
P('Решение. Сторона квадрата $=10$ (это и высота, и хорда). $R^2=(\\sqrt{39})^2+5^2=39+25=64$, $R=8$. $S_{\\text{бок}}=2\\pi\\cdot8\\cdot10=160\\pi$.'),
CS('Ответ: $160$.'),
FC('$S$ сферы', '$4\\pi R^2$'),
FC('$V$ шара', '$\\tfrac{4}{3}\\pi R^3$'),
FC('$S_{\\text{бок}}$ конуса', '$\\pi R l$'),
CI('Тренажёр: А9 и В13 по теме «Стереометрия». Цель: не менее 80% (А9) и 70% (В13).'),
];
// M29 — углы и расстояния, координатный метод (В20)
const L4 = [
H('Координатный метод: угол между прямыми'),
P('Универсальный приём для В20: ввести удобную систему координат (вершину фигуры в начало), выписать координаты нужных точек, составить направляющие векторы прямых и найти угол через косинус скалярного произведения. Если геометрия «не идёт» — считайте координатами.'),
F('\\cos\\varphi=\\dfrac{|\\vec a\\cdot\\vec b|}{|\\vec a|\\,|\\vec b|}', 'Угол между прямыми через векторы'),
F('\\vec a\\cdot\\vec b=a_xb_x+a_yb_y+a_zb_z,\\qquad |\\vec a|=\\sqrt{a_x^2+a_y^2+a_z^2}', 'Скалярное произведение и длина'),
SIM('Угол между скрещивающимися прямыми'),
ORD('Расставьте шаги решения В20 координатным методом', [
'Ввести систему координат',
'Выписать координаты точек (учесть отношения деления рёбер)',
'Составить направляющие векторы прямых',
'Найти cos φ через скалярное произведение и длины',
]),
CW('В числителе — модуль скалярного произведения (угол между прямыми не превосходит $90^\\circ$). Частые ошибки В20 — потеря модуля и неверные координаты точек деления рёбер.'),
ACC('Альтернативы (раскрыть)', 'Угол между прямой и плоскостью считают через нормаль плоскости; есть также теорема о трёх синусах. Но координатный метод универсален и почти всегда быстрее в задачах ЦТ.'),
H('Разбор В20', 3),
P('Пример. В кубе $ABCDA_1B_1C_1D_1$ с ребром $1$ найдите $8\\cos^2\\varphi$, где $\\varphi$ — угол между прямыми $AB_1$ и $BC_1$.'),
P('Решение. Координаты: $A(0;0;0)$, $B(1;0;0)$, $B_1(1;0;1)$, $C_1(1;1;1)$. Векторы $\\vec{AB_1}=(1;0;1)$, $\\vec{BC_1}=(0;1;1)$. $\\cos\\varphi=\\dfrac{|1|}{\\sqrt2\\cdot\\sqrt2}=\\dfrac{1}{2}$, поэтому $8\\cos^2\\varphi=8\\cdot\\dfrac14=2$.'),
CS('Ответ: $2$.'),
FC('Угол между прямыми (векторы)', '$\\cos\\varphi=\\dfrac{|\\vec a\\cdot\\vec b|}{|\\vec a||\\vec b|}$'),
FC('Скалярное произведение', '$a_xb_x+a_yb_y+a_zb_z$'),
FC('Длина вектора', '$\\sqrt{a_x^2+a_y^2+a_z^2}$'),
CI('Тренажёр: В20 по теме «Стереометрия» (координатный метод). Цель: не менее 60% — это самые «дорогие» баллы.'),
];
const LESSONS = [
{ title: 'Расположение прямых и плоскостей. Сечения', read: 9, blocks: L1 },
{ title: 'Многогранники: объёмы, площади, подобие', read: 11, blocks: L2 },
{ title: 'Тела вращения: цилиндр, конус, шар', read: 11, blocks: L3 },
{ title: 'Углы и расстояния: координатный метод', read: 12, blocks: L4 },
];
console.log(DRY ? '[DRY-RUN]' : '[APPLY]', `курс id=${course.id}, секция «${SECTION_TITLE}» id=${section.id}`);
const insLesson = db.prepare('INSERT INTO lessons (course_id, title, order_index, is_published, section_id, read_time) VALUES (?,?,?,1,?,?)');
const insBlock = db.prepare('INSERT INTO lesson_blocks (lesson_id, type, order_index, data) VALUES (?,?,?,?)');
LESSONS.forEach((L, i) => {
const ex = db.prepare('SELECT id FROM lessons WHERE course_id=? AND title=?').get(course.id, L.title);
if (ex) { console.log(` есть урок: «${L.title}» (id ${ex.id}) — пропуск`); return; }
if (DRY) { console.log(` + урок «${L.title}» (${L.blocks.length} блоков)`); return; }
const lid = insLesson.run(course.id, L.title, 10 + i + 1, section.id, L.read).lastInsertRowid;
L.blocks.forEach(([type, data], bi) => insBlock.run(lid, type, bi, JSON.stringify(data)));
console.log(` + урок «${L.title}» (id ${lid}, ${L.blocks.length} блоков)`);
});
console.log(DRY ? 'DRY-RUN: ничего не записано.' : 'Готово. Уроки стереометрии добавлены (черновик курса).');
+6 -6
View File
@@ -52,11 +52,11 @@
Работает на `/exam-prep/ctmath` (дашборд, темы, практика, слабые темы, пробники). Скрипт-конвертер:
`backend/scripts/seed_ctmath_exam_tasks.js`.
Также (на общих подсистемах): теория-курс `courses.id=13` (черновик) + уроки тригонометрии `4143` +
диагностика `tests.id=164` + новые темы.
Также (на общих подсистемах): теория-курс `courses.id=13` (черновик) + уроки **тригонометрии `41–43`**
и **стереометрии `44–47`** (по пилотам) + диагностика `tests.id=164` + новые темы.
Осталось:
- выдать доступ ученикам: `content_access` (content_type='exam', content_ref='ctmath') классу/ученику;
- добавить пункт сайдбара на `/exam-prep/ctmath`;
- мелкий фикс задачи `exam_tasks.id=1248` (бракованный источник);
- (опц.) дотегировать вопросы под тонкие подтемы; дополнить уроки остальных блоков.
- ✅ пункт сайдбара на `/exam-prep/ctmath` — добавлен.
- выдать доступ ученикам: `content_access` (content_type='exam', content_ref='ctmath') классу/ученику; решить видимость пункта для учеников;
- мелкий фикс задач `exam_tasks.id=866, 1248` — скрипт `backend/scripts/fix_ctmath_misc.js --apply` (запускает пользователь);
- (опц.) уроки остальных 7 блоков; колоды карточек формул; дотегировать вопросы под тонкие подтемы.