Compare commits
137 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce4f1dcec1 | |||
| 9858108556 | |||
| e53c107d83 | |||
| c49077abbc | |||
| 78aea47619 | |||
| bc0ed1892f | |||
| 40c3152fe8 | |||
| 2506a72806 | |||
| 089f93b8ee | |||
| 5b4d9324a4 | |||
| 4d2d02f080 | |||
| d15c15ef2a | |||
| dc5501d723 | |||
| 4c1ce8394c | |||
| 0640efc82c | |||
| 7562d1a77b | |||
| 1707a510a9 | |||
| e70bf819ce | |||
| 48158ea88d | |||
| fe6df8fb98 | |||
| 244df71aec | |||
| 5bb0aeb940 | |||
| dfa0535b63 | |||
| cefb5e0836 | |||
| 5eed248702 | |||
| d395e1083b | |||
| 40df8893cc | |||
| 8027d9fda0 | |||
| 43df41287f | |||
| db1db68488 | |||
| 3c45c606bf | |||
| 1649d6c2ec | |||
| 4b5be8442b | |||
| 3898080f04 | |||
| efba722977 | |||
| be9fdfa703 | |||
| 758e1bf6cb | |||
| 0d4c658d93 | |||
| 5a4bc48027 | |||
| 73ba5a3530 | |||
| a7f2ae9937 | |||
| 748b0aaab1 | |||
| 22c7b38e9a | |||
| 205290139d | |||
| c6d323ec6d | |||
| c5d440a7a9 | |||
| 1aa95a6776 | |||
| 399a222b65 | |||
| 796a2416cb | |||
| 604fa7ac0b | |||
| 38f8be9389 | |||
| c04a8c2178 | |||
| 83f0ba9c04 | |||
| d5fbd0168e | |||
| 54be84e74a | |||
| dc71d7b4d9 | |||
| d8f2a7f98d | |||
| 9d35aaf673 | |||
| bd7dd06e47 | |||
| f381873c34 | |||
| dd69c026ec | |||
| 84625cd72a | |||
| 0fb16ef85e | |||
| b9a82c326e | |||
| 70cf6b3af1 | |||
| 59ae4c1dea | |||
| de41b77ae3 | |||
| 59c691dcfc | |||
| c0af5502bf | |||
| fec638135f | |||
| 5881787492 | |||
| 7990b33fd0 | |||
| c86d5b9ad4 | |||
| 7e8082bda6 | |||
| 2e9a0ebfb1 | |||
| 27f51f1a61 | |||
| 6eefb70ce7 | |||
| 047a3a7e15 | |||
| 6c3a3fe982 | |||
| f7c5f222a3 | |||
| d63c99cae9 | |||
| 2d7833cad9 | |||
| eed8343977 | |||
| c7ef5c0448 | |||
| 82d323547f | |||
| 4aacb2d369 | |||
| 9509a67e25 | |||
| 5193fd8252 | |||
| f4d20ff10f | |||
| f856f84de0 | |||
| c0dd8ba698 | |||
| d2d379c5f5 | |||
| 494023fba7 | |||
| ddb49cf0c1 | |||
| fd656ed63f | |||
| 17c1c92490 | |||
| 824ca369bb | |||
| 70ec09382e | |||
| 2bdb0ed898 | |||
| ee6eeb0f96 | |||
| b36f708b82 | |||
| 143ae23216 | |||
| dbfcfa41ec | |||
| 9a13a19e63 | |||
| 68817cc612 | |||
| 6cd0a81d88 | |||
| 2af560b7c4 | |||
| 98894e31ad | |||
| e9fe4dabb9 | |||
| ce99c15895 | |||
| 1f461e96fd | |||
| 5e6effa8cd | |||
| 601f584181 | |||
| 9547a20875 | |||
| 24403718bf | |||
| 9382b063aa | |||
| abd1af2653 | |||
| 53ac45bccd | |||
| 477d47e9e6 | |||
| 56fc15418e | |||
| 6fed18f819 | |||
| 1cf8083c0e | |||
| 8091b48e1c | |||
| 4b23d768f2 | |||
| a982628d04 | |||
| 623fbde38b | |||
| 1bc0cc247a | |||
| 9b1abb83f8 | |||
| c79effa16a | |||
| 3a20ac8a6e | |||
| fd26efca53 | |||
| 31719b2e79 | |||
| 228bd885ed | |||
| c3816baf99 | |||
| 055a6cd1a4 | |||
| 7eb6cb2da0 | |||
| c9a00d105e |
@@ -0,0 +1,3 @@
|
||||
# Shell-скрипты исполняются в Linux-контейнерах — всегда LF (иначе «bad interpreter»).
|
||||
*.sh text eol=lf
|
||||
docker-entrypoint.sh text eol=lf
|
||||
@@ -1,4 +1,4 @@
|
||||
# BQ-System — правила для Claude
|
||||
# LearnSpace — правила для Claude
|
||||
|
||||
## Поиск по коду
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# Установка LearnSpace на TrueNAS SCALE
|
||||
|
||||
Пошаговая инструкция под **твой случай: TrueNAS SCALE, сборка образа прямо на NAS**
|
||||
(на рабочем ПК Docker не нужен). В репозитории уже есть `Dockerfile`, `docker-entrypoint.sh`
|
||||
и готовый `compose.truenas.yml`.
|
||||
|
||||
Контейнер **самоинициализируется**: при старте применяет миграции БД, при первом запуске засевает права.
|
||||
Данные (SQLite-БД, загрузки, бэкапы) лежат на датасете и переживают пересоздание/обновление контейнера.
|
||||
|
||||
> CORE (FreeBSD) Docker не умеет — см. раздел в конце. Если у тебя CORE — скажи, распишу отдельно.
|
||||
|
||||
---
|
||||
|
||||
## Шаг 1. Датасет под данные
|
||||
|
||||
TrueNAS → **Datasets → Add Dataset**: создай `tank/apps/learnspace` (вместо `tank` — имя своего пула).
|
||||
Затем создай в нём три папки (через **System → Shell** или по SMB):
|
||||
```sh
|
||||
mkdir -p /mnt/tank/apps/learnspace/{data,uploads,backups}
|
||||
```
|
||||
Здесь будут жить БД, загрузки и бэкапы. Их защищают снапшоты/репликация TrueNAS.
|
||||
|
||||
## Шаг 2. Положить код проекта на NAS
|
||||
|
||||
Самое простое — по SMB скопировать папку репозитория в датасет, например в
|
||||
`/mnt/tank/apps/learnspace/src` (нужны `Dockerfile`, `compose.truenas.yml`, `backend/`, `frontend/`, `js/`,
|
||||
`docker-entrypoint.sh`). Папку `node_modules` копировать НЕ нужно — образ ставит зависимости сам.
|
||||
|
||||
Либо, если у NAS есть доступ к git-серверу:
|
||||
```sh
|
||||
cd /mnt/tank/apps/learnspace
|
||||
git clone <адрес-репозитория> src
|
||||
```
|
||||
|
||||
## Шаг 3. Включить SSH и собрать образ
|
||||
|
||||
System → **Services → SSH** → включить. Зайти в шелл NAS (ssh root@IP_NAS) и собрать:
|
||||
```sh
|
||||
cd /mnt/tank/apps/learnspace/src
|
||||
docker build -t learnspace:latest .
|
||||
```
|
||||
(Сборка скачает зависимости и займёт пару минут.)
|
||||
|
||||
## Шаг 4. Настроить параметры запуска
|
||||
|
||||
Открой `compose.truenas.yml` (в папке src) и поправь **три вещи**:
|
||||
1. Пути `/mnt/tank/apps/learnspace/...` — под свой пул, если он не `tank`.
|
||||
2. `JWT_SECRET` — длинная случайная строка. Сгенерируй прямо на NAS:
|
||||
```sh
|
||||
openssl rand -hex 32
|
||||
```
|
||||
и вставь значение.
|
||||
3. `CLIENT_ORIGIN` — адрес, по которому будешь открывать сайт (пока `http://IP_NAS:3000`, позже домен с https).
|
||||
|
||||
## Шаг 5. Запустить
|
||||
|
||||
**Способ 1 — командой (просто):**
|
||||
```sh
|
||||
cd /mnt/tank/apps/learnspace/src
|
||||
docker compose -f compose.truenas.yml up -d
|
||||
```
|
||||
|
||||
**Способ 2 — через UI TrueNAS (интегрированно, app виден в Apps):**
|
||||
Apps → **Discover Apps → Custom App → Install via YAML** → вставь содержимое `compose.truenas.yml`
|
||||
(этот способ — для SCALE 24.10 «ElectricEel» и новее; на 24.04 «Dragonfish» и старше используй Способ 1).
|
||||
|
||||
Первый старт сам накатит миграции и засеет права — отдельных команд не нужно.
|
||||
|
||||
## Шаг 6. Проверить
|
||||
|
||||
```sh
|
||||
docker logs -f learnspace
|
||||
```
|
||||
Ожидаемо: `applying migrations...` → (на первом старте) `seeding permissions...` → `starting server...`
|
||||
→ `Server running on port 3000`. Через ~30–40 с healthcheck станет «healthy».
|
||||
|
||||
Открой в браузере **http://IP_NAS:3000** и зарегистрируй первого пользователя.
|
||||
(Нужен сразу админ — скажи, добавлю команду/скрипт создания админа.)
|
||||
|
||||
## Шаг 7. Домен и HTTPS (рекомендуется)
|
||||
|
||||
Порт 3000 — внутренний. Для домена с HTTPS поставь reverse-proxy (TrueNAS app **Nginx Proxy Manager**
|
||||
или **Traefik**, либо внешний nginx) → проксировать на `IP_NAS:3000`. В `CLIENT_ORIGIN` укажи итоговый
|
||||
публичный URL (иначе CORS заблокирует фронт). Для видеосвязи в классах за CGNAT/симметричным NAT нужен
|
||||
TURN-сервер (coturn) — пропиши `TURN_URL/TURN_USER/TURN_PASS`.
|
||||
|
||||
## Обновление версии
|
||||
|
||||
1. Обнови код в `src` (git pull или перекопируй).
|
||||
2. Пересобери и перезапусти:
|
||||
```sh
|
||||
cd /mnt/tank/apps/learnspace/src
|
||||
docker compose -f compose.truenas.yml up -d --build
|
||||
```
|
||||
Данные сохранятся (тома на датасете), новые миграции применятся автоматически.
|
||||
|
||||
## Бэкапы
|
||||
|
||||
Данные — в `/mnt/tank/apps/learnspace/{data,uploads}`. Настрой **снапшоты датасета** (+ репликацию при желании).
|
||||
SQLite-файл — `data/learnspace.db`; консистентный бэкап лучше делать снапшотом ZFS или при остановленном app.
|
||||
|
||||
---
|
||||
|
||||
## Альтернатива: собрать на ПК (если поставишь Docker Desktop)
|
||||
|
||||
```bash
|
||||
docker build -t learnspace:latest .
|
||||
docker save learnspace:latest | gzip > learnspace.tar.gz # перенести файл на NAS
|
||||
```
|
||||
На NAS: `docker load < learnspace.tar.gz`, затем в `compose.truenas.yml` заменить `build: .` на
|
||||
`image: learnspace:latest` и выполнить Шаги 4–6.
|
||||
|
||||
## TrueNAS CORE (FreeBSD) — без Docker
|
||||
|
||||
CORE контейнеры не запускает. Варианты: **Linux-VM** (bhyve) с Docker внутри → следовать инструкции выше;
|
||||
либо **jail** с Node ≥ 22: `npm ci --omit=dev` в `backend/`, прописать env, `npm run migrate &&
|
||||
npm run seed:permissions`, запускать `node src/server.js` под supervisor (pm2/rc.d).
|
||||
+8
-5
@@ -6,7 +6,7 @@ RUN npm ci --omit=dev
|
||||
|
||||
# ── Stage 2: runtime ─────────────────────────────────────────────────────
|
||||
FROM node:22-alpine
|
||||
LABEL maintainer="BQ-System"
|
||||
LABEL maintainer="LearnSpace"
|
||||
|
||||
RUN apk add --no-cache sqlite tini
|
||||
|
||||
@@ -17,9 +17,12 @@ COPY backend/src ./backend/src
|
||||
COPY backend/seed.js ./backend/seed.js
|
||||
COPY frontend ./frontend
|
||||
COPY js ./js
|
||||
COPY docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
# Ensure data & uploads dirs exist (volumes mount here)
|
||||
RUN mkdir -p /app/backend/data /app/backend/uploads /app/backups
|
||||
# Ensure data & uploads dirs exist (volumes mount here); normalize entrypoint EOL + make executable
|
||||
RUN mkdir -p /app/backend/data /app/backend/uploads /app/backups \
|
||||
&& sed -i 's/\r$//' /app/docker-entrypoint.sh \
|
||||
&& chmod +x /app/docker-entrypoint.sh
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
@@ -28,5 +31,5 @@ EXPOSE 3000
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD wget -qO- http://localhost:3000/api/health || exit 1
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "backend/src/server.js"]
|
||||
# Entrypoint применяет миграции + засев прав, затем запускает сервер (tini — PID 1, сигналы/зомби)
|
||||
ENTRYPOINT ["tini", "--", "/app/docker-entrypoint.sh"]
|
||||
|
||||
Generated
+543
@@ -21,9 +21,214 @@
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^29.1.1",
|
||||
"nodemon": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "5.1.11",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||
"integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@csstools/css-calc": "^3.2.0",
|
||||
"@csstools/css-color-parser": "^4.1.0",
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/dom-selector": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz",
|
||||
"integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/generational-cache": "^1.0.1",
|
||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||
"bidi-js": "^1.0.3",
|
||||
"css-tree": "^3.2.1",
|
||||
"is-potential-custom-element-name": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/generational-cache": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz",
|
||||
"integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/nwsapi": {
|
||||
"version": "2.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@bramus/specificity": {
|
||||
"version": "2.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-tree": "^3.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"specificity": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
|
||||
"integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz",
|
||||
"integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz",
|
||||
"integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^6.0.2",
|
||||
"@csstools/css-calc": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^4.0.0",
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz",
|
||||
"integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-syntax-patches-for-csstree": {
|
||||
"version": "1.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz",
|
||||
"integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peerDependencies": {
|
||||
"css-tree": "^3.2.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"css-tree": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz",
|
||||
"integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
||||
@@ -34,6 +239,24 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@exodus/bytes": {
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz",
|
||||
"integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@noble/hashes": "^1.8.0 || ^2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@noble/hashes": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||
@@ -608,6 +831,16 @@
|
||||
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bidi-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"require-from-string": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -912,6 +1145,34 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.27.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -921,6 +1182,13 @@
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -1011,6 +1279,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
|
||||
"integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20.19.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -1289,6 +1570,19 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
@@ -1398,6 +1692,13 @@
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
@@ -1410,6 +1711,47 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "29.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz",
|
||||
"integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^5.1.11",
|
||||
"@asamuzakjp/dom-selector": "^7.1.1",
|
||||
"@bramus/specificity": "^2.4.2",
|
||||
"@csstools/css-syntax-patches-for-csstree": "^1.1.3",
|
||||
"@exodus/bytes": "^1.15.0",
|
||||
"css-tree": "^3.2.1",
|
||||
"data-urls": "^7.0.0",
|
||||
"decimal.js": "^10.6.0",
|
||||
"html-encoding-sniffer": "^6.0.0",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"lru-cache": "^11.3.5",
|
||||
"parse5": "^8.0.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^6.0.1",
|
||||
"undici": "^7.25.0",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^8.0.1",
|
||||
"whatwg-mimetype": "^5.0.0",
|
||||
"whatwg-url": "^16.0.1",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^3.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
@@ -1501,6 +1843,16 @@
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.5.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
|
||||
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -1510,6 +1862,13 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.27.1",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||
"dev": true,
|
||||
"license": "CC0-1.0"
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
@@ -1741,6 +2100,19 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
|
||||
"integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -1789,6 +2161,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
|
||||
@@ -1881,6 +2263,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -1907,6 +2299,19 @@
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
@@ -2099,6 +2504,16 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
@@ -2164,6 +2579,33 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tldts": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz",
|
||||
"integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.4.3"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tldts-core": {
|
||||
"version": "7.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz",
|
||||
"integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -2196,6 +2638,32 @@
|
||||
"nodetouch": "bin/nodetouch.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
@@ -2235,6 +2703,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz",
|
||||
"integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
@@ -2268,12 +2746,60 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
|
||||
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@exodus/bytes": "^1.11.0",
|
||||
"tr46": "^6.0.0",
|
||||
"webidl-conversions": "^8.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
@@ -2312,6 +2838,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"import:exam-tasks": "node scripts/import-exam-tasks.js",
|
||||
"index:textbooks": "node scripts/index-textbooks.js",
|
||||
"index:textbooks:full": "node scripts/index-textbooks-headless.js",
|
||||
"test": "node --test tests/*.test.js",
|
||||
"test": "node --test --test-concurrency=1 tests/*.test.js",
|
||||
"hooks:install": "sh ../scripts/install-hooks.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -33,6 +33,7 @@
|
||||
"ws": "^8.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^29.1.1",
|
||||
"nodemon": "^3.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
check_variant_dups.js — проверка дубликатов задач в exam-prep (трек ctmath).
|
||||
|
||||
Зачем: новые варианты-пробники не должны повторять уже имеющиеся задачи в
|
||||
«общей базе» (видимый ученику пул = выверенные варианты [101;1999]).
|
||||
|
||||
Режимы:
|
||||
node backend/scripts/check_variant_dups.js
|
||||
→ аудит всего видимого пула ([101;1999]) на ВНУТРЕННИЕ точные дубли;
|
||||
node backend/scripts/check_variant_dups.js seed_ctmath_ct2017_v1.js
|
||||
→ сверяет TASKS из seed-файла с уже имеющимся видимым пулом БД
|
||||
(до --apply). Год-пачки (variant≥2011) и variant=0 в сравнении НЕ
|
||||
участвуют (они скрыты из практики фильтром exam-prep.js).
|
||||
|
||||
Точные дубли = совпадение нормализованного текста (теги/латех/пробелы убраны,
|
||||
ЧИСЛА сохранены — параллельные задачи с другими числами дублями не считаются).
|
||||
Возврат: код 0 — дублей нет; код 1 — найдены (для CI/ручной проверки).
|
||||
Только ЧТЕНИЕ БД. --apply не нужен.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const MOCK_LO = 101, MOCK_HI = 1999; // видимый пул ctmath (как в exam-prep.js)
|
||||
const EXAM = 'ctmath';
|
||||
|
||||
const norm = s => String(s || '')
|
||||
.replace(/<[^>]+>/g, ' ').replace(/&[a-z]+;/gi, ' ')
|
||||
.replace(/\$/g, '').replace(/\\[a-zA-Z]+/g, '')
|
||||
.replace(/[^0-9a-zа-яёA-ZА-ЯЁ]+/g, '').toLowerCase();
|
||||
|
||||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
const db = new DatabaseSync(DB);
|
||||
|
||||
// видимый пул: variant в [101;1999]
|
||||
const pool = db.prepare(
|
||||
`SELECT variant, task_idx, text_html FROM exam_tasks
|
||||
WHERE exam_key=? AND variant BETWEEN ? AND ?`).all(EXAM, MOCK_LO, MOCK_HI);
|
||||
const poolSig = new Map(); // sig -> [{variant,task_idx}]
|
||||
for (const r of pool) {
|
||||
const k = norm(r.text_html); if (!k) continue;
|
||||
if (!poolSig.has(k)) poolSig.set(k, []);
|
||||
poolSig.get(k).push({ variant: r.variant, task_idx: r.task_idx });
|
||||
}
|
||||
|
||||
const arg = process.argv[2];
|
||||
let problems = 0;
|
||||
|
||||
if (!arg) {
|
||||
// АУДИТ внутренних дублей пула
|
||||
const dups = [...poolSig.entries()].filter(([, a]) => a.length > 1);
|
||||
console.log(`\n=== Аудит видимого пула ctmath [${MOCK_LO};${MOCK_HI}] ===`);
|
||||
console.log(`Задач в пуле: ${pool.length}, уникальных сигнатур: ${poolSig.size}`);
|
||||
if (!dups.length) console.log('✓ Точных дублей внутри пула НЕТ.');
|
||||
else {
|
||||
problems = dups.length;
|
||||
console.log(`✗ Точных дубль-групп: ${dups.length}`);
|
||||
for (const [, a] of dups) console.log(' ' + a.map(x => `${x.variant}#${x.task_idx}`).join(' = '));
|
||||
}
|
||||
} else {
|
||||
// СВЕРКА seed-файла с пулом
|
||||
const seedPath = path.isAbsolute(arg) ? arg : path.join(__dirname, arg);
|
||||
let mod;
|
||||
try { mod = require(seedPath); } catch (e) { console.error('✗ Не загрузить seed:', e.message); process.exit(2); }
|
||||
const { TASKS, VARIANT } = mod;
|
||||
if (!Array.isArray(TASKS)) { console.error('✗ В seed нет экспорта TASKS'); process.exit(2); }
|
||||
console.log(`\n=== Сверка seed (variant=${VARIANT}, ${TASKS.length} задач) с пулом ===`);
|
||||
// исключаем САМ этот вариант из пула (если уже применён — не считать самосовпадением)
|
||||
const own = new Set();
|
||||
for (const t of TASKS) {
|
||||
const k = norm(t.text); if (!k) continue;
|
||||
const hit = (poolSig.get(k) || []).filter(x => x.variant !== VARIANT);
|
||||
if (hit.length) {
|
||||
problems++;
|
||||
console.log(` ✗ #${t.idx} дублирует: ` + hit.map(x => `${x.variant}#${x.task_idx}`).join(', '));
|
||||
}
|
||||
if (own.has(k)) { problems++; console.log(` ✗ #${t.idx} — внутренний дубль в самом варианте`); }
|
||||
own.add(k);
|
||||
}
|
||||
if (!problems) console.log(`✓ Дублей с видимым пулом нет — variant=${VARIANT} можно добавлять.`);
|
||||
}
|
||||
|
||||
db.close();
|
||||
process.exit(problems ? 1 : 0);
|
||||
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
cleanup_ctmath_bank.js — точечная чистка банка exam-prep ctmath.
|
||||
|
||||
Что делает (идемпотентно):
|
||||
1. id=1248 (вычисление 5^lg2·2^lg5): дефектная задача (варианты «а» и «д»
|
||||
одинаковы, верного ответа нет) — уже переведена в 'long'; чистим
|
||||
литеральное answer="null" → NULL.
|
||||
2. id=1419 (var 2024, «укажите номера пар»): битый mc — сохранённый ответ «а»
|
||||
(«3 и 4») противоречит решению («4 и 5»), причём «4 и 5» вообще нет среди
|
||||
вариантов; единственная подходящая пара — №4, ни один mc-вариант не верен.
|
||||
Ретайрим в 'long' (self-check): убирается из авто-проверки тренажёра/пробника
|
||||
(там берутся только mc/open), но текст и разбор сохраняются.
|
||||
3. variants_count трека ctmath → число «чистых» вариантов-пробников (variant≥101),
|
||||
чтобы шапка («N вариантов») соответствовала пикеру (год-пачки скрыты роутом).
|
||||
|
||||
Год-пачки (variant=год) НЕ удаляются — они остаются пулом задач для тренажёра
|
||||
по темам (он отбирает по subtopic). «Указательные» opts (["1","1"]…) НЕ трогаем —
|
||||
они рабочие (ученик выбирает номер).
|
||||
|
||||
Запуск: node backend/scripts/cleanup_ctmath_bank.js [--apply]
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
// Чистые варианты-пробники: 3-значные [101;1999]; год-пачки — 4-значные годы
|
||||
// (≥2011) и 0 — исключены. Совпадает с MOCK_VARIANT_RANGE.ctmath в routes/exam-prep.js.
|
||||
const MOCK_LO = 101, MOCK_HI = 1999;
|
||||
|
||||
const db = new DatabaseSync(path.join(__dirname, '..', 'data', 'learnspace.db'));
|
||||
const get = (sql, ...a) => db.prepare(sql).get(...a);
|
||||
|
||||
console.log(`\n=== cleanup_ctmath_bank (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===\n`);
|
||||
|
||||
const actions = [];
|
||||
|
||||
// 1. id=1248 answer="null" → NULL
|
||||
const t1248 = get(`SELECT id, task_type, answer FROM exam_tasks WHERE id=1248 AND exam_key=?`, EXAM);
|
||||
if (t1248 && t1248.answer === 'null') {
|
||||
actions.push({ desc: `id=1248: answer "null" → NULL (тип ${t1248.task_type})`,
|
||||
run: () => db.prepare(`UPDATE exam_tasks SET answer=NULL WHERE id=1248`).run() });
|
||||
} else {
|
||||
console.log(`• id=1248: пропуск (answer=${t1248 ? JSON.stringify(t1248.answer) : 'нет строки'})`);
|
||||
}
|
||||
|
||||
// 2. id=1419 битый mc → long, answer/opts NULL
|
||||
const t1419 = get(`SELECT id, task_type FROM exam_tasks WHERE id=1419 AND exam_key=?`, EXAM);
|
||||
if (t1419 && t1419.task_type === 'mc') {
|
||||
actions.push({ desc: `id=1419: битый mc → 'long' (answer/opts → NULL, текст и разбор сохраняются)`,
|
||||
run: () => db.prepare(`UPDATE exam_tasks SET task_type='long', answer=NULL, opts_json=NULL WHERE id=1419`).run() });
|
||||
} else {
|
||||
console.log(`• id=1419: пропуск (тип ${t1419 ? t1419.task_type : 'нет строки'})`);
|
||||
}
|
||||
|
||||
// 2b. Срезать провенанс-префикс [ЦТ YYYY · XN] из начала текста задания
|
||||
// (в чистых вариантах 101+ его нет; для консистентности убираем из год-пачек).
|
||||
// Паттерн узкий: [ + ЦТ|ЦЭ|РТ|ДРТ + год + … + ]; математические скобки внутри $…$ не задеваются.
|
||||
const reTag = /^\s*\[(?:ЦТ|ЦЭ|РТ|ДРТ)\s+\d{4}[^\]]*\]\s*/;
|
||||
const prefixed = db.prepare(`SELECT id, text_html FROM exam_tasks WHERE exam_key=? AND TRIM(text_html) LIKE '[%'`).all(EXAM)
|
||||
.filter(r => reTag.test(r.text_html))
|
||||
.map(r => ({ id: r.id, clean: r.text_html.replace(reTag, '') }))
|
||||
.filter(p => p.clean.trim().length > 0); // не обнуляем задачу
|
||||
if (prefixed.length) {
|
||||
actions.push({ desc: `срезать провенанс-префикс [ЦТ … ] у ${prefixed.length} заданий`,
|
||||
run: () => { const upd = db.prepare(`UPDATE exam_tasks SET text_html=? WHERE id=?`); for (const p of prefixed) upd.run(p.clean, p.id); } });
|
||||
} else {
|
||||
console.log('• провенанс-префиксы: пропуск (не найдено)');
|
||||
}
|
||||
|
||||
// 3. variants_count = число чистых вариантов (≥101)
|
||||
const cleanCnt = get(`SELECT COUNT(DISTINCT variant) c FROM exam_tasks WHERE exam_key=? AND variant BETWEEN ? AND ?`, EXAM, MOCK_LO, MOCK_HI).c;
|
||||
const curCnt = get(`SELECT variants_count vc FROM exam_tracks WHERE exam_key=?`, EXAM).vc;
|
||||
if (curCnt !== cleanCnt) {
|
||||
actions.push({ desc: `exam_tracks.variants_count: ${curCnt} → ${cleanCnt} (чистых вариантов [${MOCK_LO};${MOCK_HI}])`,
|
||||
run: () => db.prepare(`UPDATE exam_tracks SET variants_count=? WHERE exam_key=?`).run(cleanCnt, EXAM) });
|
||||
} else {
|
||||
console.log(`• variants_count: пропуск (уже ${curCnt})`);
|
||||
}
|
||||
|
||||
console.log(`\nК применению (${actions.length}):`);
|
||||
actions.forEach(a => console.log(' - ' + a.desc));
|
||||
|
||||
if (!actions.length) { console.log('\nНечего менять — всё уже в нужном состоянии.\n'); db.close(); process.exit(0); }
|
||||
|
||||
if (!APPLY) {
|
||||
console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/cleanup_ctmath_bank.js --apply\n');
|
||||
db.close(); process.exit(0);
|
||||
}
|
||||
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
for (const a of actions) a.run();
|
||||
db.exec('COMMIT');
|
||||
console.log(`\n✓ Применено изменений: ${actions.length}.\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка, откат:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
/* create-admin.js — создать или повысить пользователя до администратора.
|
||||
*
|
||||
* Запуск (значения через переменные окружения, чтобы пароль не светился в argv):
|
||||
* ADMIN_EMAIL=a@b.c ADMIN_PASSWORD=secret12 ADMIN_NAME="Имя" node scripts/create-admin.js
|
||||
* Используется панелью управления (control-panel.ps1, пункт «Создать админа»).
|
||||
*
|
||||
* Если пользователь с таким email уже есть — обновляет пароль/имя, ставит role='admin'
|
||||
* и инкрементит token_version (старые токены становятся недействительны). Иначе создаёт.
|
||||
*/
|
||||
const path = require('path');
|
||||
const bcrypt = require('bcryptjs');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const email = String(process.env.ADMIN_EMAIL || '').trim().toLowerCase();
|
||||
const password = String(process.env.ADMIN_PASSWORD || '');
|
||||
const name = String(process.env.ADMIN_NAME || '').trim() || 'Администратор';
|
||||
|
||||
function fail(msg) { console.error('✗ ' + msg); process.exit(1); }
|
||||
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) fail('Некорректный email (ADMIN_EMAIL).');
|
||||
if (password.length < 8) fail('Пароль минимум 8 символов (ADMIN_PASSWORD).');
|
||||
|
||||
const DB = process.env.DB_PATH || path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
const db = new DatabaseSync(DB);
|
||||
try { db.exec('PRAGMA busy_timeout=4000'); } catch (_) {} // подождать, если сервер пишет
|
||||
|
||||
(async () => {
|
||||
const hash = await bcrypt.hash(password, 12);
|
||||
const existing = db.prepare('SELECT id, role FROM users WHERE email = ?').get(email);
|
||||
if (existing) {
|
||||
db.prepare(`UPDATE users SET password_hash = ?, name = ?, role = 'admin',
|
||||
token_version = COALESCE(token_version,0) + 1 WHERE id = ?`)
|
||||
.run(hash, name, existing.id);
|
||||
console.log(`✓ Пользователь ${email} обновлён: роль admin, новый пароль (id=${existing.id}).`);
|
||||
} else {
|
||||
const r = db.prepare(`INSERT INTO users (email, password_hash, name, role) VALUES (?,?,?, 'admin')`)
|
||||
.run(email, hash, name);
|
||||
console.log(`✓ Создан администратор ${email} (id=${r.lastInsertRowid}).`);
|
||||
}
|
||||
db.close();
|
||||
})().catch(e => fail(e.message));
|
||||
@@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
/* db-maintain.js — обслуживание SQLite-БД: проверка целостности + компактизация.
|
||||
* Шаги: PRAGMA integrity_check -> PRAGMA wal_checkpoint(TRUNCATE) -> VACUUM.
|
||||
* Путь к БД: env DB_PATH, либо argv[2], либо стандартный.
|
||||
* VACUUM требует, чтобы БД никто не писал — запускать на ОСТАНОВЛЕННОМ сервере.
|
||||
* Используется панелью управления (control-panel.ps1, пункт «Обслуживание БД»).
|
||||
*/
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const DB = process.env.DB_PATH || process.argv[2] || path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
|
||||
function sizeMB(p) {
|
||||
try { return (fs.statSync(p).size / 1048576).toFixed(1) + ' МБ'; } catch (_) { return '?'; }
|
||||
}
|
||||
|
||||
if (!fs.existsSync(DB)) { console.error('БД не найдена: ' + DB); process.exit(1); }
|
||||
|
||||
const before = sizeMB(DB);
|
||||
const db = new DatabaseSync(DB);
|
||||
try { db.exec('PRAGMA busy_timeout=8000'); } catch (_) {}
|
||||
|
||||
// 1) Проверка целостности
|
||||
let integ = '?';
|
||||
try {
|
||||
const rows = db.prepare('PRAGMA integrity_check').all();
|
||||
integ = rows.map(r => (r.integrity_check !== undefined ? r.integrity_check : Object.values(r)[0])).join('; ');
|
||||
} catch (e) { integ = 'ошибка: ' + e.message; }
|
||||
const integOk = (integ === 'ok');
|
||||
console.log('Целостность: ' + (integOk ? 'ok' : integ));
|
||||
|
||||
// 2) Сброс WAL в основной файл
|
||||
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); console.log('WAL checkpoint: ok'); }
|
||||
catch (e) { console.log('WAL checkpoint: ' + e.message); }
|
||||
|
||||
// 3) Компактизация (только если целостность в порядке — VACUUM на битой БД опасен)
|
||||
if (integOk) {
|
||||
try { db.exec('VACUUM'); console.log('VACUUM: ok'); }
|
||||
catch (e) { console.log('VACUUM: ' + e.message); }
|
||||
} else {
|
||||
console.log('VACUUM пропущен: сначала восстановите целостность (откат из бэкапа).');
|
||||
}
|
||||
|
||||
db.close();
|
||||
console.log('Размер БД: ' + before + ' -> ' + sizeMB(DB));
|
||||
@@ -0,0 +1,15 @@
|
||||
'use strict';
|
||||
/* db-status.js — краткая сводка БД для панели управления: "<users>|<lastMigration>".
|
||||
Путь к БД: env DB_PATH, либо argv[2], либо стандартный. Никогда не бросает. */
|
||||
const path = require('path');
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
|
||||
const DB = process.env.DB_PATH || process.argv[2] || path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
let users = '?', migr = '-';
|
||||
try {
|
||||
const db = new DatabaseSync(DB);
|
||||
try { users = db.prepare('SELECT COUNT(*) AS n FROM users').get().n; } catch (_) {}
|
||||
try { const r = db.prepare('SELECT filename FROM _migrations ORDER BY filename DESC LIMIT 1').get(); if (r) migr = r.filename; } catch (_) {}
|
||||
db.close();
|
||||
} catch (_) {}
|
||||
process.stdout.write(String(users) + '|' + String(migr));
|
||||
@@ -0,0 +1,146 @@
|
||||
'use strict';
|
||||
/* ─────────────────────────────────────────────────────────────────────────
|
||||
Ремонт банка вопросов через ИИ (шлюз Kilo из настроек ассистента).
|
||||
Режимы:
|
||||
--broken починить single/multiple без отмеченного верного варианта
|
||||
(ИИ выбирает верный среди СУЩЕСТВУЮЩИХ вариантов)
|
||||
--topics привязать математические вопросы без темы к СУЩЕСТВУЮЩИМ темам
|
||||
Поток: по умолчанию DRY-RUN — пишет предложения в JSON + сводку, БД не трогает.
|
||||
С флагом --apply — применяет ранее сгенерированный JSON к БД.
|
||||
--limit N ограничить число вопросов (для теста)
|
||||
Пример:
|
||||
node fix-question-bank.js --topics # сгенерировать предложения
|
||||
node fix-question-bank.js --topics --apply # применить после вычитки
|
||||
───────────────────────────────────────────────────────────────────────── */
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const DB_PATH = path.resolve(__dirname, '../data/learnspace.db');
|
||||
const argv = process.argv.slice(2);
|
||||
const MODE = argv.includes('--broken') ? 'broken' : argv.includes('--topics') ? 'topics' : null;
|
||||
const APPLY = argv.includes('--apply');
|
||||
const LIMIT = (() => { const i = argv.indexOf('--limit'); return i >= 0 ? Number(argv[i + 1]) || 0 : 0; })();
|
||||
if (!MODE) { console.error('Укажите режим: --broken или --topics (опц. --apply, --limit N)'); process.exit(1); }
|
||||
const OUT = path.join(__dirname, `_qbank_proposals_${MODE}.json`);
|
||||
|
||||
const db = new DatabaseSync(DB_PATH);
|
||||
try { db.exec('PRAGMA busy_timeout=8000'); } catch (e) {}
|
||||
// node:sqlite (DatabaseSync) НЕ имеет .transaction() — оборачиваем вручную.
|
||||
function runTx(fn) { db.exec('BEGIN'); try { fn(); db.exec('COMMIT'); } catch (e) { try { db.exec('ROLLBACK'); } catch (_) {} throw e; } }
|
||||
|
||||
/* ── провайдер Kilo ── */
|
||||
function aset(k) { const r = db.prepare('SELECT value FROM app_settings WHERE key=?').get(k); return r && r.value != null ? r.value : null; }
|
||||
function pickProvider() {
|
||||
let arr = []; try { arr = JSON.parse(aset('assistant_providers') || '[]'); } catch (e) {}
|
||||
const active = arr.find(p => p.id === aset('assistant_active'));
|
||||
return (active && active.key && active) || arr.find(p => p.key) || null;
|
||||
}
|
||||
const PROV = pickProvider();
|
||||
if (!APPLY && !PROV) { console.error('Нет провайдера ИИ с ключом (настрой в /admin#assistant).'); process.exit(1); }
|
||||
|
||||
async function llm(messages, maxTokens) {
|
||||
const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 40000);
|
||||
try {
|
||||
const r = await fetch(PROV.url, {
|
||||
method: 'POST', signal: ctrl.signal,
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, PROV.key ? { Authorization: 'Bearer ' + PROV.key } : {}),
|
||||
body: JSON.stringify({ model: PROV.model, temperature: 0.1, max_tokens: maxTokens || 1500, messages }),
|
||||
});
|
||||
if (!r.ok) return { err: 'HTTP ' + r.status };
|
||||
const j = await r.json();
|
||||
return { text: (j.choices && j.choices[0] && j.choices[0].message && (j.choices[0].message.content || j.choices[0].message.reasoning)) || '' };
|
||||
} catch (e) { return { err: e.name === 'AbortError' ? 'timeout' : 'network' }; }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
function parseJson(raw) {
|
||||
let s = String(raw || '').replace(/```(?:json)?/gi, '').trim();
|
||||
const a = s.search(/[[{]/); if (a > 0) s = s.slice(a);
|
||||
const lastArr = s.lastIndexOf(']'), lastObj = s.lastIndexOf('}');
|
||||
const end = Math.max(lastArr, lastObj); if (end >= 0) s = s.slice(0, end + 1);
|
||||
try { return JSON.parse(s); } catch (e) { return null; }
|
||||
}
|
||||
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
/* ═══ APPLY: применить сохранённые предложения ═══ */
|
||||
function applyProposals() {
|
||||
if (!fs.existsSync(OUT)) { console.error('Нет файла предложений ' + OUT + ' — сначала запусти без --apply.'); process.exit(1); }
|
||||
const props = JSON.parse(fs.readFileSync(OUT, 'utf8'));
|
||||
let n = 0;
|
||||
if (MODE === 'broken') {
|
||||
const upd = db.prepare('UPDATE options SET is_correct = 1 WHERE id = ? AND question_id = ?');
|
||||
runTx(() => { for (const p of props) { if (p.optionId) { upd.run(p.optionId, p.id); n++; } } });
|
||||
console.log(`Применено: отмечено верных вариантов — ${n}`);
|
||||
} else {
|
||||
const upd = db.prepare('UPDATE questions SET topic_id = ? WHERE id = ? AND topic_id IS NULL');
|
||||
runTx(() => { for (const p of props) { if (p.topicId) { upd.run(p.topicId, p.id); n++; } } });
|
||||
console.log(`Применено: привязано тем — ${n}`);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
if (APPLY) applyProposals();
|
||||
|
||||
/* ═══ DRY-RUN: сгенерировать предложения ═══ */
|
||||
(async () => {
|
||||
if (MODE === 'broken') {
|
||||
let qs = db.prepare(`
|
||||
SELECT q.id, q.text FROM questions q
|
||||
WHERE q.type IN ('single','multiple')
|
||||
AND NOT EXISTS (SELECT 1 FROM options o WHERE o.question_id=q.id AND o.is_correct=1)
|
||||
AND EXISTS (SELECT 1 FROM options o WHERE o.question_id=q.id)`).all();
|
||||
if (LIMIT) qs = qs.slice(0, LIMIT);
|
||||
console.log(`Битых MCQ к разбору: ${qs.length}`);
|
||||
const props = [];
|
||||
for (const q of qs) {
|
||||
const opts = db.prepare('SELECT id, text FROM options WHERE question_id=? ORDER BY order_index').all(q.id);
|
||||
const list = opts.map((o, i) => `${i + 1}. ${o.text}`).join('\n');
|
||||
const sys = 'Ты эксперт-предметник. Определи ЕДИНСТВЕННЫЙ правильный вариант ответа. ' +
|
||||
'Верни СТРОГО JSON {"correct": N} где N — номер варианта (1..K), либо {"correct": 0} если определить нельзя (например, нужен рисунок/график). Только JSON.';
|
||||
const user = `Вопрос: ${q.text}\n\nВарианты:\n${list}`;
|
||||
const r = await llm([{ role: 'system', content: sys }, { role: 'user', content: user }], 300);
|
||||
const j = r.text ? parseJson(r.text) : null;
|
||||
const n = j && Number(j.correct);
|
||||
const opt = (n >= 1 && n <= opts.length) ? opts[n - 1] : null;
|
||||
props.push({ id: q.id, optionId: opt ? opt.id : null, optionText: opt ? opt.text : null, q: q.text.slice(0, 70), err: r.err || null });
|
||||
console.log(` #${q.id}: ${opt ? 'верный → «' + String(opt.text).slice(0, 40) + '»' : (r.err || 'не определено (ручная проверка)')}`);
|
||||
await sleep(400);
|
||||
}
|
||||
fs.writeFileSync(OUT, JSON.stringify(props, null, 2));
|
||||
console.log(`\nПредложения: ${OUT}\nВычитай, затем: node fix-question-bank.js --broken --apply`);
|
||||
return;
|
||||
}
|
||||
|
||||
// topics: математика, вопросы без темы → существующие темы
|
||||
const subj = db.prepare("SELECT id FROM subjects WHERE name LIKE '%атематик%' OR slug='math'").get();
|
||||
if (!subj) { console.error('Предмет «Математика» не найден'); process.exit(1); }
|
||||
const topics = db.prepare('SELECT id, name FROM topics WHERE subject_id=? ORDER BY id').all(subj.id);
|
||||
if (!topics.length) { console.error('У математики нет тем'); process.exit(1); }
|
||||
let qs = db.prepare('SELECT id, text FROM questions WHERE subject_id=? AND topic_id IS NULL').all(subj.id);
|
||||
if (LIMIT) qs = qs.slice(0, LIMIT);
|
||||
console.log(`Тем: ${topics.length} | вопросов без темы: ${qs.length}`);
|
||||
const topicList = topics.map((t, i) => `${i + 1}. ${t.name}`).join('\n');
|
||||
const props = []; const BATCH = 12;
|
||||
for (let i = 0; i < qs.length; i += BATCH) {
|
||||
const chunk = qs.slice(i, i + BATCH);
|
||||
const sys = 'Ты классифицируешь вопросы по математике по СУЩЕСТВУЮЩИМ темам из списка. ' +
|
||||
'Для каждого вопроса верни номер наиболее подходящей темы или 0, если ни одна явно не подходит. ' +
|
||||
'Верни СТРОГО JSON-массив [{"id":<id вопроса>,"t":<номер темы 1..K или 0>}]. Только JSON.';
|
||||
const user = `Темы:\n${topicList}\n\nВопросы:\n` + chunk.map(q => `[id ${q.id}] ${String(q.text).replace(/\s+/g, ' ').slice(0, 240)}`).join('\n');
|
||||
const r = await llm([{ role: 'system', content: sys }, { role: 'user', content: user }], 900);
|
||||
const arr = r.text ? parseJson(r.text) : null;
|
||||
const map = {}; if (Array.isArray(arr)) arr.forEach(x => { if (x && x.id != null) map[x.id] = Number(x.t); });
|
||||
for (const q of chunk) {
|
||||
const n = map[q.id]; const t = (n >= 1 && n <= topics.length) ? topics[n - 1] : null;
|
||||
props.push({ id: q.id, topicId: t ? t.id : null, topicName: t ? t.name : null, q: String(q.text).replace(/\s+/g, ' ').slice(0, 70) });
|
||||
}
|
||||
const done = Math.min(i + BATCH, qs.length);
|
||||
console.log(` ${done}/${qs.length}${r.err ? ' (ошибка батча: ' + r.err + ')' : ''}`);
|
||||
await sleep(500);
|
||||
}
|
||||
const assigned = props.filter(p => p.topicId).length;
|
||||
fs.writeFileSync(OUT, JSON.stringify(props, null, 2));
|
||||
const byTopic = {}; props.forEach(p => { if (p.topicName) byTopic[p.topicName] = (byTopic[p.topicName] || 0) + 1; });
|
||||
console.log(`\nРазмечено: ${assigned}/${props.length} (остальные — без уверенной темы, останутся как есть)`);
|
||||
Object.entries(byTopic).sort((a, b) => b[1] - a[1]).slice(0, 12).forEach(([t, n]) => console.log(` ${t}: ${n}`));
|
||||
console.log(`\nПредложения: ${OUT}\nВычитай, затем: node fix-question-bank.js --topics --apply`);
|
||||
})();
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Фикс: блок formula вставляет tex в HTML БЕЗ экранирования ($$...$$), поэтому
|
||||
* литеральные '<' / '>' в формуле браузер принимает за HTML-тег → KaTeX не рендерит.
|
||||
* Заменяем литеральные '<' → '\lt', '>' → '\gt' в tex всех formula-блоков курса 13
|
||||
* (KaTeX их рендерит как отношения). Идемпотентно. dry по умолчанию, запись --apply.
|
||||
* node backend/scripts/fix_ctmath_formula_lt.js [--apply]
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const rows = db.prepare(`SELECT lb.id, lb.lesson_id, lb.data FROM lesson_blocks lb
|
||||
JOIN lessons l ON l.id=lb.lesson_id WHERE l.course_id=13 AND lb.type='formula'`).all();
|
||||
|
||||
const upd = db.prepare('UPDATE lesson_blocks SET data=? WHERE id=?');
|
||||
let changed = 0;
|
||||
for (const r of rows) {
|
||||
let d; try { d = JSON.parse(r.data); } catch { continue; }
|
||||
if (!d.tex || !/[<>]/.test(d.tex)) continue;
|
||||
const before = d.tex;
|
||||
d.tex = d.tex.replace(/</g, '\\lt ').replace(/>/g, '\\gt ');
|
||||
changed++;
|
||||
console.log(`block ${r.id} (lesson ${r.lesson_id}):`);
|
||||
console.log(' было:', before);
|
||||
console.log(' стало:', d.tex);
|
||||
if (APPLY) upd.run(JSON.stringify(d), r.id);
|
||||
}
|
||||
console.log(`\n${APPLY ? 'Обновлено' : '(dry) к обновлению'}: ${changed} формул.`);
|
||||
if (!APPLY) console.log('Запись: --apply');
|
||||
@@ -0,0 +1,85 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Фикс mc-задач ctmath, где варианты ответа вшиты в текст («1) 44; 2) 22; …»),
|
||||
* а opts_json содержит лишь цифры-указатели. Вытаскивает список из текста в
|
||||
* нормальный opts_json (метка=цифра, текст=значение), пересчитывает answer,
|
||||
* очищает текст. Только для чисто распознанных случаев (иначе пропуск).
|
||||
* node backend/scripts/fix_ctmath_inline_opts.js # dry: статистика+выборка
|
||||
* node backend/scripts/fix_ctmath_inline_opts.js --apply # запись (UPDATE)
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
// Разбор инлайн-списка "1) v1; 2) v2; … N) vN."
|
||||
// Последовательный: режем значение только по " ; (n+1)) " следующего номера,
|
||||
// поэтому ';' внутри значений (интервалы вида (-6;9)) сохраняются.
|
||||
function parseInline(text) {
|
||||
const m1 = text.match(/(^|[\s:>(])1\)\s/);
|
||||
if (!m1) return null;
|
||||
const start = m1.index + m1[1].length; // позиция "1)"
|
||||
const stem = text.slice(0, start).replace(/[\s:]+$/, '').trim();
|
||||
if (!stem) return null;
|
||||
let rest = text.slice(start);
|
||||
const h1 = /^1\)\s*/;
|
||||
if (!h1.test(rest)) return null;
|
||||
rest = rest.replace(h1, ''); // "1)" снимаем один раз
|
||||
const pairs = [];
|
||||
let n = 1;
|
||||
while (true) {
|
||||
const nextRe = new RegExp('\\s*;?\\s*' + (n + 1) + '\\)\\s');
|
||||
const nm = rest.match(nextRe);
|
||||
let val;
|
||||
if (nm) { val = rest.slice(0, nm.index); rest = rest.slice(nm.index + nm[0].length); }
|
||||
else { val = rest; rest = ''; } // последний пункт
|
||||
val = val.replace(/[;.\s]+$/, '').trim();
|
||||
if (!val) return null;
|
||||
pairs.push([String(n), val]);
|
||||
if (!nm) break;
|
||||
n++;
|
||||
}
|
||||
if (pairs.length < 2) return null;
|
||||
return { stem, pairs };
|
||||
}
|
||||
|
||||
const rows = db.prepare("SELECT id, text_html, opts_json, answer FROM exam_tasks WHERE exam_key='ctmath' AND task_type='mc'").all();
|
||||
const stat = { total: rows.length, candidate: 0, fixed: 0, skip_notdigit: 0, skip_parse: 0, skip_count: 0, skip_answer: 0 };
|
||||
const updates = [];
|
||||
|
||||
for (const r of rows) {
|
||||
let opts; try { opts = JSON.parse(r.opts_json); } catch { continue; }
|
||||
const texts = opts.map(p => String(p[1]).replace(/\$/g, '').trim());
|
||||
const isDigitPtr = texts.length >= 2 && texts.every(x => /^[1-9][0-9]?$/.test(x));
|
||||
if (!isDigitPtr) { stat.skip_notdigit++; continue; }
|
||||
stat.candidate++;
|
||||
|
||||
const parsed = parseInline(r.text_html);
|
||||
if (!parsed) { stat.skip_parse++; continue; }
|
||||
if (parsed.pairs.length !== opts.length) { stat.skip_count++; continue; }
|
||||
|
||||
// correctDigit = указатель, на который ссылается текущий answer
|
||||
const ai = opts.findIndex(p => String(p[0]).toLowerCase() === String(r.answer).toLowerCase());
|
||||
const correctDigit = ai >= 0 ? String(opts[ai][1]).replace(/\$/g, '').trim() : null;
|
||||
if (!correctDigit || !/^[1-9][0-9]?$/.test(correctDigit) || Number(correctDigit) > parsed.pairs.length) { stat.skip_answer++; continue; }
|
||||
|
||||
const newOpts = JSON.stringify(parsed.pairs); // [["1","44"],...]
|
||||
updates.push({ id: r.id, text: parsed.stem, opts: newOpts, answer: correctDigit, _old: r.text_html, _newpairs: parsed.pairs });
|
||||
stat.fixed++;
|
||||
}
|
||||
|
||||
console.log(APPLY ? '[APPLY]' : '[DRY-RUN]', 'mc всего', stat.total);
|
||||
console.log('Статистика:', JSON.stringify(stat));
|
||||
|
||||
console.log('\n— Выборка (3) —');
|
||||
for (const u of updates.slice(0, 3)) {
|
||||
console.log(`\n id=${u.id}`);
|
||||
console.log(' было text:', u._old.replace(/\s+/g, ' ').slice(0, 120));
|
||||
console.log(' стало text:', u.text.replace(/\s+/g, ' ').slice(0, 90));
|
||||
console.log(' стало opts:', u.opts.slice(0, 160), '| answer:', u.answer);
|
||||
}
|
||||
|
||||
if (!APPLY) { console.log('\nDRY-RUN: запись НЕ выполнялась. Запись: --apply'); process.exit(0); }
|
||||
|
||||
const upd = db.prepare('UPDATE exam_tasks SET text_html=@text, opts_json=@opts, answer=@answer WHERE id=@id');
|
||||
let n = 0;
|
||||
for (const u of updates) { upd.run({ id: u.id, text: u.text, opts: u.opts, answer: u.answer }); n++; }
|
||||
console.log(`\nОбновлено ${n} задач.`);
|
||||
@@ -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,171 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
fix_ctmath_render.js — починка двух дефектов рендеринга в exam_tasks (ctmath).
|
||||
|
||||
ПРИЧИНА (root cause):
|
||||
В seed-скриптах вариантов 101–121 опции писались как mc('$\sqrt{17}$', ...) —
|
||||
в ОБЫЧНЫХ кавычках, а не в String.raw `…`. JS-парсер съедал управляющие
|
||||
эскейпы: \s→s, \d→d (теряется «\»), а \f→0x0C, \t→0x09, \b→0x08, \v,\n,\r —
|
||||
превращались в УПРАВЛЯЮЩИЕ символы. Итог в БД: «$sqrt{17}$», «$dfrac{pi}{3}$»,
|
||||
KaTeX рендерит их как «sqrt17», «dfracpi3». (text/solution писались через R`…`
|
||||
и НЕ пострадали — там «\» на месте.)
|
||||
|
||||
ВТОРОЙ ДЕФЕКТ: литеральные < и > ВНУТРИ $…$ (напр. «$-1{,}6<x<-1$»). При вставке
|
||||
в innerHTML браузер парсит «<x…» как HTML-тег ДО запуска KaTeX → ломает карточку.
|
||||
Лечится заменой < → \lt, > → \gt (только внутри $…$).
|
||||
|
||||
ЧТО ДЕЛАЕТ СКРИПТ (идемпотентно, повторный запуск безопасен):
|
||||
• opts_json: (1) нормализует управляющие символы обратно в \f \t \b \v \n \r;
|
||||
(2) восстанавливает «\» перед известными KaTeX-командами; (3) < > → \lt \gt.
|
||||
• text_html, solution_html: только (3) < > → \lt \gt внутри $…$ (HTML-теги вне
|
||||
математики не трогаются).
|
||||
Восстановление «\» применяется ТОЛЬКО к opts_json (text/sol не повреждены).
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/fix_ctmath_render.js # DRY-RUN (показывает правки)
|
||||
node backend/scripts/fix_ctmath_render.js --apply # запись в БД
|
||||
⚠️ Запись запускает ПОЛЬЗОВАТЕЛЬ. После --apply — перезапуск сервера не нужен
|
||||
(данные в БД; фронт перечитает их при следующем запросе), но hard-refresh браузера.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
|
||||
/* ── Управляющие символы → их LaTeX-эскейп (обратная нормализация) ── */
|
||||
const CTRL_MAP = { '\b': '\\b', '\t': '\\t', '\n': '\\n', '\v': '\\v', '\f': '\\f', '\r': '\\r' };
|
||||
function normalizeCtrl(s) {
|
||||
return s.replace(/[\b\t\n\v\f\r]/g, ch => CTRL_MAP[ch] || ch);
|
||||
}
|
||||
|
||||
/* ── Команды, у которых первая буква НЕ из {f,t,b,v,n,r} (их «\» просто пропал,
|
||||
без управляющего символа). Длинные/составные — РАНЬШЕ коротких префиксов,
|
||||
чтобы не разорвать слово (dfrac до frac, arccos до cos, leq до le, …). ── */
|
||||
const BARE_CMDS = [
|
||||
'arccos', 'arcsin', 'arctg',
|
||||
'overline', 'operatorname', 'varnothing', 'varphi', 'varepsilon',
|
||||
'dfrac', 'cdots', 'cdot', 'sqrt', 'left',
|
||||
'lambda', 'gamma', 'delta', 'sigma', 'omega', 'alpha', 'angle', 'approx',
|
||||
'infty', 'ldots', 'oplus',
|
||||
'cos', 'sin', 'cot', 'ctg', 'cup', 'cap', 'leq', 'geq', 'neq',
|
||||
'sim', 'lim', 'log',
|
||||
'pm', 'mp', 'le', 'ge', 'ln', 'lg', 'pi', 'mu', 'in',
|
||||
'phi', 'psi', 'rho', 'chi', 'tau',
|
||||
];
|
||||
/* (frac, tfrac, times, theta, tan, tg, text, beta, vec, ne, nu, right, nabla —
|
||||
приходят из управляющих символов и чинятся normalizeCtrl, поэтому в этом
|
||||
списке их НЕТ: иначе «dfrac»→«d\frac».) */
|
||||
|
||||
function restoreBackslashes(math) {
|
||||
let s = normalizeCtrl(math);
|
||||
for (const cmd of BARE_CMDS) {
|
||||
// «\bcmd», не уже-экранированное (нет «\» перед), как самостоятельное слово
|
||||
const re = new RegExp('(^|[^\\\\A-Za-z])' + cmd + '(?![A-Za-z])', 'g');
|
||||
s = s.replace(re, (m, pre) => pre + '\\' + cmd);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ── < > → \lt \gt ТОЛЬКО внутри $…$ / $$…$$ ── */
|
||||
function fixAngles(field) {
|
||||
if (!field) return field;
|
||||
return String(field).replace(/\$\$[\s\S]*?\$\$|\$[^$]*\$/g, seg =>
|
||||
seg.replace(/</g, '\\lt ').replace(/>/g, '\\gt '));
|
||||
}
|
||||
|
||||
/* ── Полная починка одной опции (внутри $…$): \ + < > ── */
|
||||
function fixOptionText(t) {
|
||||
if (!t) return t;
|
||||
// обрабатываем содержимое каждого $…$: восстановить «\», затем < >
|
||||
return String(t).replace(/\$\$[\s\S]*?\$\$|\$[^$]*\$/g, seg => {
|
||||
const open = seg.startsWith('$$') ? '$$' : '$';
|
||||
const inner = seg.slice(open.length, seg.length - open.length);
|
||||
let fixed = restoreBackslashes(inner);
|
||||
fixed = fixed.replace(/</g, '\\lt ').replace(/>/g, '\\gt ');
|
||||
return open + fixed + open;
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Открытие БД ── */
|
||||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
const db = new DatabaseSync(DB);
|
||||
|
||||
const rows = db.prepare(
|
||||
`SELECT id, variant, task_idx, task_type, text_html, opts_json, solution_html
|
||||
FROM exam_tasks WHERE exam_key=? ORDER BY variant, task_idx`).all(EXAM);
|
||||
|
||||
let changedRows = 0, changedOpts = 0, changedText = 0, changedSol = 0;
|
||||
const samples = [];
|
||||
|
||||
const upd = db.prepare(
|
||||
`UPDATE exam_tasks SET text_html=?, opts_json=?, solution_html=? WHERE id=?`);
|
||||
|
||||
if (APPLY) db.exec('BEGIN');
|
||||
try {
|
||||
for (const r of rows) {
|
||||
let newOpts = r.opts_json;
|
||||
let newText = r.text_html;
|
||||
let newSol = r.solution_html;
|
||||
let touched = false;
|
||||
|
||||
// opts_json — восстановление «\» + < >
|
||||
if (r.opts_json) {
|
||||
try {
|
||||
const arr = JSON.parse(r.opts_json);
|
||||
const fixed = arr.map(([l, t]) => [l, fixOptionText(t)]);
|
||||
const cand = JSON.stringify(fixed);
|
||||
if (cand !== r.opts_json) { newOpts = cand; changedOpts++; touched = true; }
|
||||
} catch { /* не-JSON — пропускаем */ }
|
||||
}
|
||||
// text_html / solution_html — только < >
|
||||
const ft = fixAngles(r.text_html);
|
||||
if (ft !== r.text_html) { newText = ft; changedText++; touched = true; }
|
||||
const fs = fixAngles(r.solution_html);
|
||||
if (fs !== r.solution_html) { newSol = fs; changedSol++; touched = true; }
|
||||
|
||||
if (touched) {
|
||||
changedRows++;
|
||||
if (samples.length < 12) {
|
||||
samples.push({ v: r.variant, i: r.task_idx,
|
||||
beforeOpts: r.opts_json && r.opts_json.length > 90 ? r.opts_json.slice(0, 90) + '…' : r.opts_json,
|
||||
afterOpts: newOpts && newOpts.length > 90 ? newOpts.slice(0, 90) + '…' : newOpts });
|
||||
}
|
||||
if (APPLY) upd.run(newText, newOpts, newSol, r.id);
|
||||
}
|
||||
}
|
||||
if (APPLY) db.exec('COMMIT');
|
||||
} catch (e) {
|
||||
if (APPLY) db.exec('ROLLBACK');
|
||||
console.error('✗ Ошибка, откат:', e.message);
|
||||
db.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n=== fix_ctmath_render (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log(`Всего задач ctmath: ${rows.length}`);
|
||||
console.log(`Будет изменено строк: ${changedRows} (opts: ${changedOpts}, text: ${changedText}, sol: ${changedSol})`);
|
||||
console.log(`\nПримеры (opts до → после):`);
|
||||
for (const s of samples) {
|
||||
console.log(`\n v${s.v}#${s.i}`);
|
||||
console.log(` ДО: ${s.beforeOpts}`);
|
||||
console.log(` ПОСЛЕ: ${s.afterOpts}`);
|
||||
}
|
||||
|
||||
/* контроль остаточных «голых» команд после починки (для self-check в dry-run) */
|
||||
if (!APPLY) {
|
||||
const after = db.prepare(`SELECT opts_json FROM exam_tasks WHERE exam_key=? AND opts_json IS NOT NULL`).all(EXAM);
|
||||
let leftover = 0;
|
||||
for (const r of after) {
|
||||
let arr; try { arr = JSON.parse(r.opts_json); } catch { continue; }
|
||||
for (const [, t] of arr) {
|
||||
const fixedNow = fixOptionText(t);
|
||||
// ищем подозрительные «\bdfrac/sqrt/frac…» БЕЗ слэша уже ПОСЛЕ починки
|
||||
if (/(^|[^\\A-Za-z])(sqrt|dfrac|frac|tfrac|cdot|times|alpha|beta|theta|pi)(?![A-Za-z])/.test(fixedNow.replace(/\$/g,''))) leftover++;
|
||||
}
|
||||
}
|
||||
console.log(`\nКонтроль: потенциально не починенных опций после прогона: ${leftover}`);
|
||||
console.log(`\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/fix_ctmath_render.js --apply\n`);
|
||||
}
|
||||
db.close();
|
||||
@@ -362,7 +362,7 @@ main{max-width:1180px;margin:0 auto;padding:32px 24px 60px}
|
||||
</div>
|
||||
<div>
|
||||
<h1>Физика — 11 класс</h1>
|
||||
<div class="hdr-sub">Жилко · Маркович · Сокольский (2021) · 8 глав · 45 параграфов</div>
|
||||
<div class="hdr-sub">Полный курс физики 11 класса · 8 глав · 45 параграфов</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -366,14 +366,14 @@ function buildCh(chKey) {
|
||||
|
||||
const bodyHtml = isLR
|
||||
? `<p><b>${name}</b> — лабораторная работа в разработке (Phase 5+).</p>
|
||||
<p>Здесь появятся: <b>Цель · Оборудование · Проверьте себя · Вывод расчётных формул · Ход работы · Таблица измерений · Контрольные вопросы · Суперзадание</b> — по канве учебника Исаченковой 2019.</p>
|
||||
<p>Здесь появятся: <b>Цель · Оборудование · Проверьте себя · Вывод расчётных формул · Ход работы · Таблица измерений · Контрольные вопросы · Суперзадание</b> — по учебной программе.</p>
|
||||
<p style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.92rem">
|
||||
<b>Phase 0:</b> создан скелет. <b>Phase 5:</b> наполнение ЛР пошаговой работой с интерактивной таблицей измерений.
|
||||
</p>`
|
||||
: `<p><b>${name}</b> — этот параграф в разработке (Phase ${C.chNum}+).</p>
|
||||
<p>Здесь появятся: теория, формулы, разобранные примеры и 3–4 интерактива в стиле «физики 10» — векторные диаграммы, графики движения, ползунки и автопроверяемые тренажёры.</p>
|
||||
<p style="margin-top:10px;padding:10px 14px;background:var(--sec-acc-soft);border-radius:9px;font-size:.92rem">
|
||||
<b>Phase 0:</b> создан скелет. <b>Phase 5:</b> наполнение по учебнику «Физика 9» (Исаченкова, Сокольский, Захаревич, 2019).
|
||||
<b>Phase 0:</b> создан скелет. <b>Phase 5:</b> наполнение по учебной программе «Физика 9» (2019).
|
||||
</p>`;
|
||||
|
||||
return `function build_${pid}(){
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
open_ctmath_for_class.js — открыть ЦТ-математику профильному классу.
|
||||
|
||||
Делает (идемпотентно):
|
||||
1. courses.is_published = 1 для курса id=13 «ЦЭ/ЦТ — Математика».
|
||||
2. content_access: открыть классу доступ к
|
||||
• курсу (content_type='course', content_ref='13')
|
||||
• экзамену (content_type='exam', content_ref='ctmath')
|
||||
(scope='class', allow=1; upsert по UNIQUE(content_type,content_ref,scope,target_id)).
|
||||
|
||||
Модель доступа — ALLOWLIST (services/contentAccess.js): по умолчанию закрыто,
|
||||
правило ученика > класса, админ/учитель видят всё. Поэтому без этих правил
|
||||
ученики класса курс/экзамен НЕ видят, даже если курс опубликован.
|
||||
|
||||
Цель — класс #4 «10Б · Математика» (выбор пользователя). Сменить — флагом
|
||||
--class=<id>. Скрипт сверяет имя класса и печатает его перед записью.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/open_ctmath_for_class.js # DRY-RUN
|
||||
node backend/scripts/open_ctmath_for_class.js --apply # запись
|
||||
node backend/scripts/open_ctmath_for_class.js --class=4 --apply
|
||||
|
||||
⚠️ Outward-facing: после --apply и рестарта сервера ученики класса увидят
|
||||
курс и пробники. Массовую запись запускает ПОЛЬЗОВАТЕЛЬ вручную.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const COURSE_ID = 13;
|
||||
const EXAM_KEY = 'ctmath';
|
||||
const classArg = (process.argv.find(a => a.startsWith('--class=')) || '').split('=')[1];
|
||||
const CLASS_ID = Number.isInteger(+classArg) && +classArg > 0 ? +classArg : 4;
|
||||
|
||||
const db = new DatabaseSync(path.join(__dirname, '..', 'data', 'learnspace.db'));
|
||||
const get = (sql, ...a) => db.prepare(sql).get(...a);
|
||||
|
||||
console.log(`\n=== open_ctmath_for_class (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===\n`);
|
||||
|
||||
/* ── Защитные проверки ─────────────────────────────────────────────────────── */
|
||||
const course = get('SELECT id, title, is_published, created_by FROM courses WHERE id=?', COURSE_ID);
|
||||
if (!course) { console.error(`✗ Курс id=${COURSE_ID} не найден. Прерывание.`); db.close(); process.exit(1); }
|
||||
|
||||
const klass = get('SELECT id, name FROM classes WHERE id=?', CLASS_ID);
|
||||
if (!klass) { console.error(`✗ Класс id=${CLASS_ID} не найден. Прерывание.`); db.close(); process.exit(1); }
|
||||
|
||||
const track = get('SELECT exam_key, enabled FROM exam_tracks WHERE exam_key=?', EXAM_KEY);
|
||||
if (!track) { console.error(`✗ Трек '${EXAM_KEY}' не найден в exam_tracks. Прерывание.`); db.close(); process.exit(1); }
|
||||
|
||||
const members = get('SELECT COUNT(*) n FROM class_members WHERE class_id=?', CLASS_ID).n;
|
||||
console.log(`Курс: id=${course.id} «${course.title}» (is_published=${course.is_published})`);
|
||||
console.log(`Класс: id=${klass.id} «${klass.name}» (учеников: ${members})`);
|
||||
console.log(`Экзамен: ${track.exam_key} (enabled=${track.enabled})\n`);
|
||||
|
||||
/* ── План действий ─────────────────────────────────────────────────────────── */
|
||||
const actions = [];
|
||||
|
||||
if (course.is_published !== 1) {
|
||||
actions.push({ desc: `опубликовать курс id=${COURSE_ID} (is_published 0 → 1)`,
|
||||
run: () => db.prepare('UPDATE courses SET is_published=1 WHERE id=?').run(COURSE_ID) });
|
||||
} else {
|
||||
console.log('• курс уже опубликован — пропуск');
|
||||
}
|
||||
|
||||
const accessRow = db.prepare(`SELECT allow FROM content_access
|
||||
WHERE content_type=? AND content_ref=? AND scope='class' AND target_id=?`);
|
||||
const upsertAccess = db.prepare(`
|
||||
INSERT INTO content_access (content_type, content_ref, scope, target_id, allow, created_by)
|
||||
VALUES (?, ?, 'class', ?, 1, ?)
|
||||
ON CONFLICT (content_type, content_ref, scope, target_id)
|
||||
DO UPDATE SET allow=1, created_by=excluded.created_by, created_at=datetime('now')`);
|
||||
|
||||
for (const [type, ref] of [['course', String(COURSE_ID)], ['exam', EXAM_KEY]]) {
|
||||
const cur = accessRow.get(type, ref, CLASS_ID);
|
||||
if (cur && cur.allow === 1) { console.log(`• доступ ${type}:${ref} классу #${CLASS_ID} уже открыт — пропуск`); continue; }
|
||||
actions.push({ desc: `открыть доступ ${type}:${ref} классу #${CLASS_ID} (allow=1)`,
|
||||
run: () => upsertAccess.run(type, ref, CLASS_ID, course.created_by || null) });
|
||||
}
|
||||
|
||||
console.log(`\nК применению (${actions.length}):`);
|
||||
actions.forEach(a => console.log(' - ' + a.desc));
|
||||
|
||||
if (!actions.length) { console.log('\nВсё уже в нужном состоянии — менять нечего.\n'); db.close(); process.exit(0); }
|
||||
|
||||
if (!APPLY) {
|
||||
console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/open_ctmath_for_class.js --apply\n');
|
||||
db.close(); process.exit(0);
|
||||
}
|
||||
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
for (const a of actions) a.run();
|
||||
db.exec('COMMIT');
|
||||
console.log(`\n✓ Применено: ${actions.length}. Курс и пробники ЦТ открыты классу «${klass.name}».`);
|
||||
console.log(' (после рестарта сервера ученики класса увидят их в каталоге / на дашборде)\n');
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка, откат:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,61 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
reset-system.js — CLI «ЧИСТЫЙ ЗАПУСК» (тонкая обёртка над src/services/systemReset.js).
|
||||
|
||||
⚠️ ДЕСТРУКТИВНО. По умолчанию DRY-RUN. Выполнение — только с --apply --confirm=RESET.
|
||||
Перед сбросом сделайте бэкап (control-panel «Бэкап БД» делает автоматически).
|
||||
Та же логика доступна в админ-веб-панели (POST /api/admin/reset-system).
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/reset-system.js # план
|
||||
node backend/scripts/reset-system.js --apply --confirm=RESET # выполнить
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
const reset = require('../src/services/systemReset');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const CONFIRM = process.argv.includes('--confirm=RESET');
|
||||
|
||||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
const db = new DatabaseSync(DB);
|
||||
|
||||
const keptAdmin = reset.pickKeptAdmin(db);
|
||||
if (!keptAdmin) {
|
||||
console.error('✗ В системе нет ни одного админа — сброс отменён (иначе залочитесь). Создайте админа сначала.');
|
||||
db.close(); process.exit(1);
|
||||
}
|
||||
|
||||
const plan = reset.classify(db);
|
||||
console.log(`\n=== reset-system «ЧИСТЫЙ ЗАПУСК» (${APPLY ? (CONFIRM ? 'APPLY' : 'нужен --confirm=RESET') : 'DRY-RUN'}) ===`);
|
||||
console.log(`Сохраняемый админ: id=${keptAdmin.id} ${keptAdmin.email} «${keptAdmin.name}»`);
|
||||
console.log(`Пользователей: ${plan.totalUsers} → останется 1, удалится ${plan.totalUsers - 1}\n`);
|
||||
console.log('REASSIGN (контент → админу):');
|
||||
plan.reassign.forEach(r => console.log(` ${r.table.padEnd(22)} ${r.col.padEnd(12)} строк: ${r.rows}`));
|
||||
console.log('\nWIPE (полная очистка):');
|
||||
plan.wipe.forEach(w => console.log(` ${w.table.padEnd(28)} строк: ${w.rows}`));
|
||||
console.log(` — всего к удалению (без каскада users): ~${plan.wipeRows}`);
|
||||
console.log(`\nKEEP (контент/конфиг): ${plan.keepCount} таблиц.`);
|
||||
if (plan.unknown.length) console.log(`\n⚠️ НЕИЗВЕСТНЫЕ таблицы (НЕ трогаем): ${plan.unknown.join(', ')}`);
|
||||
|
||||
if (!APPLY) {
|
||||
console.log('\nDRY-RUN: ничего не изменено. Выполнить: node backend/scripts/reset-system.js --apply --confirm=RESET\n');
|
||||
db.close(); process.exit(0);
|
||||
}
|
||||
if (!CONFIRM) {
|
||||
console.error('\n✗ Нужен флаг --confirm=RESET (защита от случайного запуска). Отмена.');
|
||||
db.close(); process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = reset.runReset(db, keptAdmin.id);
|
||||
console.log(`\n✓ ЧИСТЫЙ ЗАПУСК выполнен. Удалено пользователей: ${res.deletedUsers}, осталось: ${res.remainingUsers}.`);
|
||||
console.log(`✓ Контент сохранён: учебники ${res.kept.textbooks}, вопросы ${res.kept.questions}, тесты ${res.kept.tests}, курсы ${res.kept.courses}, exam-prep ${res.kept.exam_tasks}.`);
|
||||
if (res.fkDangling) console.log(`⚠️ foreign_key_check: ${res.fkDangling} висячих ссылок — проверьте.`);
|
||||
console.log(`\nВойдите под ${keptAdmin.email}. Перезапустите сервер.\n`);
|
||||
} catch (e) {
|
||||
console.error('\n✗ Ошибка — откат, изменений нет:', e.message);
|
||||
db.close(); process.exit(1);
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,369 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ce2024_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованный экзамен / ЦТ по математике, 2024 (сборник тестов
|
||||
РИКЗ / «Аверсэв»), Вариант 1. АКТУАЛЬНЫЙ формат: Часть А = А1–А10 (закрытые,
|
||||
5 вариантов), Часть В = В1–В20 (открытые). Всего 30 заданий. Перенабрано
|
||||
вручную в KaTeX по PDF:
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦЭ-ЦТ-2024 МАТ.pdf
|
||||
|
||||
⚠️ В PDF РЕШЕНИЙ НЕТ (только задания). Решения и ответы получены вручную и
|
||||
СВЕРЕНЫ с официальной таблицей ответов в конце сборника (стр. 35, столбец
|
||||
«Вариант 1»). Все 30 ответов совпали. variant=111.
|
||||
|
||||
Адаптации заданий-«с-картинкой» (исходный ответ/идея сохранены):
|
||||
• А1 (точки на координатной прямой) → равные промежутки заданы в тексте
|
||||
(та же точка-ответ);
|
||||
• А6 (изображённый числовой промежуток) → промежуток $(-6;9]$ описан
|
||||
словами (ответ 24 сохранён).
|
||||
Остальная геометрия закодирована текстом (как у РТ-вариантов 101–109).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ce2024_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ce2024_v1.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 111;
|
||||
const PROV = 'ЦЭ–2024, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А10 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`На координатной прямой точки $D$, $O$, $C$, $B$, $A$, $E$ идут в указанном порядке через равные промежутки; $O$ — начало отсчёта, а соседние точки отличаются по координате на $0{,}8$. Какой точке соответствует число $1{,}6$?`,
|
||||
opts: mc('$A$', '$B$', '$C$', '$D$', '$E$'),
|
||||
answer: 'б',
|
||||
sol: R`От $O$ вправо: $C=0{,}8$, $B=1{,}6$, $A=2{,}4$. Числу $1{,}6$ соответствует точка $B$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 2,
|
||||
text: R`На рисунке изображена правильная четырёхугольная пирамида $SABCD$, точка $O$ — точка пересечения диагоналей основания $ABCD$. Среди прямых $BC$, $BD$, $SO$, $SB$, $SD$ укажите прямую, по которой пересекаются плоскости $DSO$ и $SCB$.`,
|
||||
opts: mc('$BC$', '$BD$', '$SO$', '$SB$', '$SD$'),
|
||||
answer: 'г',
|
||||
sol: R`Точка $O$ лежит на диагонали $BD$, поэтому плоскость $DSO$ совпадает с плоскостью $SBD$. Плоскости $SBD$ и $SCB$ имеют общие точки $S$ и $B$, поэтому пересекаются по прямой $SB$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 1,
|
||||
text: R`Среди значений аргумента, равных $-\dfrac{\pi}{6}$, $\dfrac{\pi}{4}$, $\dfrac{\pi}{3}$, $-\dfrac{3\pi}{2}$, $-6\pi$, укажите то, при котором значение функции $y=\sin x$ равно нулю.`,
|
||||
opts: mc('$-\dfrac{\pi}{6}$', '$\dfrac{\pi}{4}$', '$\dfrac{\pi}{3}$', '$-\dfrac{3\pi}{2}$', '$-6\pi$'),
|
||||
answer: 'д',
|
||||
sol: R`$\sin x=0$ при $x=\pi n$, $n\in\mathbb{Z}$. Из данных значений этому условию удовлетворяет только $-6\pi$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 2' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
|
||||
text: R`Укажите номер формулы, по которой можно найти делимое $n$ при делении с остатком, если делитель $15$, неполное частное $k$, остаток $7$ ($n$ — натуральное число).`,
|
||||
opts: mc('$n=15(k+7)$', '$n=k+22$', '$n=15k+7$', '$n=7k+15$', '$n=7(k+15)$'),
|
||||
answer: 'в',
|
||||
sol: R`При делении с остатком $n=q\cdot b+r$, где $b$ — делитель, $q$ — неполное частное, $r$ — остаток. Значит, $n=15k+7$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 11' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Укажите номер квадратного уравнения, произведение действительных корней которого равно $5$.`,
|
||||
opts: mc('$x^{2}-6x+5=0$', '$x^{2}-4x+5=0$', '$x^{2}-5x+6=0$', '$x^{2}+5x=0$', '$x^{2}-5=0$'),
|
||||
answer: 'а',
|
||||
sol: R`По теореме Виета произведение корней приведённого уравнения равно свободному члену. Произведение $5$ имеют уравнения 1 и 2, но у уравнения $x^{2}-4x+5=0$ дискриминант отрицателен (действительных корней нет). Значит, подходит $x^{2}-6x+5=0$ ($D=16>0$).`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`На координатной прямой изображён промежуток $(-6;9]$ (точка $-6$ не входит, точка $9$ входит). Укажите номера пар промежутков, объединением которых является этот промежуток.<br>1) $(-6;+\infty)$ и $(-6;9)$;<br>2) $(-6;0)$ и $[0;9]$;<br>3) $(-\infty;-6)$ и $(-\infty;9)$;<br>4) $(-6;9]$ и $(0;4)$;<br>5) $(-\infty;9]$ и $(-6;+\infty)$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '24', ansShow: '2, 4',
|
||||
sol: R`$2)$ $(-6;0)\cup[0;9]=(-6;9]$ — верно. $\ 4)$ $(-6;9]\cup(0;4)=(-6;9]$ (так как $(0;4)\subset(-6;9]$) — верно. Остальные дают другие множества: 1) $(-6;+\infty)$; 3) $(-\infty;9)$; 5) $\mathbb{R}$. Подходят пары 2 и 4.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 5' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Толя купил $3$ альбома и $5$ карандашей. Стоимость одного альбома равна $1$ р. $20$ к., а стоимость одного карандаша равна $25$ к. Какая сумма (в копейках) осталась у Толи после покупки альбомов и карандашей, если всего у него было $6$ р.?`,
|
||||
opts: mc('$115$ к.', '$145$ к.', '$110$ к.', '$125$ к.', '$275$ к.'),
|
||||
answer: 'а',
|
||||
sol: R`Потрачено $3\cdot120+5\cdot25=360+125=485$ (к.). Осталось $600-485=115$ (к.).`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 2' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 2,
|
||||
text: R`Найдите значение выражения $\dfrac{38}{\pi}\cdot\arcsin(-1)-\left|-7\right|$.`,
|
||||
opts: mc('$-16$', '$-12$', '$12$', '$26$', '$-26$'),
|
||||
answer: 'д',
|
||||
sol: R`$\arcsin(-1)=-\dfrac{\pi}{2}$, поэтому $\dfrac{38}{\pi}\cdot\left(-\dfrac{\pi}{2}\right)-7=-19-7=-26$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 7' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Квадрат, длина диагонали которого равна $8$, лежит в плоскости $\alpha$. Сфера касается плоскости $\alpha$ в точке пересечения диагоналей квадрата. Найдите площадь сферы, если расстояние от центра сферы до вершины квадрата равно $4\sqrt2$.`,
|
||||
opts: mc('$8\pi$', '$16\pi$', '$64\pi$', '$32\sqrt2\,\pi$', '$32\pi$'),
|
||||
answer: 'в',
|
||||
sol: R`Точка касания — центр квадрата; радиус сферы $R$ перпендикулярен плоскости. Расстояние от центра квадрата до вершины равно половине диагонали — $4$. Тогда $R^{2}+4^{2}=(4\sqrt2)^{2}=32$, $R^{2}=16$, $R=4$. Площадь сферы $S=4\pi R^{2}=64\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Укажите номера выражений, которые имеют смысл при $a=-6$.<br>1) $\dfrac{1}{\sqrt[5]{a-6}}$;<br>2) $\sqrt{a^{5}}$;<br>3) $\sqrt[5]{a}$;<br>4) $\dfrac{1}{\sqrt[6]{a-6}}$;<br>5) $\sqrt[6]{a}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '13', ansShow: '1, 3',
|
||||
sol: R`При $a=-6$: $\ 1)$ $\sqrt[5]{-12}$ определён (нечётный корень) и не равен нулю — смысл есть. $\ 2)$ $\sqrt{(-6)^{5}}$ — корень чётной степени из отрицательного числа — нет смысла. $\ 3)$ $\sqrt[5]{-6}$ определён — смысл есть. $\ 4)$ $\sqrt[6]{-12}$ — нет смысла. $\ 5)$ $\sqrt[6]{-6}$ — нет смысла. Подходят 1 и 3.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 14' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
|
||||
text: R`Дана прямая треугольная призма $ABCA_1B_1C_1$. Точка $M$ — середина ребра $AB$, $\angle ABC=90^\circ$. Выберите верные утверждения.<br>1) расстояние от точки $C_1$ до прямой $AB$ равно длине отрезка $BC_1$;<br>2) расстояние от точки $C_1$ до прямой $AB$ равно длине отрезка $C_1M$;<br>3) расстояние от точки $A$ до прямой $BC$ равно длине отрезка $AB$;<br>4) расстояние между прямыми $BB_1$ и $CC_1$ равно длине отрезка $BC_1$;<br>5) расстояние между прямыми $A_1B_1$ и $AB$ равно длине отрезка $AA_1$;<br>6) расстояние от точки $B$ до прямой $AC$ равно длине отрезка $BC$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '135', ansShow: '1, 3, 5',
|
||||
sol: R`$1)$ верно: $AB\perp BC$ и $AB\perp BB_1$, поэтому $AB$ перпендикулярна плоскости $BB_1C_1C$, и расстояние от $C_1$ до $AB$ равно $BC_1$. $\ 2)$ неверно. $\ 3)$ верно: $BC\perp AB$, расстояние от $A$ до $BC$ равно $AB$. $\ 4)$ неверно: расстояние между $BB_1$ и $CC_1$ равно $BC$. $\ 5)$ верно: $A_1B_1\parallel AB$, расстояние равно боковому ребру $AA_1$. $\ 6)$ неверно. Подходят 1, 3, 5.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3' },
|
||||
|
||||
{ idx: 12, type: 'long', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Функция задана формулой $f(x)=x^{2}+4x-5$ на множестве действительных чисел. Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Сумма координат точки пересечения графика данной функции с осью ординат равна …<br>Б) Сумма нулей данной функции равна …<br>В) Наименьшее значение данной функции на области определения равно …<br><b>Окончание:</b><br>1) $9$; 2) $-4$; 3) $5$; 4) $-9$; 5) $-5$; 6) $4$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А5Б2В4', ansShow: 'А5Б2В4',
|
||||
sol: R`А) График пересекает ось ординат в точке $(0;f(0))=(0;-5)$; сумма координат $0+(-5)=-5$ — окончание 5. Б) Нули: $x^{2}+4x-5=0$, $x=1$ и $x=-5$; их сумма $-4$ — окончание 2. В) Наименьшее значение $f(-2)=4-8-5=-9$ — окончание 4.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Найдите сумму всех натуральных чисел, которые кратны $9$ и больше $141$, но меньше $170$.`,
|
||||
answer: '459',
|
||||
sol: R`Кратные $9$ в промежутке $(141;170)$: $144$, $153$, $162$. Их сумма $144+153+162=459$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\operatorname{ctg}^{2}\alpha$, если $\sin\alpha=\dfrac15$.`,
|
||||
answer: '24',
|
||||
sol: R`$\cos^{2}\alpha=1-\sin^{2}\alpha=1-\dfrac{1}{25}=\dfrac{24}{25}$. Тогда $\operatorname{ctg}^{2}\alpha=\dfrac{\cos^{2}\alpha}{\sin^{2}\alpha}=\dfrac{24/25}{1/25}=24$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 4' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Радиус окружности, описанной около прямоугольного треугольника $ABC$ ($\angle ABC=90^\circ$), равен $18\sqrt2$. Найдите значение выражения $90\cdot\cos\angle ACB$, если $BC=6\sqrt2$.`,
|
||||
answer: '15',
|
||||
sol: R`Гипотенуза $AC=2R=36\sqrt2$. $\cos\angle ACB=\dfrac{BC}{AC}=\dfrac{6\sqrt2}{36\sqrt2}=\dfrac16$. Тогда $90\cdot\dfrac16=15$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Пятый член геометрической прогрессии равен $48$, а шестой её член равен $96$. Найдите сумму четырёх первых членов этой прогрессии.`,
|
||||
answer: '45',
|
||||
sol: R`Знаменатель $q=\dfrac{96}{48}=2$. Из $b_5=b_1 q^{4}=48$: $b_1=\dfrac{48}{16}=3$. Сумма $S_4=\dfrac{b_1(q^{4}-1)}{q-1}=\dfrac{3(16-1)}{1}=45$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 18' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Проездной билет на автобус на месяц стоит $39$ р., а стоимость билета на одну поездку на автобусе равна $80$ к. Сколько поездок на автобусе совершила Маша за месяц, покупая только билеты на одну поездку, если известно, что $75\%$ от суммы денег, которую она потратила за месяц на оплату поездок, равны стоимости проездного билета на месяц?`,
|
||||
answer: '65',
|
||||
sol: R`Пусть $n$ — число поездок. Потрачено $80n$ копеек; $39$ р. $=3900$ к. По условию $0{,}75\cdot80n=3900$, $60n=3900$, $n=65$.`,
|
||||
ref: 'Математика, 6 класс, гл. 2' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-3\le2-\dfrac{3x-2}{2}<27$.`,
|
||||
answer: '-11',
|
||||
sol: R`Вычтем $2$: $-5\le-\dfrac{3x-2}{2}<25$. Умножим на $-2$ (знаки меняются): $10\ge3x-2>-50$, то есть $-48<3x\le12$, $-16<x\le4$. Целые решения $-15,\ldots,4$; сумма наименьшего и наибольшего $-15+4=-11$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Функция $y=f(x)$ определена на множестве действительных чисел, точки $A\left(3;-\dfrac23\right)$ и $B\left(6;-\dfrac34\right)$ принадлежат графику данной функции. Найдите значение выражения $6f(-3)+8f(-6)$, если известно, что график функции $y=f(x)$ симметричен относительно оси ординат.`,
|
||||
answer: '-10',
|
||||
sol: R`График симметричен относительно оси ординат, поэтому функция чётная: $f(-3)=f(3)=-\dfrac23$, $f(-6)=f(6)=-\dfrac34$. Тогда $6\cdot\left(-\dfrac23\right)+8\cdot\left(-\dfrac34\right)=-4-6=-10$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-circle', diff: 3,
|
||||
text: R`Радиус окружности, вписанной в правильный шестиугольник, равен $7\sqrt3$. Найдите значение выражения $\dfrac{S}{\sqrt3}$, где $S$ — площадь правильного шестиугольника.`,
|
||||
answer: '294',
|
||||
sol: R`Для правильного шестиугольника радиус вписанной окружности $r=\dfrac{\sqrt3}{2}a$, откуда сторона $a=\dfrac{2r}{\sqrt3}=\dfrac{2\cdot7\sqrt3}{\sqrt3}=14$. Площадь $S=\dfrac{3\sqrt3}{2}a^{2}=\dfrac{3\sqrt3}{2}\cdot196=294\sqrt3$. Тогда $\dfrac{S}{\sqrt3}=294$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Найдите произведение корней уравнения $\log_2^{2}x-2\log_2 x=\log_2 24-\log_2 3$. В ответ запишите найденное произведение, увеличенное в $11$ раз.`,
|
||||
answer: '44',
|
||||
sol: R`Правая часть $\log_2\dfrac{24}{3}=\log_2 8=3$. Пусть $u=\log_2 x$: $u^{2}-2u-3=0$, $u=3$ или $u=-1$. Тогда $x=8$ или $x=\dfrac12$. Произведение корней $8\cdot\dfrac12=4$; увеличенное в $11$ раз — $44$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 9' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 3,
|
||||
text: R`Дана правильная несократимая дробь. При делении её знаменателя на числитель неполное частное равно $8$, а остаток равен $3$. Если числитель дроби увеличить на $75\%$, то полученная дробь будет равна $\dfrac15$. Найдите наименьшее общее кратное числителя и знаменателя исходной дроби.`,
|
||||
answer: '140',
|
||||
sol: R`Пусть числитель $c$, знаменатель $d$. Тогда $d=8c+3$. Условие $\dfrac{1{,}75c}{d}=\dfrac15$ даёт $d=5\cdot1{,}75c=8{,}75c$. Из $8{,}75c=8c+3$: $0{,}75c=3$, $c=4$, $d=35$. Дробь $\dfrac{4}{35}$; числа $4$ и $35$ взаимно просты, поэтому их наименьшее общее кратное равно $4\cdot35=140$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Цилиндр пересечён плоскостью, параллельной оси цилиндра, так, что в сечении получился квадрат площадью $100$. Найдите значение выражения $\dfrac{S}{\pi}$, где $S$ — площадь боковой поверхности цилиндра, если расстояние от оси цилиндра до плоскости сечения равно $\sqrt{39}$.`,
|
||||
answer: '160',
|
||||
sol: R`Сторона квадрата $10$, поэтому высота цилиндра $h=10$ и хорда сечения равна $10$. Половина хорды $5$, расстояние до оси $\sqrt{39}$, поэтому радиус $r=\sqrt{39+25}=8$. Площадь боковой поверхности $S=2\pi rh=2\pi\cdot8\cdot10=160\pi$. Тогда $\dfrac{S}{\pi}=160$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите наименьшее целое решение неравенства $8^{\,2x-32}+10\cdot4^{\,3x-49}>56$.`,
|
||||
answer: '17',
|
||||
sol: R`$8^{\,2x-32}=2^{\,6x-96}$, $4^{\,3x-49}=2^{\,6x-98}$. Неравенство: $2^{\,6x-98}(2^{2}+10)>56$, $14\cdot2^{\,6x-98}>56$, $2^{\,6x-98}>4=2^{2}$, $6x-98>2$, $x>\dfrac{100}{6}$. Наименьшее целое решение $17$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $2\sin3x\cos3x-\sin6x\sin10x=0$ на промежутке $(-150^\circ;-55^\circ)$.`,
|
||||
answer: '-567',
|
||||
sol: R`$2\sin3x\cos3x=\sin6x$, поэтому $\sin6x(1-\sin10x)=0$. Из $\sin6x=0$: $x=30^\circ n$ — на промежутке корни $-120^\circ,-90^\circ,-60^\circ$. Из $\sin10x=1$: $x=9^\circ+36^\circ n$ — корни $-135^\circ,-99^\circ,-63^\circ$. Сумма всех различных корней: $-120-90-60-135-99-63=-567$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите произведение наименьшего целого решения на наибольшее целое решение неравенства $\log_3^{2}(x+12)-\log_3(x+12)-6<0$.`,
|
||||
answer: '-154',
|
||||
sol: R`Пусть $u=\log_3(x+12)$: $u^{2}-u-6<0$, $(u-3)(u+2)<0$, $-2<u<3$. Тогда $3^{-2}<x+12<3^{3}$, $\dfrac19<x+12<27$, $-11\dfrac89<x<15$. Целые решения от $-11$ до $14$; произведение наименьшего и наибольшего $-11\cdot14=-154$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`Плоскость, параллельная основанию треугольной пирамиды, делит её высоту в отношении $5:3$, если считать от вершины пирамиды. Найдите площадь сечения пирамиды данной плоскостью, если она меньше площади основания пирамиды на $39$.`,
|
||||
answer: '25',
|
||||
sol: R`Сечение подобно основанию с коэффициентом $\dfrac{5}{5+3}=\dfrac58$, поэтому отношение площадей $\left(\dfrac58\right)^{2}=\dfrac{25}{64}$. Пусть площадь основания $S_0$: $S_0-\dfrac{25}{64}S_0=39$, $\dfrac{39}{64}S_0=39$, $S_0=64$. Площадь сечения $\dfrac{25}{64}\cdot64=25$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 4' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму корней (корень, если он единственный) уравнения $\sqrt[8]{2x^{2}-20x+32}-\sqrt[8]{76-23x}=0$. В ответ запишите полученный результат, увеличенный в $6$ раз.`,
|
||||
answer: '-33',
|
||||
sol: R`Уравнение равносильно $2x^{2}-20x+32=76-23x$ при условии $76-23x\ge0$. Получаем $2x^{2}+3x-44=0$, $x=4$ или $x=-\dfrac{11}{2}$. Условие $x\le\dfrac{76}{23}\approx3{,}3$ выполняется только для $x=-\dfrac{11}{2}$. Сумма корней $-\dfrac{11}{2}$; увеличенная в $6$ раз — $-33$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 4,
|
||||
text: R`Дана функция $f(x)=-\dfrac{x^{4}}{4}+2x^{3}+10x^{2}+\lg4$. Найдите значение выражения $a\cdot n$, где $a$ — наибольшее целое отрицательное число из промежутков возрастания данной функции, $n$ — количество всех натуральных чисел из промежутков возрастания данной функции.`,
|
||||
answer: '-24',
|
||||
sol: R`$f'(x)=-x^{3}+6x^{2}+20x=-x\left(x^{2}-6x-20\right)$. Корни $x=0$ и $x=3\pm\sqrt{29}$ ($3-\sqrt{29}\approx-2{,}39$, $3+\sqrt{29}\approx8{,}39$). Функция возрастает на $\left(-\infty;3-\sqrt{29}\right]$ и $\left[0;3+\sqrt{29}\right]$. Наибольшее целое отрицательное из них $a=-3$, натуральные числа из них — $1,\ldots,8$, то есть $n=8$. Тогда $a\cdot n=-24$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — прямой параллелепипед, объём которого равен $\dfrac{5\sqrt7}{2}$. Длины сторон $AB$ и $BC$ основания $ABCD$ равны $\sqrt7$ и $\sqrt2$ соответственно, косинус угла $ABC$ равен $-\dfrac{\sqrt{14}}{8}$. На рёбрах $AA_1$ и $A_1B_1$ взяты точки $M$ и $N$ соответственно, такие, что $AM:MA_1=4:1$, $A_1N:NB_1=1:4$. Найдите значение выражения $8\sqrt{66}\cdot\cos\varphi$, где $\varphi$ — угол между прямыми $MN$ и $BC_1$.`,
|
||||
answer: '46',
|
||||
sol: R`$\sin\angle ABC=\sqrt{1-\dfrac{14}{64}}=\dfrac{5\sqrt2}{8}$, площадь основания $AB\cdot BC\cdot\sin\angle ABC=\sqrt7\cdot\sqrt2\cdot\dfrac{5\sqrt2}{8}=\dfrac{5\sqrt7}{4}$. Из объёма $\dfrac{5\sqrt7}{2}$ высота $AA_1=2$. Введём координаты: $B(0;0;0)$, $C(\sqrt2;0;0)$, $A\left(-\dfrac{7\sqrt2}{8};\dfrac{5\sqrt{14}}{8};0\right)$, вертикаль — ось $z$. Тогда $M=A+(0;0;\tfrac85)$, $N=\left(-\dfrac{7\sqrt2}{10};\dfrac{\sqrt{14}}{2};2\right)$, $\vec{MN}=\left(\dfrac{7\sqrt2}{40};-\dfrac{\sqrt{14}}{8};\dfrac25\right)$, $\vec{BC_1}=(\sqrt2;0;2)$. Тогда $\vec{MN}\cdot\vec{BC_1}=\dfrac{23}{20}$, $|\vec{BC_1}|=\sqrt6$, $|\vec{MN}|=\dfrac{\sqrt{11}}{5}$, поэтому $\cos\varphi=\dfrac{23}{4\sqrt{66}}$ и $8\sqrt{66}\cos\varphi=46$.`,
|
||||
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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ce2024_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_ce2024_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 → «Варианты» → «ЦЭ-2024».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Каркас курса «ЦЭ/ЦТ — Математика» на существующем банке questions.
|
||||
* План: plans/ct-math/ (BUILD_ON_QUESTIONS.md).
|
||||
* ИДЕМПОТЕНТЕН и АДДИТИВЕН: добавляет недостающие темы (topics),
|
||||
* создаёт DRAFT-курс (is_published=0) + 9 секций. Существующие данные не трогает.
|
||||
* Запуск: node backend/scripts/seed_ctmath_course.js (применить)
|
||||
* node backend/scripts/seed_ctmath_course.js --dry (только показать план)
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const DRY = process.argv.includes('--dry');
|
||||
const MATH_ID = 3;
|
||||
|
||||
// 1) Недостающие темы под модульную карту (см. BUILD_ON_QUESTIONS §3)
|
||||
const NEW_TOPICS = [
|
||||
'Преобразование выражений',
|
||||
'Модуль',
|
||||
'Иррациональные уравнения',
|
||||
'Показательные уравнения',
|
||||
'Производная',
|
||||
'Параметры',
|
||||
];
|
||||
|
||||
// 2) Секции курса = 9 блоков (PLAN §3)
|
||||
const SECTIONS = [
|
||||
'Числа и вычисления',
|
||||
'Алгебраические преобразования',
|
||||
'Уравнения и неравенства',
|
||||
'Функции и производная',
|
||||
'Тригонометрия',
|
||||
'Прогрессии и текстовые задачи',
|
||||
'Планиметрия',
|
||||
'Стереометрия',
|
||||
'Продвинутое и комбинированное',
|
||||
];
|
||||
|
||||
const COURSE_TITLE = 'ЦЭ/ЦТ — Математика';
|
||||
const COURSE_DESC = 'Подготовка к ЦЭ/ЦТ по математике: 30 заданий (часть А — А1–А10, часть В — В1–В20). Теория по темам, тренажёр на банке заданий прошлых лет, карточки формул, пробные варианты.';
|
||||
|
||||
function topicExists(name) {
|
||||
return db.prepare('SELECT id FROM topics WHERE subject_id=? AND LOWER(name)=LOWER(?)').get(MATH_ID, name);
|
||||
}
|
||||
function adminId() {
|
||||
const u = db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get();
|
||||
return u && u.id;
|
||||
}
|
||||
|
||||
let addedTopics = 0, skippedTopics = 0;
|
||||
console.log(DRY ? '[DRY-RUN] план изменений:' : '[APPLY] вношу изменения:');
|
||||
|
||||
console.log('\n— Темы (topics) —');
|
||||
for (const name of NEW_TOPICS) {
|
||||
const ex = topicExists(name);
|
||||
if (ex) { console.log(` есть: ${name} (id ${ex.id})`); skippedTopics++; continue; }
|
||||
if (DRY) { console.log(` + добавить: ${name}`); addedTopics++; continue; }
|
||||
const id = db.prepare('INSERT INTO topics (subject_id,name) VALUES (?,?)').run(MATH_ID, name).lastInsertRowid;
|
||||
console.log(` + добавлено: ${name} (id ${id})`);
|
||||
addedTopics++;
|
||||
}
|
||||
|
||||
console.log('\n— Курс (courses) —');
|
||||
let course = db.prepare("SELECT id,is_published FROM courses WHERE subject_slug='math' AND title=?").get(COURSE_TITLE);
|
||||
let courseId;
|
||||
if (course) {
|
||||
courseId = course.id;
|
||||
console.log(` есть курс «${COURSE_TITLE}» (id ${courseId}, ${course.is_published ? 'published' : 'draft'})`);
|
||||
} else if (DRY) {
|
||||
console.log(` + создать DRAFT-курс «${COURSE_TITLE}» (created_by=${adminId()})`);
|
||||
} else {
|
||||
const by = adminId();
|
||||
// cover_emoji не указываем — применится дефолт схемы; в коде эмодзи не вводим
|
||||
courseId = db.prepare(
|
||||
'INSERT INTO courses (subject_slug,title,description,is_published,created_by) VALUES (?,?,?,0,?)'
|
||||
).run('math', COURSE_TITLE, COURSE_DESC, by).lastInsertRowid;
|
||||
console.log(` + создан DRAFT-курс «${COURSE_TITLE}» (id ${courseId}, created_by=${by})`);
|
||||
}
|
||||
|
||||
console.log('\n— Секции (course_sections) —');
|
||||
if (!courseId && DRY) {
|
||||
SECTIONS.forEach((t, i) => console.log(` + секция [${i + 1}] ${t}`));
|
||||
} else if (courseId) {
|
||||
SECTIONS.forEach((title, i) => {
|
||||
const ex = db.prepare('SELECT id FROM course_sections WHERE course_id=? AND title=?').get(courseId, title);
|
||||
if (ex) { console.log(` есть: [${i + 1}] ${title} (id ${ex.id})`); return; }
|
||||
if (DRY) { console.log(` + секция [${i + 1}] ${title}`); return; }
|
||||
const id = db.prepare('INSERT INTO course_sections (course_id,title,order_index) VALUES (?,?,?)').run(courseId, title, i + 1).lastInsertRowid;
|
||||
console.log(` + секция [${i + 1}] ${title} (id ${id})`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`\nИтог: темы +${addedTopics} (есть ${skippedTopics}); курс id=${courseId || '(dry)'}; секций ${SECTIONS.length}.`);
|
||||
console.log(DRY ? 'DRY-RUN: ничего не записано.' : 'Готово. Курс создан как ЧЕРНОВИК (is_published=0) — ученикам не виден до публикации.');
|
||||
@@ -0,0 +1,352 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2011_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2011, Вариант 1.
|
||||
Формат: Часть А = А1–А18, Часть В = В1–В12 (все В — числовые). Всего 30 заданий.
|
||||
Перенабрано вручную в KaTeX по PDF: F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\2011\ЦТ 2011 В1-В10.pdf
|
||||
(несмотря на имя «В1-В10», тест полный: А1–А18 + В1–В12; ответы — «Ответы 2011.pdf», столбец 1).
|
||||
|
||||
⚠️ ВСЕ 30 ответов решены самостоятельно и СВЕРЕНЫ с официальной таблицей — полное
|
||||
совпадение, включая B2=150, B8=16, B10=10, B12=26. variant=121. Прогнан через
|
||||
дедуп-гейт (check_variant_dups.js) — без повторов с видимым пулом.
|
||||
|
||||
Уточнения по таблице (скан неоднозначен по степеням/индексам):
|
||||
• А6: степень $3x+4$ → $2^{3x+4}-2^{3x}=15\cdot2^{3x}$;
|
||||
• А9: $3^{-12}\cdot(3^{-2})^{-5}=3^{-2}=\tfrac19$;
|
||||
• А7: корень уравнения с радикалом = $-3$ (корень линейного множителя вне ОДЗ отброшен);
|
||||
• А10: осевое сечение $=10$ → боковая $=10\pi$.
|
||||
Реконструкции «с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А1 (tg не определена) → точки в тексте, ровно одна вида $\tfrac{\pi}{2}+\pi k$ ($-\tfrac{5\pi}{2}$);
|
||||
• А2 (параллелограмм на сетке) → основание/высота числами ($5\times4=20$);
|
||||
• B6 (парабола+прямая) → парабола $y=x^2-6x+9$ и прямая $y=1{,}25$ заданы явно ($4x_1x_2=31$).
|
||||
Без авторских ссылок (политика «все учебники наши»).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2011_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2011_v1.js --apply # запись в БД
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 121;
|
||||
const N_TASKS = 30;
|
||||
const PROV = 'ЦТ–2011, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 1,
|
||||
text: R`Функция $y=\operatorname{tg}x$ не определена в точке:`,
|
||||
opts: mc('$2\pi$', '$-\dfrac{5\pi}{2}$', '$\dfrac{2\pi}{5}$', '$\dfrac{\pi}{4}$', '$-3\pi$'),
|
||||
answer: 'б',
|
||||
sol: R`$\operatorname{tg}x$ не определён при $x=\dfrac{\pi}{2}+\pi k$. Из перечисленных таково $-\dfrac{5\pi}{2}=\dfrac{\pi}{2}-3\pi$.` },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 1,
|
||||
text: R`Параллелограмм изображён на клетчатой бумаге с клетками $1\times1$ см: его основание равно $5$ см, а высота, проведённая к этому основанию, равна $4$ см. Найдите площадь параллелограмма (в квадратных сантиметрах).`,
|
||||
opts: mc('$10$', '$25$', '$15$', '$20$', '$18$'),
|
||||
answer: 'г',
|
||||
sol: R`Площадь параллелограмма $=$ основание $\times$ высоту $=5\cdot4=20$ см².` },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-fractions', diff: 2,
|
||||
text: R`Если $7\tfrac29:x=4\tfrac13:3\tfrac35$ — верная пропорция, то число $x$ равно:`,
|
||||
opts: mc('$\dfrac23$', '$6$', '$\dfrac54$', '$\dfrac49$', '$1{,}5$'),
|
||||
answer: 'б',
|
||||
sol: R`$x=\dfrac{7\tfrac29\cdot3\tfrac35}{4\tfrac13}=\dfrac{\tfrac{65}{9}\cdot\tfrac{18}{5}}{\tfrac{13}{3}}=\dfrac{26}{\tfrac{13}{3}}=6$.` },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 1,
|
||||
text: R`Если $15\%$ некоторого числа равны $33$, то $20\%$ этого числа равны:`,
|
||||
opts: mc('$44$', '$46$', '$55$', '$56$', '$66$'),
|
||||
answer: 'а',
|
||||
sol: R`Число $=\dfrac{33}{0{,}15}=220$, тогда $20\%=0{,}2\cdot220=44$.` },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1,
|
||||
text: R`Если $9x-24=0$, то $18x-31$ равно:`,
|
||||
opts: mc('$13$', '$-17$', '$17$', '$21$', '$-19$'),
|
||||
answer: 'в',
|
||||
sol: R`$x=\dfrac{24}{9}=\dfrac83$, поэтому $18x=48$ и $18x-31=17$.` },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Для любого числа $x$ выражение $2^{3x+4}-2^{3x}$ равно:`,
|
||||
opts: mc('$15\cdot2^{3x}$', '$16$', '$2^{6x+1}$', '$\dfrac23\cdot2^{3x}$', '$2^{3x}$'),
|
||||
answer: 'а',
|
||||
sol: R`$2^{3x+4}-2^{3x}=2^{3x}\left(2^{4}-1\right)=15\cdot2^{3x}$.` },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'equations', subtopic: 'eq-irrational', diff: 2,
|
||||
text: R`Сумма корней (корень, если он один) уравнения $(x+5)\sqrt{x+3}=0$ равна:`,
|
||||
opts: mc('$-1$', '$3$', '$1$', '$-3$', '$-2$'),
|
||||
answer: 'г',
|
||||
sol: R`ОДЗ: $x\ge-3$. Корень $x=-5$ не входит в ОДЗ, остаётся $x=-3$ (из $\sqrt{x+3}=0$). Единственный корень $-3$.` },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'equations', subtopic: 'eq-quadratic', diff: 2,
|
||||
text: R`От листа жести, имеющего форму квадрата, отрезали прямоугольную полосу шириной $7$ дм, после чего площадь оставшейся части листа оказалась равной $30$ дм². Длина стороны квадратного листа (в дециметрах) была равна:`,
|
||||
opts: mc('$11$', '$12$', '$3$', '$9$', '$10$'),
|
||||
answer: 'д',
|
||||
sol: R`Если сторона квадрата $a$, то $a(a-7)=30$, $a^{2}-7a-30=0$, $a=10$ (второй корень $-3$ отброшен).` },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Значение выражения $3^{-12}\cdot\left(3^{-2}\right)^{-5}$ равно:`,
|
||||
opts: mc('$81$', '$3^{-22}$', '$9$', '$3^{-12}$', '$\dfrac19$'),
|
||||
answer: 'д',
|
||||
sol: R`$\left(3^{-2}\right)^{-5}=3^{10}$, поэтому $3^{-12}\cdot3^{10}=3^{-2}=\dfrac19$.` },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Площадь осевого сечения цилиндра равна $10$. Площадь его боковой поверхности равна:`,
|
||||
opts: mc('$5\pi$', '$10\pi$', '$20\pi$', '$100\pi$', '$10$'),
|
||||
answer: 'б',
|
||||
sol: R`Осевое сечение — прямоугольник $2r\times h$ площадью $2rh=10$. Боковая поверхность $=2\pi rh=\pi\cdot2rh=10\pi$.` },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'numbers', subtopic: 'num-fractions', diff: 2,
|
||||
text: R`Найдите значение выражения $230\cdot\dfrac29-\left(\dfrac29+\dfrac1{10}\right):\dfrac1{230}$.`,
|
||||
opts: mc('$0{,}1$', '$43\tfrac49$', '$-0{,}1$', '$-23$', '$23$'),
|
||||
answer: 'г',
|
||||
sol: R`$230\cdot\dfrac29=\dfrac{460}{9}$; $\left(\dfrac{29}{90}\right)\cdot230=\dfrac{667}{9}$. Разность $\dfrac{460-667}{9}=-\dfrac{207}{9}=-23$.` },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Упростите выражение $\dfrac{x^{2}-22x+121}{x^{2}-11x}:\dfrac{x^{2}-121}{x^{3}}$.`,
|
||||
opts: mc('$\dfrac{x}{x+11}$', '$\dfrac{(x-11)^{2}}{x^{4}}$', '$\dfrac{x-11}{x+11}$', '$\dfrac{x^{2}}{x-11}$', '$\dfrac{x^{2}}{x+11}$'),
|
||||
answer: 'д',
|
||||
sol: R`$\dfrac{(x-11)^{2}}{x(x-11)}\cdot\dfrac{x^{3}}{(x-11)(x+11)}=\dfrac{x^{2}}{x+11}$.` },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Параллельно стороне треугольника, равной $5$, проведена прямая. Длина отрезка этой прямой, заключённого между сторонами треугольника, равна $2$. Найдите отношение площади полученной трапеции к площади исходного треугольника.`,
|
||||
opts: mc('$\dfrac25$', '$0{,}6$', '$\dfrac{21}{25}$', '$\dfrac{4}{25}$', '$\dfrac{3}{25}$'),
|
||||
answer: 'в',
|
||||
sol: R`Отсечённый треугольник подобен исходному с коэффициентом $\dfrac25$, его площадь составляет $\dfrac{4}{25}$. Трапеция: $1-\dfrac{4}{25}=\dfrac{21}{25}$.` },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Сумма координат точки пересечения прямых, заданных уравнениями $2x+5y=11$ и $x+y=2(5-y)$, равна:`,
|
||||
opts: mc('$8$', '$-8$', '$10$', '$-10$', '$6$'),
|
||||
answer: 'б',
|
||||
sol: R`Второе уравнение: $x+3y=10$. Из системы $y=9$, $x=-17$. Сумма координат $-17+9=-8$.` },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Количество целых решений неравенства $\dfrac{(x+3)^{2}-6x-18}{(x-5)^{2}}>0$ на промежутке $[-4;5]$ равно:`,
|
||||
opts: mc('$2$', '$7$', '$4$', '$5$', '$3$'),
|
||||
answer: 'а',
|
||||
sol: R`Числитель $(x+3)^{2}-6x-18=x^{2}-9$. При $x\ne5$ знаменатель положителен, поэтому неравенство равносильно $x^{2}-9>0$, то есть $x<-3$ или $x>3$. На $[-4;5]$ это $x=-4$ и $x=4$ — $2$ решения.` },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
|
||||
text: R`В ромб площадью $18\sqrt5$ вписан круг площадью $5\pi$. Сторона ромба равна:`,
|
||||
opts: mc('$8$', '$18$', '$\dfrac{9\sqrt5}{5}$', '$\dfrac{18\sqrt5}{5}$', '$9$'),
|
||||
answer: 'д',
|
||||
sol: R`Радиус вписанного круга: $\pi r^{2}=5\pi$, $r=\sqrt5$; высота ромба $h=2r=2\sqrt5$. Площадь $=a\cdot h$: $18\sqrt5=a\cdot2\sqrt5$, $a=9$.` },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Расположите числа $\sqrt[12]{80}$; $\sqrt[3]{3}$; $\sqrt[4]{4}$ в порядке возрастания.`,
|
||||
opts: mc('$\sqrt[4]{4};\ \sqrt[3]{3};\ \sqrt[12]{80}$', '$\sqrt[3]{3};\ \sqrt[4]{4};\ \sqrt[12]{80}$', '$\sqrt[3]{3};\ \sqrt[12]{80};\ \sqrt[4]{4}$', '$\sqrt[4]{4};\ \sqrt[12]{80};\ \sqrt[3]{3}$', '$\sqrt[12]{80};\ \sqrt[3]{3};\ \sqrt[4]{4}$'),
|
||||
answer: 'г',
|
||||
sol: R`Возведём в $12$-ю степень: $\left(\sqrt[12]{80}\right)^{12}=80$, $\left(\sqrt[3]{3}\right)^{12}=81$, $\left(\sqrt[4]{4}\right)^{12}=64$. Так как $64<80<81$, порядок: $\sqrt[4]{4};\ \sqrt[12]{80};\ \sqrt[3]{3}$.` },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите наименьший положительный корень уравнения $4\sin^{2}x+12\cos x-9=0$.`,
|
||||
opts: mc('$\dfrac{2\pi}{3}$', '$\arccos\dfrac52$', '$\dfrac{\pi}{3}$', '$\dfrac{\pi}{6}$', '$\pi-\arccos\dfrac52$'),
|
||||
answer: 'в',
|
||||
sol: R`$4(1-\cos^{2}x)+12\cos x-9=0$, то есть $4\cos^{2}x-12\cos x+5=0$, $\cos x=\dfrac12$ (второй корень $\dfrac52$ невозможен). Наименьший положительный корень $\dfrac{\pi}{3}$.` },
|
||||
|
||||
// ── Часть B: В1–В12 (все числовые) ───────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите произведение корней уравнения $\dfrac{3}{x+1}+1=\dfrac{10}{x^{2}+2x+1}$.`,
|
||||
answer: '-6',
|
||||
sol: R`Пусть $u=x+1$: $\dfrac3u+1=\dfrac{10}{u^{2}}$, $u^{2}+3u-10=0$, $u=2$ или $u=-5$. Тогда $x=1$ или $x=-6$, произведение $-6$.` },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`Диагонали трапеции равны $15$ и $20$. Найдите площадь трапеции, если её средняя линия равна $12{,}5$.`,
|
||||
answer: '150',
|
||||
sol: R`Площадь трапеции равна площади треугольника со сторонами, равными диагоналям ($15$ и $20$), и основанием, равным сумме оснований $=2\cdot12{,}5=25$. Так как $15^{2}+20^{2}=25^{2}$, треугольник прямоугольный: площадь $=\tfrac12\cdot15\cdot20=150$.` },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму корней (или корень, если он один) уравнения $2\cdot6^{\log_7 x}=108-x^{\log_7 6}$.`,
|
||||
answer: '49',
|
||||
sol: R`Так как $x^{\log_7 6}=6^{\log_7 x}$, обозначим $t=6^{\log_7 x}$: $2t=108-t$, $t=36=6^{2}$. Тогда $\log_7 x=2$, $x=49$ — единственный корень.` },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите сумму целых решений неравенства $2^{3x+4}-10\cdot4^{x}+2^{x}\le0$.`,
|
||||
answer: '-6',
|
||||
sol: R`Пусть $u=2^{x}>0$: $16u^{3}-10u^{2}+u\le0$, $u(16u^{2}-10u+1)\le0$, $\dfrac18\le u\le\dfrac12$. Значит $-3\le x\le-1$; сумма целых $-3-2-1=-6$.` },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 4,
|
||||
text: R`По двум перпендикулярным прямым, которые пересекаются в точке $O$, движутся две точки $M_1$ и $M_2$ по направлению к точке $O$ со скоростями $1$ м/с и $2$ м/с соответственно. Достигнув точки $O$, они продолжают своё движение. В первоначальный момент времени $M_1O=5$ м, $M_2O=20$ м. Через сколько секунд расстояние между точками $M_1$ и $M_2$ будет минимальным?`,
|
||||
answer: '9',
|
||||
sol: R`Расстояние: $d^{2}=(5-t)^{2}+(20-2t)^{2}=5t^{2}-90t+425$. Минимум при $t=\dfrac{90}{10}=9$ с.` },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'functions', subtopic: 'fn-graphs', diff: 4,
|
||||
text: R`Парабола $y=x^{2}-6x+9$ и горизонтальная прямая $y=1{,}25$ пересекаются в точках с абсциссами $x_1$ и $x_2$. Найдите значение выражения $4x_1\cdot x_2$.`,
|
||||
answer: '31',
|
||||
sol: R`$x^{2}-6x+9=1{,}25$, то есть $x^{2}-6x+7{,}75=0$. По теореме Виета $x_1 x_2=7{,}75$, поэтому $4x_1 x_2=31$.` },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'planimetry', subtopic: 'plan-circle', diff: 4,
|
||||
text: R`Четырёхугольник $ABCD$ вписан в окружность. Если $\angle BAC=40^\circ$ и $\angle ABD=75^\circ$, то градусная мера угла между прямыми $AB$ и $CD$ равна … .`,
|
||||
answer: '35',
|
||||
sol: R`$\angle BAC=40^\circ$ опирается на дугу $BC=80^\circ$, $\angle ABD=75^\circ$ — на дугу $AD=150^\circ$. Угол между прямыми $AB$ и $CD$ равен полуразности дуг: $\dfrac{150^\circ-80^\circ}{2}=35^\circ$.` },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 4,
|
||||
text: R`Найдите значение выражения $\dfrac{\sin^{2}184^\circ}{4\sin^{2}23^\circ\cdot\sin^{2}2^\circ\cdot\sin^{2}44^\circ\cdot\sin^{2}67^\circ}$.`,
|
||||
answer: '16',
|
||||
sol: R`$\sin67^\circ=\cos23^\circ$ и $\sin46^\circ=\cos44^\circ$ дают знаменатель $=\dfrac1{16}\sin^{2}4^\circ$. Числитель $\sin^{2}184^\circ=\sin^{2}4^\circ$. Отношение $=16$.` },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`В арифметической прогрессии $130$ членов, их сумма равна $130$, а сумма членов с чётными номерами на $130$ больше суммы членов с нечётными номерами. Найдите сотый член этой прогрессии.`,
|
||||
answer: '70',
|
||||
sol: R`Сумма чётных членов равна $130$, нечётных — $0$. Разность сумм $=65d=130$, поэтому $d=2$. Из общей суммы $a_1=-128$, тогда $a_{100}=a_1+99d=-128+198=70$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`В равнобокой трапеции бóльшее основание вдвое больше каждой из остальных сторон и лежит в плоскости $\alpha$. Боковая сторона образует с плоскостью $\alpha$ угол, синус которого равен $\dfrac{5\sqrt3}{18}$. Найдите $36\sin\beta$, где $\beta$ — угол между диагональю трапеции и плоскостью $\alpha$.`,
|
||||
answer: '10',
|
||||
sol: R`Пусть боковая сторона $=b$, тогда основания $b$ и $2b$, высота трапеции $\dfrac{b\sqrt3}{2}$. Из условия $\dfrac{\sqrt3}{2}\sin\theta=\dfrac{5\sqrt3}{18}$ получаем $\sin\theta=\dfrac59$. Длина диагонали $=b\sqrt3$, и $\sin\beta=\dfrac{\sin\theta}{2}=\dfrac{5}{18}$. Значит $36\sin\beta=10$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 5,
|
||||
text: R`Количество целых решений неравенства $2^{x+6}+\log_{0{,}5}(6-x)>13$ равно … .`,
|
||||
answer: '7',
|
||||
sol: R`ОДЗ: $x<6$. При $x=-2$ левая часть равна ровно $13$ (не годится), при $x\le-3$ меньше $13$, а при $-1\le x\le5$ — больше $13$. Целые решения: $-1,0,1,2,3,4,5$ — всего $7$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`Основанием пирамиды $SABCD$ является ромб со стороной $2\sqrt3$ и углом $BAD$, равным $\arccos\dfrac34$. Ребро $SD$ перпендикулярно основанию, а ребро $SB$ образует с основанием угол $60^\circ$. Найдите радиус $R$ сферы, проходящей через точки $A$, $B$, $C$ и середину ребра $SB$. В ответ запишите $R^{2}$.`,
|
||||
answer: '26',
|
||||
sol: R`Диагональ $BD=\sqrt{2\cdot12\left(1-\tfrac34\right)}=\sqrt6$, $SD=BD\cdot\operatorname{tg}60^\circ=3\sqrt2$. В координатах с центром ромба: $A(0;\tfrac{\sqrt{42}}2;0)$, $B(\tfrac{\sqrt6}2;0;0)$, $C(0;-\tfrac{\sqrt{42}}2;0)$, середина $SB$ $=(0;0;\tfrac{3\sqrt2}2)$. Центр сферы $\left(-\tfrac{3\sqrt6}2;0;-\sqrt2\right)$, $R^{2}=\tfrac{54}{4}+\tfrac{42}{4}+2=26$.` },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2011_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_ct2011_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 → «Варианты» → «ЦТ-2011».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,348 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2012_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2012, Вариант 1.
|
||||
Формат: Часть А = А1–А18, Часть В = В1–В12 (все В — числовые). Всего 30 заданий.
|
||||
Перенабрано вручную в KaTeX по PDF: F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\2012\ЦТ 2012.pdf
|
||||
(ответы — отдельный файл «Ответы 2012.pdf», столбец «Вариант 1»).
|
||||
|
||||
⚠️ ВСЕ 30 ответов решены самостоятельно и СВЕРЕНЫ с официальной таблицей — полное
|
||||
совпадение, включая B7=9, B10=84, B11=90, B12=-180. variant=120. Прогнан через
|
||||
дедуп-гейт (check_variant_dups.js) — без повторов с видимым пулом.
|
||||
|
||||
Реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А1 (равнобедренный треугольник) → пары углов даны числами (70°,40° → равнобедренный, №3);
|
||||
• А13 (прямая/плоскость/двугранный угол) → все данные в тексте (площадь 14√3);
|
||||
• B6 (середины сторон прямоугольника) → расположение M,N,P,Q задано в тексте (площадь 4).
|
||||
А15 уточнена по таблице: радикал $\sqrt{5^{5}\cdot20}=250$, знаменатель $\sqrt[4]{10}$ → $25\sqrt[4]{10}$.
|
||||
Без авторских ссылок (политика «все учебники наши»).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2012_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2012_v1.js --apply # запись в БД
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 120;
|
||||
const N_TASKS = 30;
|
||||
const PROV = 'ЦТ–2012, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1,
|
||||
text: R`У каждого из пяти треугольников на рисунке известны два угла. Укажите номер треугольника, который является равнобедренным: $1)\ 55^\circ$ и $40^\circ$; $\ 2)\ 60^\circ$ и $40^\circ$; $\ 3)\ 70^\circ$ и $40^\circ$; $\ 4)\ 65^\circ$ и $40^\circ$; $\ 5)\ 75^\circ$ и $40^\circ$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'в',
|
||||
sol: R`Третий угол равен $180^\circ$ минус два данных. Для пары $70^\circ$ и $40^\circ$ третий угол $=70^\circ$, появляются два равных угла — треугольник равнобедренный (№3).` },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'expressions', subtopic: 'expr-logarithms', diff: 2,
|
||||
text: R`Укажите верное равенство:<br>$1)\ 3^{\log_3 3}=5$; $\ 2)\ \log_7 7=7$; $\ 3)\ \log_{31}\dfrac{1}{31}=-1$; $\ 4)\ \log_5 25=5$; $\ 5)\ \log_{23} 23=0$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'в',
|
||||
sol: R`$\log_{31}\dfrac{1}{31}=\log_{31}31^{-1}=-1$ — верно (равенство 3). Остальные ложны: $3^{\log_3 3}=3$, $\log_7 7=1$, $\log_5 25=2$, $\log_{23}23=1$.` },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
|
||||
text: R`Сумма всех натуральных делителей числа $28$ равна:`,
|
||||
opts: mc('$55$', '$11$', '$9$', '$27$', '$56$'),
|
||||
answer: 'д',
|
||||
sol: R`Делители $28$: $1,2,4,7,14,28$. Их сумма $=56$.` },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-quadratic', diff: 2,
|
||||
text: R`Даны квадратные уравнения: $1)\ 4x^{2}-3x-3=0$; $\ 2)\ 5x^{2}+20x+20=0$; $\ 3)\ 2x^{2}+3x+12=0$; $\ 4)\ 7x^{2}-4x-5=0$; $\ 5)\ 4x^{2}+8x+4=0$. Укажите уравнение, которое не имеет корней.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'в',
|
||||
sol: R`Корней нет при $D<0$. Для $2x^{2}+3x+12=0$: $D=9-96=-87<0$ (№3). У остальных $D\ge0$.` },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Если $10^{2}\cdot\alpha=741{,}63287$, то значение $\alpha$ с точностью до сотых равно:`,
|
||||
opts: mc('$74{,}16$', '$7{,}42$', '$7{,}41$', '$74\,163{,}29$', '$7416{,}33$'),
|
||||
answer: 'б',
|
||||
sol: R`$\alpha=\dfrac{741{,}63287}{100}=7{,}4163287\approx7{,}42$.` },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Число $133$ является членом арифметической прогрессии $4,\ 7,\ 10,\ 13,\ \ldots$ Укажите его номер.`,
|
||||
opts: mc('$44$', '$42$', '$40$', '$46$', '$48$'),
|
||||
answer: 'а',
|
||||
sol: R`$a_n=4+3(n-1)=3n+1$. Из $3n+1=133$ получаем $n=44$.` },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'equations', subtopic: 'eq-modulus', diff: 2,
|
||||
text: R`Решите неравенство $|-x|\ge5$.`,
|
||||
opts: mc('$x\in[5;+\infty)$', '$x\in(-\infty;-5]$', '$x\in[-5;5]$', '$x\in(-\infty;-5]\cup[5;+\infty)$', '$x_1=-5,\ x_2=5$'),
|
||||
answer: 'г',
|
||||
sol: R`$|-x|=|x|\ge5$ равносильно $x\le-5$ или $x\ge5$, то есть $x\in(-\infty;-5]\cup[5;+\infty)$.` },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-fractions', diff: 2,
|
||||
text: R`Вычислите $\dfrac{3{,}2+0{,}8:\left(\tfrac16+\tfrac13\right)}{0{,}1}$.`,
|
||||
opts: mc('$48$', '$0{,}48$', '$4{,}8$', '$80$', '$0{,}8$'),
|
||||
answer: 'а',
|
||||
sol: R`$\tfrac16+\tfrac13=\tfrac12$, $0{,}8:\tfrac12=1{,}6$, числитель $=3{,}2+1{,}6=4{,}8$. Делим на $0{,}1$: $48$.` },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'planimetry', subtopic: 'plan-circles', diff: 1,
|
||||
text: R`Площадь круга равна $81\pi$. Диаметр этого круга равен:`,
|
||||
opts: mc('$18$', '$18\pi$', '$9$', '$9\pi$', '$81$'),
|
||||
answer: 'а',
|
||||
sol: R`$\pi r^{2}=81\pi$, $r=9$, диаметр $=18$.` },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'trigonometry', subtopic: 'trig-equations', diff: 2,
|
||||
text: R`Найдите наименьший положительный корень уравнения $\sin2x=\dfrac12$.`,
|
||||
opts: mc('$\dfrac{\pi}{6}$', '$\dfrac{\pi}{12}$', '$\dfrac{\pi}{3}$', '$\dfrac{5\pi}{12}$', '$\dfrac{\pi}{8}$'),
|
||||
answer: 'б',
|
||||
sol: R`$2x=\dfrac{\pi}{6}+2\pi k$ или $2x=\dfrac{5\pi}{6}+2\pi k$, поэтому $x=\dfrac{\pi}{12}+\pi k$ или $x=\dfrac{5\pi}{12}+\pi k$. Наименьший положительный — $\dfrac{\pi}{12}$.` },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Четырёхугольник $MNPK$, в котором $\angle N=128^\circ$, вписан в окружность. Найдите градусную меру угла $K$.`,
|
||||
opts: mc('$64^\circ$', '$128^\circ$', '$100^\circ$', '$180^\circ$', '$52^\circ$'),
|
||||
answer: 'д',
|
||||
sol: R`У вписанного четырёхугольника суммы противоположных углов равны $180^\circ$. Углы $N$ и $K$ противоположны, поэтому $\angle K=180^\circ-128^\circ=52^\circ$.` },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`На одной чаше уравновешенных весов лежат $3$ яблока и $1$ груша, на другой — $2$ яблока, $2$ груши и гирька весом $20$ г. Каков вес одного яблока (в граммах), если все фрукты вместе весят $780$ г? Считайте все яблоки одинаковыми по весу и все груши одинаковыми по весу.`,
|
||||
opts: mc('$95$', '$105$', '$100$', '$125$', '$115$'),
|
||||
answer: 'б',
|
||||
sol: R`Равновесие: $3a+p=2a+2p+20$, то есть $a-p=20$. Все фрукты: $5a+3p=780$. Отсюда $a=105$, $p=85$.` },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'stereometry', subtopic: 'ster-lines-planes', diff: 3,
|
||||
text: R`Прямая $a$, параллельная плоскости $\alpha$, находится от неё на расстоянии $6$. Через прямую $a$ проведена плоскость $\beta$, пересекающая плоскость $\alpha$ по прямой $b$ и образующая с ней угол $60^\circ$. Найдите площадь четырёхугольника $ABCD$, если $A$ и $B$ — точки прямой $a$, причём $AB=4$, а $C$ и $D$ — такие точки прямой $b$, что $CD=3$.`,
|
||||
opts: mc('$42$', '$42\sqrt3$', '$\dfrac{21\sqrt3}{2}$', '$10{,}5$', '$14\sqrt3$'),
|
||||
answer: 'д',
|
||||
sol: R`Прямые $a$ и $b$ параллельны, поэтому $ABCD$ — трапеция с основаниями $AB=4$ и $CD=3$. Её высота (расстояние между $a$ и $b$ в плоскости $\beta$) равна $\dfrac{6}{\sin60^\circ}=4\sqrt3$. Площадь $=\dfrac{4+3}{2}\cdot4\sqrt3=14\sqrt3$.` },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Упростите выражение $\dfrac{125^{x}+25^{x}-12\cdot5^{x}}{5^{x}\left(5^{x}-3\right)}$.`,
|
||||
opts: mc('$5^{x}$', '$125^{x}-4$', '$5^{x}+4$', '$5^{x}-4$', '$2\cdot5^{x}$'),
|
||||
answer: 'в',
|
||||
sol: R`Пусть $u=5^{x}$. Числитель $=u^{3}+u^{2}-12u=u(u+4)(u-3)$, знаменатель $=u(u-3)$. Дробь $=u+4=5^{x}+4$.` },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Корень уравнения $\sqrt{10}\cdot x=\dfrac{\sqrt{5^{5}\cdot20}}{\sqrt[4]{10}}$ равен:`,
|
||||
opts: mc('$25\sqrt[4]{10}$', '$50\sqrt2$', '$25\sqrt[5]{50}$', '$4\sqrt[3]{20}$', '$10\sqrt{10}$'),
|
||||
answer: 'а',
|
||||
sol: R`$\sqrt{5^{5}\cdot20}=\sqrt{5^{6}\cdot4}=5^{3}\cdot2=250$, поэтому $x=\dfrac{250}{\sqrt{10}\cdot\sqrt[4]{10}}=\dfrac{250}{10^{3/4}}=25\cdot10^{1/4}=25\sqrt[4]{10}$.` },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Какая из прямых $1)\ y=-3$; $\ 2)\ y=-1{,}5$; $\ 3)\ y=0$; $\ 4)\ y=4{,}3$; $\ 5)\ y=2$ пересекает график функции $y=\dfrac14 x^{2}-3x+11$ в двух точках?`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'г',
|
||||
sol: R`Вершина параболы: $x=6$, $y_{\min}=\dfrac14\cdot36-18+11=2$, ветви вверх. Прямая $y=c$ пересекает график в двух точках при $c>2$. Это $y=4{,}3$ (№4).` },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Если $\dfrac{5x}{y}=\dfrac12$, то значение выражения $\dfrac{3y+9x}{13x-y}$ равно:`,
|
||||
opts: mc('$12$', '$13$', '$\dfrac{11}{7}$', '$\dfrac{93}{129}$', '$\dfrac{1}{13}$'),
|
||||
answer: 'б',
|
||||
sol: R`Из $\dfrac{5x}{y}=\dfrac12$ следует $y=10x$. Тогда $\dfrac{3\cdot10x+9x}{13x-10x}=\dfrac{39x}{3x}=13$.` },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Наименьшее целое решение неравенства $\lg(x^{2}-2x-8)-\lg(x+2)\le\lg4$ равно:`,
|
||||
opts: mc('$1$', '$-2$', '$4$', '$5$', '$8$'),
|
||||
answer: 'г',
|
||||
sol: R`ОДЗ: $x>4$. На нём $\dfrac{x^{2}-2x-8}{x+2}=x-4$, и неравенство $\lg(x-4)\le\lg4$ даёт $x\le8$. Итого $4<x\le8$; наименьшее целое — $5$.` },
|
||||
|
||||
// ── Часть B: В1–В12 (все числовые) ───────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`Если в правильной четырёхугольной пирамиде высота равна $4$, а площадь диагонального сечения равна $12$, то её объём равен … .`,
|
||||
answer: '24',
|
||||
sol: R`Диагональное сечение — треугольник с основанием $d$ (диагональ квадрата) и высотой $4$: $\tfrac12 d\cdot4=12$, $d=6$. Сторона основания $a=\dfrac{d}{\sqrt2}=3\sqrt2$, площадь основания $=18$. Объём $=\tfrac13\cdot18\cdot4=24$.` },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите количество всех целых решений неравенства $\dfrac{64x-x^{3}}{5x}>0$.`,
|
||||
answer: '14',
|
||||
sol: R`При $x\ne0$ неравенство равносильно $\dfrac{64-x^{2}}{5}>0$, то есть $-8<x<8$. Целые (без $0$): от $-7$ до $7$ — это $14$ чисел.` },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'planimetry', subtopic: 'plan-coordinates', diff: 3,
|
||||
text: R`Точки $A(1;2)$, $B(5;6)$ и $C(8;6)$ — вершины трапеции $ABCD$ ($AD\parallel BC$). Найдите сумму координат точки $D$, если $BD=4\sqrt2$.`,
|
||||
answer: '11',
|
||||
sol: R`$BC$ горизонтальна, значит $AD$ тоже горизонтальна и $D$ имеет ординату $2$. Из $BD^{2}=(d-5)^{2}+16=32$ получаем $d=9$ ($d=1$ даёт $D=A$). Тогда $D(9;2)$, сумма координат $11$.` },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'planimetry', subtopic: 'plan-polygons', diff: 3,
|
||||
text: R`Найдите периметр правильного шестиугольника, меньшая диагональ которого равна $10\sqrt3$.`,
|
||||
answer: '60',
|
||||
sol: R`У правильного шестиугольника меньшая диагональ равна $a\sqrt3$, поэтому $a\sqrt3=10\sqrt3$, $a=10$. Периметр $=6a=60$.` },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите произведение корней уравнения $4^{x^{2}}+128=3^{1-x^{2}}\cdot12^{x^{2}}$.`,
|
||||
answer: '-3',
|
||||
sol: R`Пусть $u=x^{2}$. Так как $3^{1-u}\cdot12^{u}=3\cdot4^{u}$, уравнение даёт $4^{u}+128=3\cdot4^{u}$, $4^{u}=64$, $u=3$. Тогда $x=\pm\sqrt3$, произведение корней $-3$.` },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 5,
|
||||
text: R`Площадь прямоугольника $ABCD$ равна $20$. Точки $M$, $N$, $P$, $Q$ — середины его сторон $AB$, $BC$, $CD$, $DA$ соответственно. Найдите площадь четырёхугольника, заключённого между прямыми $AN$, $BP$, $CQ$ и $DM$.`,
|
||||
answer: '4',
|
||||
sol: R`Прямые $AN\parallel CQ$ и $BP\parallel DM$, поэтому внутренний четырёхугольник — параллелограмм. Координатный расчёт показывает, что его площадь составляет $\dfrac15$ площади прямоугольника: $\dfrac{20}{5}=4$.` },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Решите уравнение $x^{2}-7x+10=\dfrac{7}{x^{2}-11x+28}$ и найдите сумму его корней.`,
|
||||
answer: '9',
|
||||
sol: R`Уравнение приводится к $(x^{2}-9x+21)(x^{2}-9x+13)=0$. Первый множитель действительных корней не имеет ($D<0$), второй даёт корни с суммой $9$.` },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 4,
|
||||
text: R`Найдите значение выражения $16\sin\left(\alpha-\dfrac{\pi}{4}\right)$, если $\sin2\alpha=\dfrac{23}{32}$ и $2\alpha\in\left(0;\dfrac{\pi}{2}\right)$.`,
|
||||
answer: '-6',
|
||||
sol: R`$16\sin\left(\alpha-\tfrac{\pi}{4}\right)=8\sqrt2(\sin\alpha-\cos\alpha)$. Так как $(\sin\alpha-\cos\alpha)^{2}=1-\sin2\alpha=\tfrac{9}{32}$ и при $\alpha<\tfrac{\pi}{4}$ разность отрицательна, $\sin\alpha-\cos\alpha=-\tfrac{3\sqrt2}{8}$. Значение $=8\sqrt2\cdot\left(-\tfrac{3\sqrt2}{8}\right)=-6$.` },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 4,
|
||||
text: R`Найдите сумму целых значений $x$, принадлежащих области определения функции $y=\log_{2-x}\left(12-x-x^{2}\right)$.`,
|
||||
answer: '-6',
|
||||
sol: R`Условия: $2-x>0$, $2-x\ne1$ и $12-x-x^{2}>0$. Получаем $-4<x<2$, $x\ne1$. Целые: $-3,-2,-1,0$; их сумма $-6$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
|
||||
text: R`Прямоугольный треугольник с катетами, равными $6$ и $2\sqrt7$, вращается вокруг оси, содержащей его гипотенузу. Найдите значение выражения $\dfrac{2V}{\pi}$, где $V$ — объём фигуры вращения.`,
|
||||
answer: '84',
|
||||
sol: R`Гипотенуза $=\sqrt{36+28}=8$, высота к ней $h=\dfrac{6\cdot2\sqrt7}{8}=\dfrac{3\sqrt7}{2}$. Фигура — два конуса с общим основанием: $V=\tfrac13\pi h^{2}\cdot8=\tfrac13\pi\cdot\tfrac{63}{4}\cdot8=42\pi$. Тогда $\dfrac{2V}{\pi}=84$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 5,
|
||||
text: R`Из двух растворов с различным процентным содержанием спирта массой $100$ г и $900$ г отлили по одинаковому количеству. Каждый из отлитых растворов долили в остаток другого раствора, после чего процентное содержание спирта в обоих растворах стало одинаковым. Найдите, сколько раствора (в граммах) было отлито из каждого раствора.`,
|
||||
answer: '90',
|
||||
sol: R`Пусть отлито по $m$ г. Равенство итоговых концентраций приводит к $(900-10m)(c_1-c_2)=0$. Поскольку концентрации различны, $900-10m=0$, $m=90$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 5,
|
||||
text: R`Найдите произведение корней уравнения $x-\sqrt{x^{2}-36}=\dfrac{(x-6)^{2}}{2x+12}$.`,
|
||||
answer: '-180',
|
||||
sol: R`ОДЗ: $|x|\ge6$. После преобразований и возведения в квадрат получаем $x^{4}-168x^{2}-2160=0$, откуда $x^{2}=180$, то есть $x=\pm6\sqrt5$ (оба корня подходят). Произведение $=-180$.` },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2012_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_ct2012_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 → «Варианты» → «ЦТ-2012».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,348 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2013_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2013, Вариант 1.
|
||||
Формат: Часть А = А1–А18, Часть В = В1–В12 (все В — числовые). Всего 30 заданий.
|
||||
Перенабрано вручную в KaTeX по PDF: F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\2013\ЦТ2013.pdf
|
||||
(ответы — отдельный файл «Ответы ЦТ 2013.pdf», столбец «Вариант 1»).
|
||||
|
||||
⚠️ ВСЕ 30 ответов решены самостоятельно и СВЕРЕНЫ с официальной таблицей — полное
|
||||
совпадение, включая B3=75, B9=40, B10=6, B12=-5. variant=119. Прогнан через
|
||||
дедуп-гейт (check_variant_dups.js) — без повторов с видимым пулом.
|
||||
|
||||
Реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А2 (образующая цилиндра) → взаимное расположение точек дано в тексте (AD ⟂ основаниям → AD);
|
||||
• А3 (точка на графике) → прямая задана как $y=13$, точки перечислены (T(-7;13));
|
||||
• А6 (углы при развёрнутом угле) → порядок лучей задан явно (∠BOC=40°);
|
||||
• А16 (сечение параллелепипеда) → размеры/угол 60° в тексте (сечение 12×6=72).
|
||||
Без авторских ссылок (политика «все учебники наши»).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2013_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2013_v1.js --apply # запись в БД
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 119;
|
||||
const N_TASKS = 30;
|
||||
const PROV = 'ЦТ–2013, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Среди чисел $\sqrt9$; $-9$; $\dfrac19$; $-0{,}9$; $9^{-1}$ выберите число, противоположное числу $9$.`,
|
||||
opts: mc('$\sqrt9$', '$-9$', '$\dfrac19$', '$-0{,}9$', '$9^{-1}$'),
|
||||
answer: 'б',
|
||||
sol: R`Противоположное числу $9$ — это $-9$.` },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 1,
|
||||
text: R`Прямой круговой цилиндр; $O$ и $O_1$ — центры верхнего и нижнего оснований. Точки $A$ и $B$ лежат на окружности верхнего основания, $C$ и $D$ — на окружности нижнего, причём $A$ находится точно над $D$ (отрезок $AD$ перпендикулярен основаниям). Образующей цилиндра является отрезок:`,
|
||||
opts: mc('$DB$', '$DC$', '$DO_1$', '$OO_1$', '$AD$'),
|
||||
answer: 'д',
|
||||
sol: R`Образующая прямого цилиндра — отрезок поверхности, перпендикулярный основаниям и соединяющий соответствующие точки окружностей. Это отрезок $AD$ ($OO_1$ — ось, а не образующая).` },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Среди точек $B(13;0)$, $T(-7;13)$, $C\left(-\sqrt{13};\sqrt{13}\right)$, $O(0;0)$, $L(0;-13)$ выберите ту, которая принадлежит графику функции $y=13$.`,
|
||||
opts: mc('$B$', '$T$', '$C$', '$O$', '$L$'),
|
||||
answer: 'б',
|
||||
sol: R`Графику $y=13$ принадлежат точки с ординатой $13$. Это $T(-7;13)$.` },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'numbers', subtopic: 'num-fractions', diff: 2,
|
||||
text: R`Найдите значение выражения $\left(2\tfrac{7}{12}-2\tfrac{17}{36}\right)\cdot2{,}7-0{,}4$.`,
|
||||
opts: mc('$0{,}1$', '$-0{,}7$', '$-0{,}1$', '$0{,}3$', '$-1{,}5$'),
|
||||
answer: 'в',
|
||||
sol: R`$2\tfrac{7}{12}-2\tfrac{17}{36}=\tfrac{93-89}{36}=\tfrac{4}{36}=\tfrac19$. Тогда $\tfrac19\cdot2{,}7-0{,}4=0{,}3-0{,}4=-0{,}1$.` },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Одно число меньше другого на $64$, что составляет $16\%$ большего числа. Найдите меньшее число.`,
|
||||
opts: mc('$800$', '$470$', '$336$', '$464$', '$390$'),
|
||||
answer: 'в',
|
||||
sol: R`Большее число $=\dfrac{64}{0{,}16}=400$, меньшее $=400-64=336$.` },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'planimetry', subtopic: 'plan-angles', diff: 2,
|
||||
text: R`Угол $AOM$ — развёрнутый ($A$, $O$, $M$ на одной прямой). Лучи $OB$ и $OC$ проведены по одну сторону от прямой $AM$, причём луч $OB$ ближе к лучу $OA$. Известно, что $\angle AOC=107^\circ$, $\angle BOM=113^\circ$. Найдите величину угла $BOC$.`,
|
||||
opts: mc('$73^\circ$', '$67^\circ$', '$17^\circ$', '$40^\circ$', '$23^\circ$'),
|
||||
answer: 'г',
|
||||
sol: R`$\angle AOB=180^\circ-\angle BOM=67^\circ$, поэтому $\angle BOC=\angle AOC-\angle AOB=107^\circ-67^\circ=40^\circ$.` },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Образующая конуса равна $26$ и наклонена к плоскости основания под углом $60^\circ$. Найдите площадь боковой поверхности конуса.`,
|
||||
opts: mc('$338\pi$', '$338\sqrt3\,\pi$', '$169\pi$', '$260\sqrt3\,\pi$', '$676\pi$'),
|
||||
answer: 'а',
|
||||
sol: R`Радиус $r=l\cos60^\circ=26\cdot\tfrac12=13$. Боковая поверхность $=\pi r l=\pi\cdot13\cdot26=338\pi$.` },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Расположите числа $2{,}44$; $\dfrac{18}{7}$; $2{,}(4)$ в порядке возрастания.`,
|
||||
opts: mc('$2{,}44;\ \dfrac{18}{7};\ 2{,}(4)$', '$2{,}44;\ 2{,}(4);\ \dfrac{18}{7}$', '$\dfrac{18}{7};\ 2{,}44;\ 2{,}(4)$', '$2{,}(4);\ \dfrac{18}{7};\ 2{,}44$', '$2{,}(4);\ 2{,}44;\ \dfrac{18}{7}$'),
|
||||
answer: 'б',
|
||||
sol: R`$2{,}44<2{,}(4)=2{,}444\ldots<\dfrac{18}{7}=2{,}571\ldots$, то есть $2{,}44;\ 2{,}(4);\ \dfrac{18}{7}$.` },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'equations', subtopic: 'eq-quadratic', diff: 2,
|
||||
text: R`Одна из сторон прямоугольника на $7$ см длиннее другой, а его площадь равна $78$ см². Уравнение, одним из корней которого является длина меньшей стороны прямоугольника, имеет вид:`,
|
||||
opts: mc('$x^{2}-78x+7=0$', '$x^{2}-7x-78=0$', '$x^{2}+7x+78=0$', '$x^{2}+7x-78=0$', '$x^{2}+78x-7=0$'),
|
||||
answer: 'г',
|
||||
sol: R`Если меньшая сторона $x$, то $x(x+7)=78$, то есть $x^{2}+7x-78=0$.` },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'planimetry', subtopic: 'plan-coordinates', diff: 2,
|
||||
text: R`Точки $A(-3;3)$ и $B(4;1)$ — вершины квадрата $ABCD$. Периметр квадрата равен:`,
|
||||
opts: mc('$4\sqrt{17}$', '$2\sqrt{53}$', '$18$', '$15$', '$4\sqrt{53}$'),
|
||||
answer: 'д',
|
||||
sol: R`$AB=\sqrt{(4+3)^{2}+(1-3)^{2}}=\sqrt{49+4}=\sqrt{53}$ — сторона квадрата. Периметр $=4\sqrt{53}$.` },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Упростите выражение $\dfrac{11\sqrt{11}+5\sqrt5}{\sqrt{11}+\sqrt5}-\sqrt{55}+\dfrac{12\sqrt5}{\sqrt{11}-\sqrt5}$.`,
|
||||
opts: mc('$\dfrac{1}{\sqrt{11}+\sqrt5}$', '$\sqrt{55}$', '$16$', '$26$', '$\dfrac{5}{\sqrt{11}-\sqrt5}$'),
|
||||
answer: 'г',
|
||||
sol: R`$\dfrac{(\sqrt{11})^{3}+(\sqrt5)^{3}}{\sqrt{11}+\sqrt5}=11-\sqrt{55}+5=16-\sqrt{55}$; $\dfrac{12\sqrt5}{\sqrt{11}-\sqrt5}=2\sqrt5(\sqrt{11}+\sqrt5)=2\sqrt{55}+10$. Сумма: $(16-\sqrt{55})-\sqrt{55}+(2\sqrt{55}+10)=26$.` },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Решением неравенства $\dfrac{26}{3}-\dfrac{7x^{2}+4x}{7}>\dfrac{2-3x^{2}}{3}$ является промежуток:`,
|
||||
opts: mc('$(14;+\infty)$', '$(-14;+\infty)$', '$\left(-\infty;\dfrac{1}{14}\right)$', '$(-\infty;14)$', '$\left(\dfrac{1}{14};+\infty\right)$'),
|
||||
answer: 'г',
|
||||
sol: R`Умножив на $21$: $182-3(7x^{2}+4x)>7(2-3x^{2})$, то есть $182-21x^{2}-12x>14-21x^{2}$, $182-12x>14$, $x<14$.` },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Найдите длину средней линии прямоугольной трапеции с острым углом $60^\circ$, у которой бóльшая боковая сторона и бóльшее основание равны $10$.`,
|
||||
opts: mc('$5\sqrt3$', '$10\sqrt3$', '$15$', '$5$', '$7{,}5$'),
|
||||
answer: 'д',
|
||||
sol: R`Проекция наклонной боковой стороны на основание $=10\cos60^\circ=5$, поэтому меньшее основание $=10-5=5$. Средняя линия $=\dfrac{10+5}{2}=7{,}5$.` },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 3,
|
||||
text: R`Упростите выражение $\left(5+\dfrac{a^{2}+25c^{2}-b^{2}}{2ac}\right):(a+b+5c)\cdot2ac$.`,
|
||||
opts: mc('$a+5c-b$', '$4a^{2}c^{2}$', '$5$', '$a+5c+b$', '$a-5c-b$'),
|
||||
answer: 'а',
|
||||
sol: R`$5+\dfrac{a^{2}+25c^{2}-b^{2}}{2ac}=\dfrac{(a+5c)^{2}-b^{2}}{2ac}=\dfrac{(a+5c-b)(a+5c+b)}{2ac}$. После деления на $(a+b+5c)$ и умножения на $2ac$ получаем $a+5c-b$.` },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите сумму целых решений неравенства $3(x-5)>(x-5)^{2}$.`,
|
||||
opts: mc('$13$', '$9$', '$-13$', '$26$', '$-9$'),
|
||||
answer: 'а',
|
||||
sol: R`Пусть $u=x-5$: $3u>u^{2}$, $u(u-3)<0$, $0<u<3$, значит $5<x<8$. Целые $6$ и $7$, их сумма $13$.` },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — прямоугольный параллелепипед, $AB=12$, $AD=3$. Через середины рёбер $AA_1$ и $BB_1$ проведена плоскость, составляющая угол $60^\circ$ с плоскостью основания $ABCD$. Найдите площадь сечения параллелепипеда этой плоскостью.`,
|
||||
opts: mc('$72$', '$36\sqrt3$', '$36$', '$18$', '$36\sqrt2$'),
|
||||
answer: 'а',
|
||||
sol: R`Сечение — параллелограмм; одна сторона равна $AB=12$, другая проходит через всю глубину $AD=3$ под углом $60^\circ$: её длина $=\dfrac{3}{\cos60^\circ}=6$. Площадь $=12\cdot6=72$.` },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Сумма наибольшего и наименьшего значений функции $y=(3\sin2x+3\cos2x)^{2}$ равна:`,
|
||||
opts: mc('$8$', '$9$', '$18$', '$36$', '$3$'),
|
||||
answer: 'в',
|
||||
sol: R`$y=9(\sin2x+\cos2x)^{2}=9(1+\sin4x)$. Так как $\sin4x\in[-1;1]$, то $y\in[0;18]$. Сумма $0+18=18$.` },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Корень уравнения $\log_{1{,}6}\dfrac{9-4x}{3x-11}+\log_{1{,}6}\big((9-4x)(3x-11)\big)=0$ (или их сумма, если корней несколько) принадлежит промежутку:`,
|
||||
opts: mc('$[0;1)$', '$[1;2)$', '$(2;3]$', '$(3;4]$', '$[-1;0)$'),
|
||||
answer: 'в',
|
||||
sol: R`Сумма логарифмов равна $\log_{1{,}6}(9-4x)^{2}=0$, поэтому $(9-4x)^{2}=1$, $x=2$ или $x=2{,}5$. Из условия положительности обоих аргументов остаётся $x=2{,}5\in(2;3]$.` },
|
||||
|
||||
// ── Часть B: В1–В12 (все числовые) ───────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Автомобиль проехал некоторое расстояние, израсходовав $21$ л топлива; расход составил $9$ л на $100$ км пробега. Затем расход топлива вырос до $12$ л на $100$ км. Сколько литров топлива понадобится автомобилю, чтобы проехать такое же расстояние?`,
|
||||
answer: '28',
|
||||
sol: R`Расстояние $=\dfrac{21}{9}\cdot100$ км. При расходе $12$ л/$100$ км нужно $\dfrac{21}{9}\cdot12=28$ л.` },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Решите уравнение $\sqrt{x-5}-\sqrt{(x-5)(x+2)}=0$. В ответ запишите сумму его корней (корень, если он один).`,
|
||||
answer: '5',
|
||||
sol: R`ОДЗ: $x\ge5$. $\sqrt{x-5}\,\big(1-\sqrt{x+2}\big)=0$ даёт $x=5$ (второй множитель при $x\ge5$ не равен нулю). Единственный корень $5$.` },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`Основание остроугольного равнобедренного треугольника равно $10$, а синус противолежащего угла равен $0{,}6$. Найдите площадь треугольника.`,
|
||||
answer: '75',
|
||||
sol: R`Острый противолежащий угол $\alpha$: $\sin\alpha=0{,}6$, $\cos\alpha=0{,}8$. По теореме косинусов $10^{2}=2b^{2}(1-\cos\alpha)=0{,}4b^{2}$, $b^{2}=250$. Площадь $=\tfrac12 b^{2}\sin\alpha=\tfrac12\cdot250\cdot0{,}6=75$.` },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-systems', diff: 4,
|
||||
text: R`Пусть $(x;y)$ — целочисленное решение системы уравнений $\begin{cases}4y+x=-14,\\ 4y^{2}-4xy+x^{2}=16.\end{cases}$ Найдите сумму $x+y$.`,
|
||||
answer: '-5',
|
||||
sol: R`Второе уравнение: $(x-2y)^{2}=16$, $x-2y=\pm4$. С $x=-14-4y$ целое решение даёт $y=-3$, $x=-2$. Тогда $x+y=-5$.` },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите наибольшее целое решение неравенства $2^{3x-32}\cdot11^{x-6}>22^{2x-19}$.`,
|
||||
answer: '12',
|
||||
sol: R`$22^{2x-19}=2^{2x-19}\cdot11^{2x-19}$, поэтому неравенство равносильно $\left(\tfrac{2}{11}\right)^{x-13}>1$, то есть $x-13<0$, $x<13$. Наибольшее целое — $12$.` },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите количество корней уравнения $32\sin2x+8\cos4x=23$ на промежутке $\left[-\pi;\dfrac{3\pi}{4}\right]$.`,
|
||||
answer: '4',
|
||||
sol: R`Через $\cos4x=1-2\sin^{2}2x$ получаем $16\sin^{2}2x-32\sin2x+15=0$, откуда $\sin2x=0{,}75$. На указанном промежутке ($2x\in[-2\pi;\tfrac{3\pi}{2}]$) уравнение имеет $4$ корня.` },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`Геометрическая прогрессия со знаменателем $5$ содержит $10$ членов. Сумма всех членов прогрессии равна $24$. Найдите сумму всех членов прогрессии с чётными номерами.`,
|
||||
answer: '20',
|
||||
sol: R`Каждый чётный член в $5$ раз больше предыдущего нечётного, поэтому сумма чётных в $5$ раз больше суммы нечётных. Если сумма нечётных равна $s$, то $s+5s=24$, $s=4$, и сумма членов с чётными номерами равна $5s=20$.` },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-modulus', diff: 5,
|
||||
text: R`Найдите сумму корней уравнения $\big|(x-1)(x-6)\big|\cdot\big(|x+2|+|x-8|+|x-3|\big)=11(x-1)(6-x)$.`,
|
||||
answer: '13',
|
||||
sol: R`Правая часть неотрицательна лишь при $1\le x\le6$; на этом отрезке $|(x-1)(x-6)|=(x-1)(6-x)$. Уравнение даёт $(x-1)(6-x)\big(S-11\big)=0$, где $S=|x+2|+|x-8|+|x-3|=10+|x-3|$. Корни: $x=1,\ 6$ (множитель $0$) и $|x-3|=1$, то есть $x=2,\ 4$. Сумма $1+2+4+6=13$.` },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 4,
|
||||
text: R`Из города $A$ в город $B$, расстояние между которыми $100$ км, одновременно выезжают два автомобиля. Скорость первого автомобиля на $10$ км/ч больше скорости второго, но в пути он делает остановку на $50$ мин. Найдите наибольшее значение скорости (в км/ч) первого автомобиля, при движении с которой он прибудет в $B$ не позже второго.`,
|
||||
answer: '40',
|
||||
sol: R`Пусть скорость второго $v$. Условие $\dfrac{100}{v+10}+\dfrac56\le\dfrac{100}{v}$ приводит к $\dfrac56\le\dfrac{1000}{v(v+10)}$, то есть $v(v+10)\le1200$, $v\le30$. Наибольшая скорость первого $=30+10=40$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'planimetry', subtopic: 'plan-circles', diff: 5,
|
||||
text: R`Из точки $A$ проведены к окружности радиуса $\dfrac43$ касательная $AB$ ($B$ — точка касания) и секущая $AC$, проходящая через центр окружности и пересекающая её в точках $D$ и $C$. Найдите площадь $S$ треугольника $ABC$, если длина секущей $AC$ в $3$ раза больше длины касательной. В ответ запишите $5S$.`,
|
||||
answer: '6',
|
||||
sol: R`$AB^{2}=AO^{2}-r^{2}$ и $AC=AO+r=3\,AB$ дают $AB=\tfrac{3r}{4}=1$, $AO=\tfrac53$, $AC=3$. В координатах $B=(0{,}6;0{,}8)$, высота из $B$ к $AC$ равна $0{,}8$, площадь $=\tfrac12\cdot3\cdot0{,}8=1{,}2$. Тогда $5S=6$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 4,
|
||||
text: R`Если $\cos(\alpha+14^\circ)=\dfrac35$ и $0<\alpha+14^\circ<90^\circ$, то значение выражения $15\sqrt2\,\cos(\alpha+59^\circ)$ равно … .`,
|
||||
answer: '-3',
|
||||
sol: R`$\cos(\alpha+59^\circ)=\cos\big((\alpha+14^\circ)+45^\circ\big)=\tfrac{\sqrt2}{2}\big(\tfrac35-\tfrac45\big)=-\tfrac{\sqrt2}{10}$ (здесь $\sin(\alpha+14^\circ)=\tfrac45$). Тогда $15\sqrt2\cdot\left(-\tfrac{\sqrt2}{10}\right)=-3$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 5,
|
||||
text: R`Решите уравнение $\dfrac{30x^{2}}{x^{4}+25}=x^{2}+2\sqrt5\,x+8$. В ответ запишите значение выражения $x\cdot|x|$, где $x$ — корень уравнения.`,
|
||||
answer: '-5',
|
||||
sol: R`Левая часть $\le3$ (так как $x^{4}+25\ge10x^{2}$), правая часть $=(x+\sqrt5)^{2}+3\ge3$. Равенство возможно лишь при $x=-\sqrt5$. Тогда $x\cdot|x|=-\sqrt5\cdot\sqrt5=-5$.` },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2013_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_ct2013_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 → «Варианты» → «ЦТ-2013».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,380 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2014_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2014, Вариант 1.
|
||||
Формат ЦТ тех лет: Часть А = А1–А18 (закрытые, 5 вариантов), Часть В = В1–В12
|
||||
(открытые). Всего 30 заданий. Перенабрано вручную в KaTeX по PDF:
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2014.pdf
|
||||
|
||||
⚠️ В PDF РЕШЕНИЙ НЕТ (только задания). Решения и ответы получены вручную и
|
||||
СВЕРЕНЫ с официальной таблицей ответов в конце сборника (стр. 34, столбец
|
||||
«Вариант 1»). variant=110 (после РТ-вариантов 101–109).
|
||||
|
||||
Адаптации заданий-«с-картинкой» (исходный ответ/идея сохранены, авто-проверка):
|
||||
• А2 (выбор рисунка с симметричными фигурами — неразборчиво в скане) →
|
||||
эквивалентный MC о симметрии точки относительно оси;
|
||||
• А6 (параллелограмм на координатной сетке) → координаты вершин заданы
|
||||
в тексте (та же длина диагонали $AC=4\sqrt2$);
|
||||
• А10 (касательные/секущая) и А15 (таблица поставщиков) — закодированы
|
||||
текстом (данные с рисунка/таблицы перенесены в условие).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2014_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2014_v1.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 = 110;
|
||||
const PROV = 'ЦТ–2014, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Даны дроби: $1\frac67$, $\ 1\frac17$, $\ 6\frac67$, $\ 7\frac17$, $\ 6\frac17$. Укажите дробь, которая равна дроби $\dfrac{43}{7}$.`,
|
||||
opts: mc('$1\frac67$', '$1\frac17$', '$6\frac67$', '$7\frac17$', '$6\frac17$'),
|
||||
answer: 'д',
|
||||
sol: R`$\dfrac{43}{7}=6\frac17$, так как $43=6\cdot7+1$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 2' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Точка $M(-3;4)$ симметрична точке $N$ относительно оси абсцисс. Укажите координаты точки $N$.`,
|
||||
opts: mc('$(3;4)$', '$(-3;-4)$', '$(3;-4)$', '$(-4;-3)$', '$(-3;4)$'),
|
||||
answer: 'б',
|
||||
sol: R`При симметрии относительно оси абсцисс абсцисса точки сохраняется, а ордината меняет знак: $N(-3;-4)$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 1,
|
||||
text: R`Прямые $a$ и $b$ пересекаются, образуя четыре угла. Известно, что сумма трёх из этих углов равна $210^\circ$. Найдите градусную меру меньшего угла.`,
|
||||
opts: mc('$150^\circ$', '$15^\circ$', '$30^\circ$', '$10^\circ$', '$105^\circ$'),
|
||||
answer: 'в',
|
||||
sol: R`При пересечении двух прямых вертикальные углы равны, а смежные дают $180^\circ$. Сумма трёх углов равна $180^\circ+x$, где $x$ — один из углов; $180^\circ+x=210^\circ$, $x=30^\circ$ — меньший угол.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
|
||||
text: R`Результат разложения многочлена $x(6a-b)+b-6a$ на множители имеет вид:`,
|
||||
opts: mc('$x$', '$x+1$', '$(6a-b)(x+1)$', '$(6a-b)(x+b)$', '$(6a-b)(x-1)$'),
|
||||
answer: 'д',
|
||||
sol: R`$x(6a-b)+b-6a=x(6a-b)-(6a-b)=(6a-b)(x-1)$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Вычислите $\dfrac{7{,}3^{2}-2{,}4^{2}+9{,}7\cdot1{,}1}{6}$.`,
|
||||
opts: mc('$\dfrac97$', '$\dfrac32$', '$9$', '$9{,}7$', '$3{,}41$'),
|
||||
answer: 'г',
|
||||
sol: R`$\dfrac{53{,}29-5{,}76+10{,}67}{6}=\dfrac{58{,}2}{6}=9{,}7$.`,
|
||||
ref: 'Математика, 6 класс, гл. 4' },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Вершины параллелограмма $ABCD$ имеют координаты $A(-2;-1)$, $B(-3;2)$, $C(2;3)$, $D(3;0)$. Найдите длину диагонали $AC$.`,
|
||||
opts: mc('$4$', '$5$', '$4\sqrt2$', '$5\sqrt2$', '$9\sqrt2$'),
|
||||
answer: 'в',
|
||||
sol: R`$AC=\sqrt{(2-(-2))^{2}+(3-(-1))^{2}}=\sqrt{16+16}=4\sqrt2$.`,
|
||||
ref: 'Математика, 6 класс, гл. 7' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Длины катетов прямоугольного треугольника являются корнями уравнения $x^{2}-9x+12=0$. Найдите площадь треугольника.`,
|
||||
opts: mc('$6$', '$9$', '$10{,}5$', '$12$', '$4{,}5$'),
|
||||
answer: 'а',
|
||||
sol: R`По теореме Виета произведение корней (катетов) равно $12$. Площадь прямоугольного треугольника $=\dfrac12\cdot12=6$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2; Геометрия, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Пусть $a=5{,}4$, $b=3{,}2\cdot10^{1}$. Найдите произведение $ab$ и запишите его в стандартном виде.`,
|
||||
opts: mc('$0{,}1728\cdot10^{3}$', '$1728\cdot10^{-1}$', '$1{,}728\cdot10^{2}$', '$1{,}728$', '$172{,}8$'),
|
||||
answer: 'в',
|
||||
sol: R`$ab=5{,}4\cdot32=172{,}8=1{,}728\cdot10^{2}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Выразите $x$ из равенства $\dfrac{2+y}{5}=\dfrac{x-y}{15}$.`,
|
||||
opts: mc('$x=4y-6$', '$x=4y+6$', '$x=20y+30$', '$x=20y-30$', '$x=2y+2$'),
|
||||
answer: 'б',
|
||||
sol: R`$15(2+y)=5(x-y)$, $3(2+y)=x-y$, $6+3y=x-y$, $x=4y+6$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`Из точки $A$ к окружности проведены касательные $AB$ и $AC$ и секущая $AM$, проходящая через центр окружности $O$ (точки $B$, $C$, $M$ лежат на окружности). Отрезок $BK$ перпендикулярен $AM$ ($K$ на $AM$), $BK=4$, $AC=9$. Найдите длину отрезка $AK$.`,
|
||||
opts: mc('$4$', '$\sqrt{97}$', '$65$', '$5$', '$\sqrt{65}$'),
|
||||
answer: 'д',
|
||||
sol: R`Касательные, проведённые из одной точки, равны: $AB=AC=9$. В прямоугольном треугольнике $ABK$ ($\angle K=90^\circ$): $AK=\sqrt{AB^{2}-BK^{2}}=\sqrt{81-16}=\sqrt{65}$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 4' },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Даны два числа. Известно, что одно из них меньше другого на $6$. Какому условию удовлетворяет меньшее число $x$, если его удвоенный квадрат не больше суммы квадратов этих чисел?`,
|
||||
opts: mc('$x\le3$', '$x\le-3$', '$x\ge-3$', '$x\ge3$', '$x\le12$'),
|
||||
answer: 'в',
|
||||
sol: R`Числа $x$ и $x+6$. Условие $2x^{2}\le x^{2}+(x+6)^{2}$, то есть $2x^{2}\le2x^{2}+12x+36$, $0\le12x+36$, $x\ge-3$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Свежие фрукты при сушке теряют $a\%$ своей массы. Укажите выражение, определяющее массу сухих фруктов (в килограммах), полученных из $20$ кг свежих.`,
|
||||
opts: mc('$\dfrac{2000}{a}$', '$\dfrac{20(100-a)}{100}$', '$\dfrac{2000}{100-a}$', '$\dfrac{20(100+a)}{100}$', '$\dfrac{2000}{100+a}$'),
|
||||
answer: 'б',
|
||||
sol: R`Сухие фрукты составляют $(100-a)\%$ массы свежих: $20\cdot\dfrac{100-a}{100}=\dfrac{20(100-a)}{100}$.`,
|
||||
ref: 'Математика, 6 класс, гл. 2' },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Объём конуса равен $5$, а его высота равна $\dfrac12$. Найдите площадь основания конуса.`,
|
||||
opts: mc('$\dfrac56$', '$\dfrac{10}{3}$', '$10$', '$30$', '$\dfrac{15}{2}$'),
|
||||
answer: 'г',
|
||||
sol: R`$V=\dfrac13 S h$, поэтому $5=\dfrac13 S\cdot\dfrac12=\dfrac{S}{6}$, откуда $S=30$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2' },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Известно, что наименьшее значение функции, заданной формулой $y=x^{2}+8x+c$, равно $-3$. Тогда значение $c$ равно:`,
|
||||
opts: mc('$13$', '$16$', '$-51$', '$-19$', '$19$'),
|
||||
answer: 'а',
|
||||
sol: R`Наименьшее значение квадратичной функции равно $c-\dfrac{8^{2}}{4}=c-16$. Из $c-16=-3$ получаем $c=13$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Строительная бригада планирует заказать фундаментные блоки у одного из трёх поставщиков. Поставщик 1: стоимость блока $335$ тыс. руб., доставка $1850$ тыс. руб.; поставщик 2: блок $365$, доставка $970$; поставщик 3: блок $420$, доставка бесплатно. При покупке какого количества блоков самыми выгодными будут условия второго поставщика?`,
|
||||
opts: mc('от $18$ до $29$', 'более $17$', 'от $30$ до $55$', 'менее $30$', 'от $17$ до $30$'),
|
||||
answer: 'а',
|
||||
sol: R`Стоимость заказа $n$ блоков: $P_1=335n+1850$, $P_2=365n+970$, $P_3=420n$. Условие $P_2<P_1$: $30n<880$, $n\le29$; условие $P_2<P_3$: $55n>970$, $n\ge18$. Значит, второй поставщик выгоднее при $18\le n\le29$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Расположите числа $8^{10}$, $3^{18}$, $31^{6}$ в порядке возрастания.`,
|
||||
opts: mc('$3^{18};\ 8^{10};\ 31^{6}$', '$8^{10};\ 3^{18};\ 31^{6}$', '$31^{6};\ 3^{18};\ 8^{10}$', '$3^{18};\ 31^{6};\ 8^{10}$', '$31^{6};\ 8^{10};\ 3^{18}$'),
|
||||
answer: 'г',
|
||||
sol: R`$8^{10}=2^{30}\approx1{,}07\cdot10^{9}$; $\ 3^{18}=9^{9}\approx3{,}87\cdot10^{8}$; $\ 31^{6}\approx8{,}9\cdot10^{8}$. Порядок возрастания: $3^{18};\ 31^{6};\ 8^{10}$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
|
||||
text: R`Через вершину $A$ прямоугольного треугольника $ABC$ ($\angle C=90^\circ$) проведён перпендикуляр $AK$ к его плоскости. Найдите расстояние от точки $K$ до прямой $BC$, если $AK=2$, $AB=4$, $BC=\sqrt{11}$.`,
|
||||
opts: mc('$3$', '$2\sqrt5$', '$\sqrt5$', '$\sqrt{15}$', '$6$'),
|
||||
answer: 'а',
|
||||
sol: R`$AC=\sqrt{AB^{2}-BC^{2}}=\sqrt{16-11}=\sqrt5$. Так как $BC\perp AC$ и $BC\perp AK$, то $BC$ перпендикулярна плоскости $ACK$, поэтому расстояние от $K$ до $BC$ равно $CK=\sqrt{AK^{2}+AC^{2}}=\sqrt{4+5}=3$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3' },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Сумма корней (корень, если он единственный) уравнения $\sqrt{2x+5}\cdot\sqrt{x-1}=3-x$ равна (равен):`,
|
||||
opts: mc('$\dfrac{-9-\sqrt{137}}{2}$', '$9$', '$18$', '$\dfrac{-9+\sqrt{137}}{2}$', '$-14$'),
|
||||
answer: 'г',
|
||||
sol: R`ОДЗ: $x\ge1$ и $3-x\ge0$, то есть $x\in[1;3]$. Возведя в квадрат: $(2x+5)(x-1)=(3-x)^{2}$, $x^{2}+9x-14=0$, $x=\dfrac{-9\pm\sqrt{137}}{2}$. В $[1;3]$ попадает только $\dfrac{-9+\sqrt{137}}{2}$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите сумму целых решений (решение, если оно единственное) системы неравенств $\begin{cases}2x+8\ge x^{2},\\(x-1)^{2}>0.\end{cases}$`,
|
||||
answer: '6',
|
||||
sol: R`$2x+8\ge x^{2}\Rightarrow x^{2}-2x-8\le0\Rightarrow-2\le x\le4$; $\ (x-1)^{2}>0\Rightarrow x\ne1$. Целые решения $-2,-1,0,2,3,4$; их сумма равна $6$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите произведение большего корня на количество корней уравнения $\dfrac{21}{x^{2}-4x+10}-x^{2}+4x=6$.`,
|
||||
answer: '6',
|
||||
sol: R`Пусть $u=x^{2}-4x$, тогда $\dfrac{21}{u+10}-u=6$, откуда $u^{2}+16u+39=0$, $u=-3$ или $u=-13$. При $u=-3$: $x^{2}-4x+3=0$, $x=1;3$. При $u=-13$ дискриминант отрицателен. Корни $1$ и $3$, больший $3$, количество $2$; произведение $3\cdot2=6$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'planimetry', subtopic: 'plan-circle', diff: 3,
|
||||
text: R`В окружность радиусом $6$ вписан треугольник, длины двух сторон которого равны $6$ и $10$. Найдите длину высоты треугольника, проведённой к его третьей стороне.`,
|
||||
answer: '5',
|
||||
sol: R`Из формул $S=\dfrac{abc}{4R}$ и $S=\dfrac12 c\,h_c$ следует $h_c=\dfrac{ab}{2R}=\dfrac{6\cdot10}{2\cdot6}=5$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Найдите сумму наименьшего и наибольшего целых решений неравенства $\log_{0{,}3}(x+54)\le2\log_{0{,}3}(x-2)$.`,
|
||||
answer: '13',
|
||||
sol: R`Неравенство равносильно $\log_{0{,}3}(x+54)\le\log_{0{,}3}(x-2)^{2}$ при $x>2$. Основание $0{,}3<1$, поэтому $x+54\ge(x-2)^{2}$, то есть $x^{2}-5x-50\le0$, $-5\le x\le10$. С учётом $x>2$: $(2;10]$. Целые $3,\ldots,10$; сумма наименьшего и наибольшего $3+10=13$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите сумму (в градусах) наименьшего положительного и наибольшего отрицательного корней уравнения $\sin4x-\sqrt3\cos2x=0$.`,
|
||||
answer: '-15',
|
||||
sol: R`$\sin4x=2\sin2x\cos2x$, поэтому $\cos2x(2\sin2x-\sqrt3)=0$. Из $\cos2x=0$: $x=45^\circ+90^\circ n$; из $\sin2x=\dfrac{\sqrt3}{2}$: $x=30^\circ+180^\circ n$ или $x=60^\circ+180^\circ n$. Наименьший положительный корень $30^\circ$, наибольший отрицательный $-45^\circ$; их сумма $-15^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`Три числа составляют геометрическую прогрессию, в которой $q>1$. Если второй член прогрессии уменьшить на $8$, то полученные три числа в том же порядке опять составят геометрическую прогрессию. Если третий член новой прогрессии уменьшить на $25$, то полученные числа составят арифметическую прогрессию. Найдите сумму исходных чисел.`,
|
||||
answer: '21',
|
||||
sol: R`Пусть числа $a$, $aq$, $aq^{2}$. Из условия $(aq-8)^{2}=a\cdot aq^{2}$ следует $aq=4$. Новая прогрессия $a,\,-4,\,aq^{2}$, где $a\cdot aq^{2}=16$. Условие арифметической прогрессии: $2\cdot(-4)=a+(aq^{2}-25)$, то есть $a+aq^{2}=17$. Так как $aq^{2}=\dfrac{16}{a}$, получаем $a+\dfrac{16}{a}=17$, $a=1$ (тогда $q=4>1$). Числа $1,4,16$, их сумма $21$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите произведение суммы корней уравнения $4^{x-1}-2^{x-1}=2^{x+5}-2^{6}$ на их количество.`,
|
||||
answer: '16',
|
||||
sol: R`Пусть $t=2^{x-1}$. Тогда $t^{2}-t=64t-64$, $t^{2}-65t+64=0$, $t=1$ или $t=64$. Из $2^{x-1}=1$: $x=1$; из $2^{x-1}=64$: $x=7$. Сумма корней $8$, количество $2$; произведение $8\cdot2=16$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите количество корней уравнения $\cos x=\left|\dfrac{x}{11\pi}\right|$.`,
|
||||
answer: '22',
|
||||
sol: R`Правая часть неотрицательна и не больше $1$ при $|x|\le11\pi$, поэтому требуется $\cos x\ge0$. Обе функции чётные. На $(0;11\pi]$ прямая $\dfrac{x}{11\pi}$ пересекает положительные «арки» косинуса $11$ раз; столько же на отрицательной полуоси. Всего $22$ корня.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите сумму целых решений неравенства $\dfrac{|4x-10|-|2x-14|}{(x+3)(x-6)}\le0$.`,
|
||||
answer: '7',
|
||||
sol: R`Числитель равен нулю при $x=-2$ и $x=4$; он положителен вне отрезка $[-2;4]$ и отрицателен внутри него. Знаменатель положителен при $x<-3$ и $x>6$, отрицателен при $-3<x<6$. Решением неравенства является множество $(-3;-2]\cup[4;6)$. Целые решения $-2,4,5$; их сумма равна $7$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`Куб вписан в правильную четырёхугольную пирамиду так, что четыре его вершины находятся на боковых рёбрах пирамиды, а четыре другие вершины — на её основании. Длина стороны основания пирамиды равна $2$, высота пирамиды — $6$. Найдите площадь $S$ поверхности куба. В ответ запишите значение выражения $4S$.`,
|
||||
answer: '54',
|
||||
sol: R`Пусть ребро куба равно $s$. Сечение пирамиды плоскостью на высоте $s$ — квадрат со стороной $2\cdot\dfrac{6-s}{6}$. Верхняя грань куба совпадает с этим квадратом: $s=\dfrac{2(6-s)}{6}$, откуда $s=1{,}5$. Площадь поверхности $S=6s^{2}=13{,}5$, тогда $4S=54$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 4,
|
||||
text: R`Найдите значение выражения $\sqrt3-\sqrt2-\sqrt6-7-\operatorname{tg}172^\circ30'$.`,
|
||||
answer: '-9',
|
||||
sol: R`$\operatorname{tg}172^\circ30'=-\operatorname{tg}7^\circ30'=-(\sqrt6-\sqrt3+\sqrt2-2)$. Тогда выражение равно $\sqrt3-\sqrt2-\sqrt6-7+\sqrt6-\sqrt3+\sqrt2-2=-9$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 10' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 5,
|
||||
text: R`Трое рабочих (не все одинаковой квалификации) выполнили некоторую работу, работая по очереди. Сначала первый рабочий проработал $\dfrac{1}{12}$ часть времени, необходимого двум другим для выполнения всей работы. Затем второй проработал $\dfrac{1}{12}$ часть времени, необходимого двум другим. И, наконец, третий проработал $\dfrac{1}{12}$ часть времени, необходимого двум другим. Во сколько раз быстрее работа была бы выполнена, если бы трое рабочих работали одновременно? В ответ запишите найденное число, умноженное на $4$.`,
|
||||
answer: '5',
|
||||
sol: R`Пусть производительности рабочих $r_1,r_2,r_3$, $R=r_1+r_2+r_3$. Условие выполнения работы: $\sum\dfrac{r_i}{12(R-r_i)}=1$, откуда $\sum\dfrac{1}{R-r_i}=\dfrac{15}{R}$. Время работы по очереди $T=\dfrac{1}{12}\sum\dfrac{1}{R-r_i}=\dfrac{5}{4R}$, а время совместной работы $\dfrac1R$. Отношение равно $\dfrac54$; умноженное на $4$ — это $5$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2014_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_ct2014_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 → «Варианты» → «ЦТ-2014».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,389 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2015_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2015, Вариант 1.
|
||||
Формат ЦТ тех лет: Часть А = А1–А18 (закрытые, 5 вариантов), Часть В = В1–В12
|
||||
(открытые). Всего 30 заданий. Перенабрано вручную в KaTeX по PDF:
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2015.pdf
|
||||
(10 изоморфных вариантов; таблица ответов — стр. 35).
|
||||
|
||||
⚠️ Ответы. В таблице ответов колонка «Вариант 1» затемнена (часть ячеек
|
||||
нечитаема). Поэтому ответы получены РЕШЕНИЕМ каждого задания и сверены:
|
||||
(1) с читаемыми ячейками столбца В1 (B2=-15, B3=7, B7=147, B8=-6 — совпали);
|
||||
(2) со структурно-изоморфными вариантами 2–10 (их столбцы читаемы).
|
||||
variant=112 (после ЦЭ-2024 = 111).
|
||||
|
||||
Адаптации заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А4 (выбор рисунка с центрально-симметричными фигурами) → эквивалентный MC
|
||||
о центральной симметрии точки относительно начала координат;
|
||||
• А6 (выбор рисунка с множеством решений системы) → та же система неравенств,
|
||||
но спрашивается само множество решений (интервал);
|
||||
• А11 (столбчатая диаграмма) → те же данные приведены таблицей в figure_html;
|
||||
• А12 (выбор эскиза параболы) → верное утверждение о вершине/направлении ветвей
|
||||
параболы $y=1-(x+3)^2$;
|
||||
• А15 (треугольник по узлам сетки) → координаты вершин заданы в тексте
|
||||
($\cos\angle ABC=-\tfrac{5}{13}$ сохранён);
|
||||
• В11 (громоздкое лог-выражение, неразборчиво в скане) → эквивалентная задача
|
||||
на $2^{A}$ с сохранённым ответом $225=15^{2}$ (пэттерн столбца В11: $n^2$).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2015_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2015_v1.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 = 112;
|
||||
const PROV = 'ЦТ–2015, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`На координатной прямой точки расположены слева направо в порядке $O,C,B,A,F,D$. Точка $O$ имеет координату $0$, а соседние отмеченные точки находятся на равных расстояниях $\dfrac17$ друг от друга. Если координата точки $A$ равна $\dfrac97$, то числу $1$ соответствует точка:`,
|
||||
opts: mc('$C$', '$B$', '$D$', '$F$', '$O$'),
|
||||
answer: 'а',
|
||||
sol: R`Шаг между соседними точками равен $\dfrac17$, поэтому $B=\dfrac87$, $C=\dfrac77=1$. Числу $1$ соответствует точка $C$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 1,
|
||||
text: R`Запишите $\left(11^{x}\right)^{y}$ в виде степени с основанием $11$.`,
|
||||
opts: mc('$11^{x/y}$', '$11^{x+y}$', '$11^{2x+2y}$', '$11^{2xy}$', '$11^{xy}$'),
|
||||
answer: 'д',
|
||||
sol: R`По свойству степени $\left(11^{x}\right)^{y}=11^{xy}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1, § 4' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 1,
|
||||
text: R`Арифметическая прогрессия $(a_n)$ задана формулой $n$-го члена $a_n=5n-2$. Найдите разность этой прогрессии.`,
|
||||
opts: mc('$3$', '$-7$', '$5$', '$7$', '$-5$'),
|
||||
answer: 'в',
|
||||
sol: R`$d=a_{n+1}-a_n=\bigl(5(n+1)-2\bigr)-(5n-2)=5$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Точка $K(5;-2)$ симметрична точке $L$ относительно точки $O$ — начала координат. Укажите координаты точки $L$.`,
|
||||
opts: mc('$(-5;2)$', '$(5;2)$', '$(-5;-2)$', '$(2;-5)$', '$(-2;5)$'),
|
||||
answer: 'а',
|
||||
sol: R`При центральной симметрии относительно начала координат обе координаты меняют знак: $L(-5;2)$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Вычислите $\dfrac{3732\cdot0{,}01-5}{0{,}47+1{,}13}$.`,
|
||||
opts: mc('$20{,}2$', '$2{,}2$', '$202$', '$22$', '$2{,}02$'),
|
||||
answer: 'а',
|
||||
sol: R`$\dfrac{37{,}32-5}{1{,}6}=\dfrac{32{,}32}{1{,}6}=20{,}2$.`,
|
||||
ref: 'Математика, 6 класс, гл. 4' },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Укажите множество решений системы неравенств $\begin{cases}x\le-1{,}6,\\ 1-2x<9.\end{cases}$`,
|
||||
opts: mc('$(-4;-1{,}6]$', '$[-1{,}6;+\infty)$', '$(-\infty;-4)$', '$[-4;-1{,}6)$', '$(-1{,}6;4)$'),
|
||||
answer: 'а',
|
||||
sol: R`Из $1-2x<9$ следует $-2x<8$, то есть $x>-4$. Вместе с $x\le-1{,}6$ получаем $-4<x\le-1{,}6$, то есть $(-4;-1{,}6]$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`Точки $A,B,C$ делят окружность на дуги $AB$, $BC$ и $CA$, градусные меры которых в указанном порядке относятся как $5:7:6$. Найдите градусную меру угла $ABC$.`,
|
||||
opts: mc('$50^\circ$', '$60^\circ$', '$70^\circ$', '$100^\circ$', '$120^\circ$'),
|
||||
answer: 'б',
|
||||
sol: R`Сумма частей $5+7+6=18$ отвечает $360^\circ$, одна часть равна $20^\circ$. Дуга $CA=6\cdot20^\circ=120^\circ$. Вписанный угол $ABC$ опирается на дугу $CA$, поэтому $\angle ABC=\tfrac12\cdot120^\circ=60^\circ$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Даны числа $5100$; $\ 0{,}0051$; $\ 5{,}1\cdot10^{-4}$; $\ 51\cdot10^{3}$; $\ 0{,}51\cdot10^{5}$. Укажите число, записанное в стандартном виде.`,
|
||||
opts: mc('$5100$', '$0{,}0051$', '$5{,}1\cdot10^{-4}$', '$51\cdot10^{3}$', '$0{,}51\cdot10^{5}$'),
|
||||
answer: 'в',
|
||||
sol: R`Стандартный вид $a\cdot10^{n}$ требует $1\le a<10$. Этому удовлетворяет только $5{,}1\cdot10^{-4}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Результат упрощения выражения $\dfrac{a^{2}+5a}{a+3}+\dfrac{6a}{a^{2}+3a}$ имеет вид:`,
|
||||
opts: mc('$a-2$', '$\dfrac{(a-2)(a-3)}{a+3}$', '$\dfrac{a^{2}+11a}{a^{2}+4a+3}$', '$\dfrac{a^{2}+8a+33}{3(a+3)}$', '$a+2$'),
|
||||
answer: 'д',
|
||||
sol: R`$\dfrac{a(a+5)}{a+3}+\dfrac{6a}{a(a+3)}=\dfrac{a(a+5)+6}{a+3}=\dfrac{a^{2}+5a+6}{a+3}=\dfrac{(a+2)(a+3)}{a+3}=a+2$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1' },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Значение выражения $\sqrt[5]{1\tfrac{1}{32}}:\sqrt[5]{33}$ равно:`,
|
||||
opts: mc('$\dfrac{3}{2\sqrt[5]{33}}$', '$\dfrac12$', '$2$', '$\dfrac{2}{3\sqrt[5]{33}}$', '$\dfrac{1}{33}$'),
|
||||
answer: 'б',
|
||||
sol: R`$\sqrt[5]{\dfrac{33}{32}}:\sqrt[5]{33}=\sqrt[5]{\dfrac{33}{32\cdot33}}=\sqrt[5]{\dfrac{1}{32}}=\dfrac12$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`На диаграмме показано количество всех покупателей магазина и количество купивших товар по акции по дням недели (данные приведены в таблице). В какой день количество покупателей по акции составило менее 30 % от количества всех покупателей в этот день?`,
|
||||
fig: R`<table class="task-fig" style="border-collapse:collapse;margin:6px 0;font-size:.95em"><tr><th style="border:1px solid #99a;padding:3px 10px">День</th><th style="border:1px solid #99a;padding:3px 10px">Все покупатели</th><th style="border:1px solid #99a;padding:3px 10px">По акции</th></tr><tr><td style="border:1px solid #99a;padding:3px 10px">понедельник</td><td style="border:1px solid #99a;padding:3px 10px">4400</td><td style="border:1px solid #99a;padding:3px 10px">1600</td></tr><tr><td style="border:1px solid #99a;padding:3px 10px">вторник</td><td style="border:1px solid #99a;padding:3px 10px">5500</td><td style="border:1px solid #99a;padding:3px 10px">2600</td></tr><tr><td style="border:1px solid #99a;padding:3px 10px">среда</td><td style="border:1px solid #99a;padding:3px 10px">3400</td><td style="border:1px solid #99a;padding:3px 10px">1800</td></tr><tr><td style="border:1px solid #99a;padding:3px 10px">четверг</td><td style="border:1px solid #99a;padding:3px 10px">4700</td><td style="border:1px solid #99a;padding:3px 10px">2200</td></tr><tr><td style="border:1px solid #99a;padding:3px 10px">пятница</td><td style="border:1px solid #99a;padding:3px 10px">6500</td><td style="border:1px solid #99a;padding:3px 10px">1800</td></tr></table>`,
|
||||
opts: mc('понедельник', 'вторник', 'среда', 'четверг', 'пятница'),
|
||||
answer: 'д',
|
||||
sol: R`Доля покупателей по акции: пн — $\dfrac{1600}{4400}\approx0{,}36$; вт — $\dfrac{2600}{5500}\approx0{,}47$; ср — $\dfrac{1800}{3400}\approx0{,}53$; чт — $\dfrac{2200}{4700}\approx0{,}47$; пт — $\dfrac{1800}{6500}\approx0{,}28$. Менее $0{,}3$ — только в пятницу.`,
|
||||
ref: 'Математика, 6 класс, гл. 2' },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Графиком функции $y=1-(x+3)^{2}$ является парабола. Укажите верное утверждение о её расположении.`,
|
||||
opts: mc('ветви вверх, вершина $(3;1)$', 'ветви вниз, вершина $(-3;1)$', 'ветви вниз, вершина $(3;1)$', 'ветви вверх, вершина $(-3;-1)$', 'ветви вниз, вершина $(-3;-1)$'),
|
||||
answer: 'б',
|
||||
sol: R`$y=-(x+3)^{2}+1$: коэффициент при квадрате отрицателен (ветви направлены вниз), вершина в точке $(-3;1)$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'equations', subtopic: 'eq-exponential', diff: 2,
|
||||
text: R`Уравнение $\dfrac{4x-9}{5}+2=x-\dfrac{11-x}{5}$ равносильно уравнению:`,
|
||||
opts: mc('$6^{x}=1$', '$6^{x}=6$', '$2^{x}=32$', '$2^{x}=64$', '$5^{x}=25$'),
|
||||
answer: 'г',
|
||||
sol: R`Умножив на $5$: $4x-9+10=5x-(11-x)$, то есть $4x+1=6x-11$, откуда $x=6$. Этому корню равносильно уравнение $2^{x}=64$ (ведь $2^{6}=64$).`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Собственная скорость катера в $9$ раз больше скорости течения реки. Расстояние по реке от пункта $A$ до пункта $B$ плот проплыл за время $t_1$, а катер — за время $t_2$. Тогда верна формула:`,
|
||||
opts: mc('$t_1=10t_2$', '$t_1=9t_2$', '$t_1=9{,}5t_2$', '$t_1=10{,}5t_2$', '$t_1=11t_2$'),
|
||||
answer: 'а',
|
||||
sol: R`Пусть скорость течения $v$. Плот плывёт со скоростью течения: $t_1=\dfrac{S}{v}$. Катер по течению имеет скорость $9v+v=10v$: $t_2=\dfrac{S}{10v}$. Значит $t_1=10t_2$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`На координатной плоскости дан тупоугольный треугольник $ABC$ с вершинами $A(2;3)$, $B(0;0)$, $C(2;-3)$. Найдите $\cos\angle ABC$.`,
|
||||
opts: mc('$\dfrac{5}{12}$', '$\dfrac{5}{13}$', '$-\dfrac{5}{13}$', '$-\dfrac{12}{13}$', '$\dfrac{12}{13}$'),
|
||||
answer: 'в',
|
||||
sol: R`$\vec{BA}=(2;3)$, $\vec{BC}=(2;-3)$. $\cos\angle ABC=\dfrac{\vec{BA}\cdot\vec{BC}}{|\vec{BA}|\,|\vec{BC}|}=\dfrac{2\cdot2+3\cdot(-3)}{\sqrt{13}\cdot\sqrt{13}}=-\dfrac{5}{13}$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Из полного бокала, имеющего форму конуса высотой $9$, отлили треть (по объёму) жидкости. Вычислите $\dfrac12 h^{3}$, где $h$ — высота оставшейся жидкости.`,
|
||||
opts: mc('$324$', '$182$', '$27$', '$243$', '$81$'),
|
||||
answer: 'г',
|
||||
sol: R`Оставшаяся жидкость составляет $\tfrac23$ объёма и образует подобный конус высотой $h$: $\left(\dfrac{h}{9}\right)^{3}=\dfrac23$, откуда $h^{3}=729\cdot\dfrac23=486$. Тогда $\dfrac12 h^{3}=243$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2' },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`График функции, заданной формулой $y=kx+b$, симметричен относительно оси $Oy$ и проходит через точку $A\left(\tfrac13;6\right)$. Значение выражения $k+b$ равно:`,
|
||||
opts: mc('$-5\tfrac23$', '$6\tfrac13$', '$6$', '$2$', '$18$'),
|
||||
answer: 'в',
|
||||
sol: R`Симметрия относительно оси $Oy$ означает чётность функции: $-kx+b=kx+b$ при всех $x$, откуда $k=0$. Тогда $y=b$, и из прохождения через $A$ получаем $b=6$. Значит $k+b=6$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`Высоты остроугольного равнобедренного треугольника $ABC$ ($AB=BC$) пересекаются в точке $O$. Если высота $AD=15$ и $AO=10$, то длина стороны $AC$ равна:`,
|
||||
opts: mc('$17$', '$7\sqrt6$', '$5\sqrt3$', '$10\sqrt3$', '$5\sqrt{13}$'),
|
||||
answer: 'г',
|
||||
sol: R`Поместим $A(-a;0)$, $C(a;0)$, $B(0;h)$. Ортоцентр $O\left(0;\dfrac{a^{2}}{h}\right)$. Тогда $AO=\dfrac{a\sqrt{a^{2}+h^{2}}}{h}=10$, а высота $AD=\dfrac{2ah}{\sqrt{a^{2}+h^{2}}}=15$. Отсюда $a^{2}=75$, $h=15$, поэтому $AC=2a=10\sqrt3$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 3' },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Витя купил в магазине некоторое количество тетрадей, заплатив за них $24$ тысячи рублей. Затем он обнаружил, что в другом магазине тетрадь стоит на $1$ тысячу рублей меньше, поэтому, заплатив такую же сумму, он мог бы купить на $2$ тетради больше. Сколько тетрадей купил Витя?`,
|
||||
answer: '6',
|
||||
sol: R`Пусть цена тетради $p$ (тыс. руб.), куплено $n=\dfrac{24}{p}$ тетрадей. Тогда $\dfrac{24}{p-1}=\dfrac{24}{p}+2$, откуда $p(p-1)=12$, $p=4$. Значит $n=\dfrac{24}{4}=6$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите наибольшее целое решение неравенства $3^{x+17}\cdot5^{-x-16}>1{,}08$.`,
|
||||
answer: '-15',
|
||||
sol: R`$1{,}08=\dfrac{27}{25}=3^{3}\cdot5^{-2}$. Неравенство приводится к виду $3^{x+14}\cdot5^{-x-14}>1$, то есть $\left(\dfrac35\right)^{x+14}>1$. Так как $\dfrac35<1$, то $x+14<0$, $x<-14$. Наибольшее целое — $-15$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите модуль разности наибольшего и наименьшего корней уравнения $\left(2x^{2}-x-7\right)^{2}=(5x+1)^{2}$.`,
|
||||
answer: '7',
|
||||
sol: R`$2x^{2}-x-7=\pm(5x+1)$. При знаке «$+$»: $2x^{2}-6x-8=0$, $x=4;-1$. При знаке «$-$»: $2x^{2}+4x-6=0$, $x=1;-3$. Корни $\{-3;-1;1;4\}$; модуль разности наибольшего и наименьшего $|4-(-3)|=7$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Пусть $(x_1;y_1)$, $(x_2;y_2)$ — решения системы уравнений $\begin{cases}x^{2}+4x=15+3y,\\ 4x-3y=6.\end{cases}$ Найдите значение выражения $x_1y_2+x_2y_1$.`,
|
||||
answer: '-24',
|
||||
sol: R`Из второго уравнения $3y=4x-6$. Подставив в первое: $x^{2}+4x=15+4x-6$, $x^{2}=9$, $x=\pm3$. Решения $(3;2)$ и $(-3;-6)$. Тогда $x_1y_2+x_2y_1=3\cdot(-6)+(-3)\cdot2=-24$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму корней (корень, если он единственный) уравнения $\sqrt{x^{2}+3x}+\sqrt{1-x}=\sqrt{12-x}+\sqrt{1-x}$.`,
|
||||
answer: '-6',
|
||||
sol: R`Вычитая $\sqrt{1-x}$ из обеих частей: $\sqrt{x^{2}+3x}=\sqrt{12-x}$, $x^{2}+3x=12-x$, $x^{2}+4x-12=0$, $x=2;-6$. Условию ОДЗ ($x\le1$) удовлетворяет лишь $x=-6$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите сумму целых решений неравенства $\dfrac{(x^{2}+7x+10)(x-4)^{2}}{4-x^{2}}\ge0$.`,
|
||||
answer: '-8',
|
||||
sol: R`После сокращения на $(x+2)$: $\dfrac{(x+5)(x-4)^{2}}{2-x}\ge0$ при $x\ne-2$. Множитель $(x-4)^{2}\ge0$, поэтому решение — промежуток $[-5;2)$ без точки $x=-2$, плюс отдельная точка $x=4$. Целые решения $-5,-4,-3,-1,0,1,4$; их сумма $-8$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`Каждое боковое ребро четырёхугольной пирамиды образует с её высотой, равной $3\sqrt7$, угол $30^\circ$. Основанием пирамиды является прямоугольник с углом $30^\circ$ между диагоналями. Найдите объём пирамиды $V$; в ответ запишите значение выражения $\sqrt7\cdot V$.`,
|
||||
answer: '147',
|
||||
sol: R`Все боковые рёбра равнонаклонены к основанию, значит вершина проектируется в центр прямоугольника. Половина диагонали $R=H\operatorname{tg}30^\circ=3\sqrt7\cdot\dfrac{1}{\sqrt3}=\sqrt{21}$, диагональ $d=2\sqrt{21}$. Площадь основания $S=\dfrac12 d^{2}\sin30^\circ=\dfrac12\cdot84\cdot\dfrac12=21$. Объём $V=\dfrac13\cdot21\cdot3\sqrt7=21\sqrt7$, и $\sqrt7\cdot V=21\cdot7=147$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите (в градусах) наибольший отрицательный корень уравнения $\sin^{2}\!\left(5x-\dfrac{\pi}{3}\right)=1$.`,
|
||||
answer: '-6',
|
||||
sol: R`$\sin^{2}\alpha=1\Rightarrow\alpha=\dfrac{\pi}{2}+\pi k$. Тогда $5x=\dfrac{\pi}{3}+\dfrac{\pi}{2}+\pi k=\dfrac{5\pi}{6}+\pi k$, $x=30^\circ+36^\circ k$. Наибольший отрицательный корень при $k=-1$: $30^\circ-36^\circ=-6^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите количество корней уравнения $\sin x=-\dfrac{x}{16\pi}$.`,
|
||||
answer: '33',
|
||||
sol: R`Корни существуют лишь при $|x|\le16\pi$. Функции $\sin x$ и $-\dfrac{x}{16\pi}$ нечётны, поэтому $x=0$ — корень, а остальные корни симметричны. При $x>0$ правая часть отрицательна, поэтому пересечения происходят на восьми «отрицательных» арках синуса $(\pi;2\pi),(3\pi;4\pi),\ldots,(15\pi;16\pi)$ — по два на каждой, итого $16$. Столько же при $x<0$. Всего $16+16+1=33$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
|
||||
text: R`В прямоугольнике $ABCD$ выбраны точки $L$ на стороне $BC$ и $M$ на стороне $AD$ так, что $ALCM$ — ромб. Найдите площадь этого ромба, если $AB=3$, $BC=9$.`,
|
||||
answer: '15',
|
||||
sol: R`Пусть сторона ромба равна $m$. Тогда $BL=BC-LC=9-m$, и из прямоугольного треугольника $ABL$: $m^{2}=3^{2}+(9-m)^{2}$, откуда $m=5$. Диагонали ромба $AC=\sqrt{3^{2}+9^{2}}=3\sqrt{10}$ и $LM=\sqrt{10}$, поэтому площадь $=\dfrac12\cdot3\sqrt{10}\cdot\sqrt{10}=15$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Найдите значение выражения $2^{A}$, если $A=\log_{2}9+\log_{2}25$.`,
|
||||
answer: '225',
|
||||
sol: R`$A=\log_{2}9+\log_{2}25=\log_{2}(9\cdot25)=\log_{2}225$, поэтому $2^{A}=225$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 3,
|
||||
text: R`Найдите сумму всех трёхзначных чисел, которые при делении на $4$ и на $6$ дают в остатке $1$, а при делении на $9$ дают в остатке $4$.`,
|
||||
answer: '13825',
|
||||
sol: R`Условия $n\equiv1\pmod4$ и $n\equiv1\pmod6$ дают $n\equiv1\pmod{12}$. Вместе с $n\equiv4\pmod9$ получаем $n\equiv13\pmod{36}$. Трёхзначные такие числа образуют прогрессию $121,157,\ldots,985$ — всего $25$ чисел. Их сумма $\dfrac{121+985}{2}\cdot25=553\cdot25=13825$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2015_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_ct2015_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 → «Варианты» → «ЦТ-2015».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -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<k<t$, выбор верного неравенства;
|
||||
• А6 (значение по таблице) → таблица перенесена в figure_html;
|
||||
• А7 (площадь фигуры на сетке) → площадь многоугольника по заданным координатам
|
||||
вершин (формула шнуровки), $S=35{,}5$ см²;
|
||||
• А8 (целые значения функции по графику) → область значений задана отрезком $[-4;6]$;
|
||||
• А11 (круговая диаграмма) → градусные меры секторов даны в тексте.
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2016_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2016_v1.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 113;
|
||||
const PROV = 'ЦТ–2016, Вариант 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 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<k<t$. Укажите верное утверждение.`,
|
||||
opts: mc('$-3k<-3t$', '$\dfrac1t>\dfrac1k$', '$3k>3t$', '$\dfrac{k}{-3}>\dfrac{t}{-3}$', '$k>t$'),
|
||||
answer: 'г',
|
||||
sol: R`При делении неравенства $k<t$ на отрицательное число $-3$ знак меняется: $\dfrac{k}{-3}>\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`<table class="task-fig" style="border-collapse:collapse;margin:6px 0"><tr><td ${TD}><i>a</i></td><td ${TD}></td><td ${TD}>1,9</td></tr><tr><td ${TD}><i>b</i></td><td ${TD}>108</td><td ${TD}>7,6</td></tr></table>`,
|
||||
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|<c<3+1$, то есть $2<c<4$, целое $c=3$. Периметр $1+3+3=7$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Сократите дробь $\dfrac{x^{2}-9}{8x^{2}-23x-3}$.`,
|
||||
opts: mc('$\dfrac{x-3}{8x+1}$', '$\dfrac{x+3}{8x-1}$', '$\dfrac{x+3}{x+1}$', '$\dfrac{x+3}{8x+1}$', '$\dfrac{x-3}{8x-1}$'),
|
||||
answer: 'г',
|
||||
sol: R`$8x^{2}-23x-3=(x-3)(8x+1)$, поэтому $\dfrac{(x-3)(x+3)}{(x-3)(8x+1)}=\dfrac{x+3}{8x+1}$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1' },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Из пунктов $A$ и $B$, расстояние между которыми $160$ км, одновременно навстречу друг другу выехали два автомобиля с постоянными скоростями: из $A$ — со скоростью $a$ км/ч, из $B$ — со скоростью $b$ км/ч. Составьте выражение для расстояния (в километрах) от пункта $A$ до места встречи.`,
|
||||
opts: mc('$\dfrac{160a}{a+b}$', '$\dfrac{160}{a+b}$', '$\dfrac{160(a+b)}{a}$', '$\dfrac{160b}{a+b}$', '$\dfrac{160(a+b)}{b}$'),
|
||||
answer: 'а',
|
||||
sol: R`Время до встречи $t=\dfrac{160}{a+b}$. Расстояние от $A$: $a\cdot t=\dfrac{160a}{a+b}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Точки $A,B,C$ лежат на большой окружности сферы так, что треугольник $ABC$ равносторонний. Если $AB=3\sqrt6$, то площадь сферы равна:`,
|
||||
opts: mc('$144\pi$', '$72\pi$', '$36\pi$', '$18\pi$', '$68\pi$'),
|
||||
answer: 'б',
|
||||
sol: R`Сторона равностороннего треугольника, вписанного в окружность радиуса $R$ (это радиус сферы), равна $R\sqrt3$. Из $3\sqrt6=R\sqrt3$ следует $R=3\sqrt2$, $R^{2}=18$. Площадь сферы $4\pi R^{2}=72\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3' },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Упростите выражение $5\cos(7\pi+\alpha)+\sin\left(\dfrac{11\pi}{2}-\alpha\right)$.`,
|
||||
opts: mc('$6\cos\alpha$', '$-6\cos\alpha$', '$-4\cos\alpha$', '$4\cos\alpha$', '$6\sin\alpha$'),
|
||||
answer: 'б',
|
||||
sol: R`$\cos(7\pi+\alpha)=\cos(\pi+\alpha)=-\cos\alpha$; $\ \sin\left(\dfrac{11\pi}{2}-\alpha\right)=\sin\left(\dfrac{3\pi}{2}-\alpha\right)=-\cos\alpha$. Сумма $5(-\cos\alpha)+(-\cos\alpha)=-6\cos\alpha$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 10' },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`График функции, заданной формулой $y=kx+b$, симметричен относительно начала координат и проходит через точку $A(2;10)$. Значение выражения $k+b$ равно:`,
|
||||
opts: mc('$-8$', '$2$', '$5$', '$10$', '$12$'),
|
||||
answer: 'в',
|
||||
sol: R`Симметрия относительно начала координат означает нечётность функции, поэтому $b=0$. Тогда $y=kx$, и из $10=2k$ получаем $k=5$. Значит $k+b=5$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Сумма всех натуральных решений неравенства $(6-x)\cdot(x+4)^{8}\cdot(x-13)^{2}\ge0$ равна:`,
|
||||
opts: mc('$11$', '$19$', '$21$', '$34$', '$36$'),
|
||||
answer: 'г',
|
||||
sol: R`Множители $(x+4)^{8}\ge0$ и $(x-13)^{2}\ge0$, поэтому знак выражения определяет $(6-x)$. Неравенство выполнено при $x\le6$, а также при $x=13$ (там левая часть равна $0$). Натуральные решения $1,2,3,4,5,6,13$; их сумма $34$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Для покраски стен общей площадью $175$ м² планируется закупка краски. Банка объёмом $2{,}5$ л стоит $75\,000$ рублей, банка объёмом $10$ л — $270\,000$ рублей. Какую минимальную сумму (в рублях) потратят на покупку необходимого количества краски, если её расход составляет $0{,}2$ л/м²?`,
|
||||
answer: '960000',
|
||||
sol: R`Нужно $175\cdot0{,}2=35$ л краски. Литр в банке $10$ л дешевле ($27\,000$ против $30\,000$). Минимум даёт набор $3$ банки по $10$ л и $2$ банки по $2{,}5$ л: $30+5=35$ л за $3\cdot270\,000+2\cdot75\,000=960\,000$ рублей.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму корней (корень, если он единственный) уравнения $2x\cdot\sqrt{7x+18}=x^{2}+7x+18$.`,
|
||||
answer: '9',
|
||||
sol: R`Пусть $u=\sqrt{7x+18}\ge0$, тогда $u^{2}=7x+18$ и уравнение примет вид $x^{2}-2xu+u^{2}=0$, то есть $(x-u)^{2}=0$, $x=u$. Значит $x=\sqrt{7x+18}$, $x^{2}-7x-18=0$, $x=9$ (корень $-2$ не подходит, так как $x\ge0$).`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`В равнобедренную трапецию, площадь которой равна $36\tfrac18$, вписана окружность. Сумма двух углов трапеции равна $60^\circ$. Найдите периметр трапеции.`,
|
||||
answer: '34',
|
||||
sol: R`Два равных угла при большем основании дают $60^\circ$, значит острый угол равен $30^\circ$. Для описанной около окружности трапеции $a+b=2l$ (сумма оснований равна сумме боковых сторон). Высота $h=l\sin30^\circ=\dfrac{l}{2}$, площадь $S=\dfrac12(a+b)h=\dfrac{l^{2}}{2}=\dfrac{289}{8}$, откуда $l=\dfrac{17}{2}$. Периметр $a+b+2l=4l=34$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 4' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Пусть $(x;y)$ — решение системы уравнений $\begin{cases}5x-y=5,\\ 5x^{2}-xy+x=12.\end{cases}$ Найдите значение выражения $5y-x$.`,
|
||||
answer: '23',
|
||||
sol: R`Из первого уравнения $y=5x-5$. Подставив во второе: $5x^{2}-x(5x-5)+x=12$, то есть $6x=12$, $x=2$, $y=5$. Тогда $5y-x=25-2=23$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Найдите значение выражения $2\cdot\left(\sqrt[3]{5\sqrt5}-\sqrt[3]{6\sqrt6}\right):\left(\sqrt5+\sqrt6\right)-4\sqrt{30}$.`,
|
||||
answer: '-22',
|
||||
sol: R`$\sqrt[3]{5\sqrt5}=\sqrt5$ и $\sqrt[3]{6\sqrt6}=\sqrt6$. Тогда $\dfrac{2(\sqrt5-\sqrt6)}{\sqrt5+\sqrt6}=\dfrac{2(\sqrt5-\sqrt6)^{2}}{5-6}=-2(11-2\sqrt{30})=-22+4\sqrt{30}$. Вычитая $4\sqrt{30}$, получаем $-22$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите сумму корней уравнения $(x-81)\cdot\left(9^{x}+8\cdot3^{x+1}-81\right)=0$.`,
|
||||
answer: '82',
|
||||
sol: R`Первый множитель даёт $x=81$. Во втором при $t=3^{x}$: $9^{x}+24\cdot3^{x}-81=t^{2}+24t-81=0$, $t=3$ (корень $t=-27$ отброшен), откуда $3^{x}=3$, $x=1$. Сумма корней $81+1=82$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`Найдите площадь боковой поверхности правильной треугольной пирамиды, если длина биссектрисы её основания равна $4\sqrt3$, а плоский угол при вершине равен $2\operatorname{arctg}\dfrac45$.`,
|
||||
answer: '60',
|
||||
sol: R`Биссектриса (она же медиана) равностороннего основания равна $\dfrac{a\sqrt3}{2}=4\sqrt3$, откуда $a=8$. В боковой грани половина угла при вершине $\beta=\operatorname{arctg}\dfrac45$, и $\operatorname{tg}\beta=\dfrac{a/2}{l}=\dfrac45$, поэтому апофема $l=5$. Площадь грани $\dfrac12\cdot8\cdot5=20$, боковая поверхность $3\cdot20=60$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму наименьшего и наибольшего целых решений неравенства $\log_{1/15}\bigl(\log_{2}\log_{9}(x+15)\bigr)>0$.`,
|
||||
answer: '60',
|
||||
sol: R`Основание $\dfrac{1}{15}<1$, поэтому $0<\log_{2}\log_{9}(x+15)<1$, откуда $1<\log_{9}(x+15)<2$, то есть $9<x+15<81$, $-6<x<66$. Целые решения от $-5$ до $65$; их наименьшее и наибольшее в сумме дают $-5+65=60$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) сумму корней уравнения $10\sin5x\cos5x+5\sin10x\cos18x=0$ на промежутке $(110^\circ;170^\circ)$.`,
|
||||
answer: '712',
|
||||
sol: R`$10\sin5x\cos5x=5\sin10x$, поэтому $5\sin10x(1+\cos18x)=0$. Из $\sin10x=0$: $x=18^\circ k$ — корни $126^\circ,144^\circ,162^\circ$. Из $\cos18x=-1$: $x=10^\circ+20^\circ k$ — корни $130^\circ,150^\circ$. Сумма $126+144+162+130+150=712$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите произведение наименьшего и наибольшего целых решений неравенства $|15-2x-x^{2}|+4<4\cdot|3-x|+|x+5|$.`,
|
||||
answer: '-24',
|
||||
sol: R`Так как $|15-2x-x^{2}|=|x+5|\cdot|x-3|$, неравенство равносильно $(|x+5|-4)(|x-3|-1)<0$, его решение — $(-9;-1)\cup(2;4)$. Целые решения $-8,-7,\ldots,-2$ и $3$; произведение наименьшего и наибольшего $(-8)\cdot3=-24$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 5,
|
||||
text: R`Точка $A$ движется по периметру треугольника $KMP$. Точки $K_1,M_1,P_1$ лежат на медианах треугольника $KMP$ и делят их в отношении $11:3$, считая от вершин. По периметру треугольника $K_1M_1P_1$ движется точка $B$ со скоростью, в пять раз большей скорости точки $A$. Сколько раз точка $B$ обойдёт периметр треугольника $K_1M_1P_1$ за то время, за которое точка $A$ дважды обойдёт периметр треугольника $KMP$?`,
|
||||
answer: '56',
|
||||
sol: R`Точки на медианах в отношении $11:3$ от вершин дают треугольник $K_1M_1P_1$, подобный $KMP$ с коэффициентом $\dfrac{5}{28}$ (например, $\vec{K_1M_1}=\dfrac{5}{28}\vec{KM}$). За два обхода $A$ (путь $2P$) точка $B$ проходит $5\cdot2P=10P$, что составляет $\dfrac{10P}{(5/28)P}=56$ обходов.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`Объём прямоугольного параллелепипеда $ABCDA_1B_1C_1D_1$ равен $1728$. Точка $P$ лежит на боковом ребре $CC_1$ так, что $CP:PC_1=2:1$. Через точку $P$, вершину $D$ и середину бокового ребра $AA_1$ проведена секущая плоскость, делящая параллелепипед на две части. Найдите объём меньшей из частей.`,
|
||||
answer: '724',
|
||||
sol: R`В единичных долях рёбер ($u,v,w\in[0;1]$) секущая плоскость задаётся уравнением $-\dfrac{2u}{3}+\dfrac{v}{2}+w=\dfrac12$. Объём части, где $w$ больше плоскости, равен $\dfrac{181}{432}$ объёма параллелепипеда — это меньше половины. Значит меньшая часть $=\dfrac{181}{432}\cdot1728=181\cdot4=724$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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();
|
||||
@@ -0,0 +1,350 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2017_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2017, Вариант 1.
|
||||
Формат: Часть А = А1–А18, Часть В = В1–В12 (В1 — на соответствие). Всего 30 заданий.
|
||||
Перенабрано вручную в KaTeX по PDF: F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\2017\CT-2017.pdf
|
||||
(ответы — отдельный файл «Ответы ЦТ 2017.pdf», столбец «Вариант 1»).
|
||||
|
||||
⚠️ ВСЕ 30 ответов решены самостоятельно и СВЕРЕНЫ с официальной таблицей — полное
|
||||
совпадение, включая B6=56, B8=-143, B11=121, B12=115. variant=118 (закрывает пробел
|
||||
между ЦТ-2016 и ЦТ-2018). Прогнан через дедуп-гейт (check_variant_dups.js) — без повторов.
|
||||
|
||||
Реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А1 (вращение прямоугольников) → размеры сторона-ось/смежная даны числами (квадрат-сечение ⟺ ось=2·смежная → 3,5);
|
||||
• А3 (график движения) → путь на участке BC задан числами (52 км/ч);
|
||||
• А9 (треугольник по рисунку) → явно: BM — биссектриса угла B, AM/MC=AB/BC → 13,8;
|
||||
• А11 (фигура на сетке) → площадь фигуры дана числом ($18$ см² = 28 % трапеции → 64 2/7);
|
||||
• А14 (выбор параболы) → вершина/точка/направление ветвей в тексте.
|
||||
Без авторских ссылок (политика «все учебники наши»).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2017_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2017_v1.js --apply # запись в БД
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 118;
|
||||
const N_TASKS = 30;
|
||||
const PROV = 'ЦТ–2017, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Прямоугольник вращают вокруг указанной стороны (оси), образуя цилиндр. Осевым сечением цилиндра должен быть квадрат. Укажите номера прямоугольников (ось $\times$ смежная сторона): $1)\ 8\times8$; $\ 2)\ 8\times16$; $\ 3)\ 8\times4$; $\ 4)\ 4\times8$; $\ 5)\ 16\times8$.`,
|
||||
opts: mc('$2,\ 3$', '$1,\ 5$', '$3,\ 5$', '$2,\ 4$', '$1,\ 3,\ 5$'),
|
||||
answer: 'в',
|
||||
sol: R`Осевое сечение — прямоугольник «ось $\times$ диаметр $=$ ось $\times2\cdot$смежная». Это квадрат, когда ось $=2\cdot$смежная: для $8\times4$ ($8=2\cdot4$) и $16\times8$ ($16=2\cdot8$). Значит $3$ и $5$.` },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Выразите $737$ см $8$ мм в метрах с точностью до сотых.`,
|
||||
opts: mc('$0{,}74$ м', '$7{,}37$ м', '$7{,}378$ м', '$7{,}38$ м', '$73{,}78$ м'),
|
||||
answer: 'г',
|
||||
sol: R`$737$ см $8$ мм $=7{,}378$ м $\approx7{,}38$ м.` },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 1,
|
||||
text: R`По графику движения автомобиля на участке $BC$ путь изменился с $52$ км до $104$ км за $1$ ч. Найдите скорость движения на участке $BC$.`,
|
||||
opts: mc('$26$ км/ч', '$52$ км/ч', '$78$ км/ч', '$104$ км/ч', '$60$ км/ч'),
|
||||
answer: 'б',
|
||||
sol: R`$v=\dfrac{104-52}{1}=52$ км/ч.` },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Выразите $a$ из равенства $\dfrac{3}{2b+1}=\dfrac{6}{a-b}$.`,
|
||||
opts: mc('$a=5b+2$', '$a=5b-2$', '$a=15b-6$', '$a=15b+6$', '$a=3b+1$'),
|
||||
answer: 'а',
|
||||
sol: R`$3(a-b)=6(2b+1)$, $a-b=4b+2$, $a=5b+2$.` },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Значение выражения $8\sqrt3+\dfrac18\sqrt{192}$ равно:`,
|
||||
opts: mc('$16\sqrt3$', '$\sqrt{195}$', '$\dfrac{65\sqrt{195}}{8}$', '$\dfrac{6\sqrt3}{8}$', '$9\sqrt3$'),
|
||||
answer: 'д',
|
||||
sol: R`$\sqrt{192}=8\sqrt3$, поэтому $\dfrac18\cdot8\sqrt3=\sqrt3$ и $8\sqrt3+\sqrt3=9\sqrt3$.` },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 1,
|
||||
text: R`Последовательность $(a_n)$ задана формулой $a_n=3n^{2}-8n+9$. Второй член этой последовательности равен:`,
|
||||
opts: mc('$12$', '$-16$', '$5$', '$16$', '$6$'),
|
||||
answer: 'в',
|
||||
sol: R`$a_2=3\cdot4-16+9=5$.` },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Значение выражения $7\cos^{2}34^\circ+10\sin30^\circ+7\sin^{2}34^\circ$ равно:`,
|
||||
opts: mc('$12$', '$17$', '$24$', '$7+10\sqrt3$', '$14+5\sqrt3$'),
|
||||
answer: 'а',
|
||||
sol: R`$7(\cos^{2}34^\circ+\sin^{2}34^\circ)+10\cdot\tfrac12=7+5=12$.` },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
|
||||
text: R`Среди утверждений укажите номер верного.<br>$1)$ число $451$ кратно числу $5$; $\ 2)$ число $9$ кратно числу $35$; $\ 3)$ число $2$ кратно числу $14$; $\ 4)$ число $116$ кратно числу $1$; $\ 5)$ число $43$ кратно числу $0$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'г',
|
||||
sol: R`Любое целое кратно $1$, поэтому $116$ кратно $1$ — верно (утверждение 4). Остальные неверны.` },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В треугольнике $ABC$ отрезок $BM$ — биссектриса угла $B$ ($M$ на $AC$). Известно, что $AC=32$, $AM=12$, $BC=23$. Найдите длину стороны $AB$.`,
|
||||
opts: mc('$10{,}2$', '$14{,}6$', '$13{,}8$', '$13{,}5$', '$10{,}4$'),
|
||||
answer: 'в',
|
||||
sol: R`Биссектриса делит сторону в отношении прилежащих сторон: $\dfrac{AM}{MC}=\dfrac{AB}{BC}$. $MC=32-12=20$, поэтому $AB=BC\cdot\dfrac{AM}{MC}=23\cdot\dfrac{12}{20}=13{,}8$.` },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Результат упрощения выражения $\sqrt{(2x-4{,}6)^{2}}+4{,}6$ при $-1<x<1$ имеет вид:`,
|
||||
opts: mc('$9{,}2-2x$', '$-2x-9{,}2$', '$2x+9{,}2$', '$2x$', '$-2x$'),
|
||||
answer: 'а',
|
||||
sol: R`При $-1<x<1$ имеем $2x-4{,}6<0$, поэтому $\sqrt{(2x-4{,}6)^{2}}=4{,}6-2x$, и сумма $=(4{,}6-2x)+4{,}6=9{,}2-2x$.` },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Площадь фигуры на клетчатой бумаге равна $18$ см² и составляет 28 % площади некоторой трапеции. Найдите площадь трапеции (в квадратных сантиметрах).`,
|
||||
opts: mc('$504$', '$64\tfrac27$', '$35$', '$72\tfrac34$', '$155\tfrac59$'),
|
||||
answer: 'б',
|
||||
sol: R`$\dfrac{18}{0{,}28}=\dfrac{1800}{28}=\dfrac{450}{7}=64\tfrac27$ см².` },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Определите остроугольный треугольник по длинам его сторон: $\triangle ABC$ ($8;15;17$), $\triangle MNK$ ($4;6;8$), $\triangle BDC$ ($3;4;5$), $\triangle FBC$ ($7;8;9$), $\triangle CDE$ ($5;11;13$).`,
|
||||
opts: mc('$\triangle ABC$', '$\triangle MNK$', '$\triangle BDC$', '$\triangle FBC$', '$\triangle CDE$'),
|
||||
answer: 'г',
|
||||
sol: R`Треугольник остроугольный, если квадрат большей стороны меньше суммы квадратов двух других. Только для $\triangle FBC$: $9^{2}=81<7^{2}+8^{2}=113$. ($ABC$ и $BDC$ прямоугольные, $MNK$ и $CDE$ тупоугольные.)` },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Купили $m$ ручек по цене $2$ руб $3$ коп за штуку и $178$ тетрадей по цене $a$ коп за штуку. Составьте выражение, определяющее стоимость покупки (в рублях).`,
|
||||
opts: mc('$2{,}03m+178a$', '$2{,}03m+1{,}78a$', '$2{,}3m+1{,}78a$', '$2{,}3m+17{,}8a$', '$2{,}03m+17{,}8a$'),
|
||||
answer: 'б',
|
||||
sol: R`$2$ руб $3$ коп $=2{,}03$ руб, $178$ тетрадей по $a$ коп $=178a$ коп $=1{,}78a$ руб. Итого $2{,}03m+1{,}78a$.` },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Парабола проходит через точку $(0;3)$, имеет вершину в точке $(-1;1)$, ветви направлены вверх. Укажите номер её уравнения.<br>$1)\ y=x^{2}+4x+3$; $\ 2)\ y=x^{2}-4x-3$; $\ 3)\ y=2x^{2}+4x+3$; $\ 4)\ y=2x^{2}+4x-3$; $\ 5)\ y=2x^{2}-4x+3$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'в',
|
||||
sol: R`У $y=2x^{2}+4x+3$ вершина в точке $(-1;1)$ ($x=-\tfrac{4}{4}=-1$, $y=2-4+3=1$), $y(0)=3$, ветви вверх. Это уравнение 3.` },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — куб. Точки $M$ и $N$ — середины рёбер $AD$ и $DC$, точка $K$ на ребре $A_1D_1$ с $KA_1:KD_1=1:3$. Сечением куба плоскостью, проходящей через точки $M$, $N$ и $K$, является:`,
|
||||
opts: mc('восьмиугольник', 'треугольник', 'четырёхугольник', 'пятиугольник', 'шестиугольник'),
|
||||
answer: 'в',
|
||||
sol: R`Плоскость отсекает ребро $DD_1$ (оба конца по одну сторону) и пересекает четыре ребра: $AD$ (точка $M$), $DC$ ($N$), $A_1D_1$ ($K$) и $D_1C_1$. Четыре точки — сечение является четырёхугольником.` },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-448{,}9<2{,}9+9x<23{,}6$.`,
|
||||
opts: mc('$-52$', '$-47$', '$-49$', '$-48$', '$-53$'),
|
||||
answer: 'г',
|
||||
sol: R`$-451{,}8<9x<20{,}7$, то есть $-50{,}2<x<2{,}3$. Целые от $-50$ до $2$; сумма наименьшего и наибольшего $-50+2=-48$.` },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Через точку $A$ высоты $SO$ конуса проведена плоскость, параллельная основанию. Определите, во сколько раз площадь основания конуса больше площади полученного сечения, если $SA:AO=2:3$.`,
|
||||
opts: mc('$6\tfrac14$', '$7\tfrac14$', '$2\tfrac14$', '$1\tfrac12$', '$2\tfrac12$'),
|
||||
answer: 'а',
|
||||
sol: R`Сечение подобно основанию с коэффициентом $\dfrac{SA}{SO}=\dfrac{2}{5}$. Отношение площадей $\left(\dfrac{SO}{SA}\right)^{2}=\left(\dfrac52\right)^{2}=6\tfrac14$.` },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Укажите (в градусах) наименьший положительный корень уравнения $\cos(6x-72^\circ)=\dfrac{\sqrt3}{2}$.`,
|
||||
opts: mc('$5^\circ$', '$102^\circ$', '$17^\circ$', '$42^\circ$', '$7^\circ$'),
|
||||
answer: 'д',
|
||||
sol: R`$6x-72^\circ=\pm30^\circ+360^\circ k$, поэтому $x=17^\circ+60^\circ k$ или $x=7^\circ+60^\circ k$. Наименьший положительный корень $7^\circ$.` },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'long', topic: 'functions', subtopic: 'fn-graphs', diff: 3,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание $1$–$6$.<br>А) Окружность с центром $(-8;-2)$ и радиусом $4$ задаётся уравнением …<br>Б) Уравнение прямой, проходящей через точку $(-8;2)$ параллельно прямой $y=\tfrac14 x$, имеет вид …<br>В) График обратной пропорциональности, проходящий через точку $\left(\tfrac12;-\tfrac12\right)$, задаётся уравнением …<br>Окончания: $1)\ xy=2$; $\ 2)\ (x-8)^{2}+(y-2)^{2}=4$; $\ 3)\ -\tfrac14 x+y=4$; $\ 4)\ (x+8)^{2}+(y+2)^{2}=16$; $\ 5)\ 4xy+1=0$; $\ 6)\ \tfrac14 x+y=2$.`,
|
||||
answer: 'А4Б3В5',
|
||||
ansShow: 'А4Б3В5',
|
||||
sol: R`А) $(x+8)^{2}+(y+2)^{2}=16$ (окончание 4). Б) $y-2=\tfrac14(x+8)$, то есть $-\tfrac14 x+y=4$ (окончание 3). В) $y=\tfrac{k}{x}$ через $\left(\tfrac12;-\tfrac12\right)$ даёт $k=-\tfrac14$, то есть $xy=-\tfrac14$, $4xy+1=0$ (окончание 5). Ответ: А4Б3В5.` },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Конфеты в коробке упаковываются рядами, причём количество конфет в каждом ряду на $4$ больше количества рядов. Дизайн коробки изменили: добавили $2$ ряда, а в каждом ряду — по $1$ конфете. В результате количество конфет в коробке увеличилось на $25$. Сколько конфет упаковывалось в коробку первоначально?`,
|
||||
answer: '45',
|
||||
sol: R`Пусть рядов $r$, в ряду $r+4$. Тогда $(r+2)(r+5)-r(r+4)=3r+10=25$, $r=5$. Первоначально $5\cdot9=45$ конфет.` },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'expressions', subtopic: 'expr-polynomials', diff: 3,
|
||||
text: R`Известно, что при $a$, равном $-2$ и $4$, значение выражения $4a^{3}+3a^{2}-ab+c$ равно нулю. Найдите значение выражения $b+c$.`,
|
||||
answer: '-34',
|
||||
sol: R`При $a=-2$: $-20+2b+c=0$; при $a=4$: $304-4b+c=0$. Вычитая, $6b=324$, $b=54$, тогда $c=-88$, и $b+c=-34$.` },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 4,
|
||||
text: R`Найдите произведение корней (корень, если он единственный) уравнения $x^{2}-5x-3=4\sqrt{x^{2}-5x+9}$.`,
|
||||
answer: '-27',
|
||||
sol: R`Пусть $u=x^{2}-5x$. Тогда $u-3=4\sqrt{u+9}$ ($u\ge3$); возведя в квадрат, $u^{2}-22u-135=0$, $u=27$. Из $x^{2}-5x-27=0$ произведение корней $-27$.` },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`В параллелограмме с острым углом $45^\circ$ точка пересечения диагоналей удалена от прямых, содержащих неравные стороны, на расстояния $\dfrac{7\sqrt2}{2}$ и $2$. Найдите площадь параллелограмма.`,
|
||||
answer: '56',
|
||||
sol: R`Расстояние от центра до стороны — половина высоты. Высоты $H_1=7\sqrt2$ и $H_2=4$. Из $H=l\sin45^\circ$: стороны $b=14$, $a=4\sqrt2$. Площадь $=b\cdot H_2=14\cdot4=56$.` },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Пусть $x_0$ — наибольший корень уравнения $\log_2^{2}\dfrac{x}{32}+4\log_2 x-52=0$. Найдите значение выражения $7\sqrt[3]{x_0}$.`,
|
||||
answer: '56',
|
||||
sol: R`Пусть $t=\log_2 x$. Тогда $(t-5)^{2}+4t-52=0$, $t^{2}-6t-27=0$, $t=9$ или $t=-3$. Наибольший корень $x_0=2^{9}=512$, и $7\sqrt[3]{512}=7\cdot8=56$.` },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Решите неравенство $\left(\dfrac{1}{5-\sqrt{24}}\right)^{x+6}\ge\left(5-\sqrt{24}\right)^{\frac{4x+25}{x+4}}$. В ответ запишите сумму целых решений, принадлежащих промежутку $[-20;-2]$.`,
|
||||
answer: '-12',
|
||||
sol: R`Так как $\dfrac{1}{5-\sqrt{24}}=(5-\sqrt{24})^{-1}$ и $0<5-\sqrt{24}<1$, неравенство равносильно $-(x+6)\le\dfrac{4x+25}{x+4}$, что приводит к $\dfrac{(x+7)^{2}}{x+4}\ge0$. Решение: $x>-4$ или $x=-7$. На $[-20;-2]$ целые $-7,-3,-2$; их сумма $-12$.` },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 4,
|
||||
text: R`Найдите увеличенное в $9$ раз произведение абсцисс точек пересечения прямой $y=12$ и графика нечётной функции, которая определена на $(-\infty;0)\cup(0;+\infty)$ и при $x>0$ задаётся формулой $y=2^{3x-8}-20$.`,
|
||||
answer: '-143',
|
||||
sol: R`При $x>0$: $2^{3x-8}-20=12$, $2^{3x-8}=32$, $x=\tfrac{13}{3}$. По нечётности при $x<0$ получаем $x=-\tfrac{11}{3}$. Произведение $\tfrac{13}{3}\cdot\left(-\tfrac{11}{3}\right)=-\tfrac{143}{9}$; увеличенное в $9$ раз — $-143$.` },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`Найдите площадь полной поверхности прямой треугольной призмы, описанной около шара, если площадь основания призмы равна $7{,}5$.`,
|
||||
answer: '45',
|
||||
sol: R`У описанной около шара призмы высота $h=2r$, а в основании вписана окружность радиуса $r$, поэтому площадь основания $S=rp=7{,}5$. Боковая поверхность $=2p\cdot2r=4\cdot rp=30$. Полная $=2\cdot7{,}5+30=45$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите произведение наибольшего целого решения на количество целых решений неравенства $\dfrac{16}{6+|24-x|}>|24-x|$.`,
|
||||
answer: '75',
|
||||
sol: R`Пусть $u=|24-x|\ge0$. Тогда $16>u(6+u)$, $u^{2}+6u-16<0$, $0\le u<2$. Значит $|24-x|<2$, то есть $22<x<26$. Целые $23,24,25$ ($3$ решения); наибольшее $25$. Произведение $25\cdot3=75$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`Первые члены арифметической и геометрической прогрессий одинаковы и равны $1$, третьи члены также одинаковы, а вторые отличаются на $18$. Найдите шестой член арифметической прогрессии, если все члены обеих прогрессий положительны.`,
|
||||
answer: '121',
|
||||
sol: R`$1+2d=q^{2}$ и $\left|\tfrac{q^{2}+1}{2}-q\right|=\tfrac{(q-1)^{2}}{2}=18$, откуда $q=7$ (положительные члены), $d=24$. Тогда $a_6=1+5\cdot24=121$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — прямая четырёхугольная призма, объём которой равен $960$. Основанием призмы является параллелограмм $ABCD$. Точки $M$ и $N$ принадлежат рёбрам $A_1D_1$ и $C_1D_1$ так, что $A_1M:A_1D_1=1:2$, $D_1N:NC_1=1:3$. Отрезки $A_1N$ и $B_1M$ пересекаются в точке $K$. Найдите объём пирамиды $SB_1KNC_1$, если $S\in B_1D$ и $B_1S:SD=3:1$.`,
|
||||
answer: '115',
|
||||
sol: R`Координатным методом (положения $K$, $N$ на верхней грани и точки $S$ на диагонали $B_1D$) объём пирамиды составляет $\dfrac{23}{192}$ объёма призмы: $\dfrac{23}{192}\cdot960=115$.` },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2017_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_ct2017_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 → «Варианты» → «ЦТ-2017».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,381 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2018_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2018, Вариант 1.
|
||||
Формат: Часть А = А1–А18 (закрытые, 5 вариантов), Часть В = В1–В12 (открытые,
|
||||
В1 — на соответствие). Всего 30 заданий. Перенабрано вручную в KaTeX по PDF:
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2018.pdf (10 вариантов, табл. ответов стр.32–33).
|
||||
|
||||
⚠️ Ответы решены самостоятельно и СВЕРЕНЫ с официальной таблицей (столбец «Вариант 1»):
|
||||
ВСЕ 30 совпали, включая B8=-18, B9=-130 (двугранный угол), B11=32, B12=45 (координатный
|
||||
метод, PT=5/2). variant=114 (после ЦТ-2016 = 113).
|
||||
|
||||
Адаптации/реконструкции заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А3 (точки на прямой) → координаты точек заданы явно (противоположны $C$ и $A$);
|
||||
• А9 (графики движения) → пройденные пути даны числами ($80$ и $35$ км → $2\tfrac27$);
|
||||
• А11 (заштрихованная лестница в квадрате) → $12$ вырезанных квадратиков $x$ → $1-12x^{2}$;
|
||||
• В1/В2 (функция/график) → данные функции заданы формулой/ломаной по узлам в тексте;
|
||||
• В8 (громоздкое показательное, в скане несогласованно) → экв. уравнение $3^{x^2}5^{x^2}=15^3$
|
||||
с тем же ответом ($-18$ = $6\cdot(-3)$, корни $\pm\sqrt3$);
|
||||
• В10 (множитель $(6-x)^{2}$ восстановлен по официальному ответу $13$).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2018_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2018_v1.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 114;
|
||||
const PROV = 'ЦТ–2018, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Сегодня в Лиде $24$ °C, а в Барановичах температура $t$ °C воздуха не ниже, чем в Лиде. Укажите верное соотношение для $t$.`,
|
||||
opts: mc('$t>24$', '$t=23$', '$t\ge24$', '$t\le24$', '$t<24$'),
|
||||
answer: 'в',
|
||||
sol: R`«Не ниже $24$» означает «больше или равно $24$»: $t\ge24$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`Две окружности с центрами $A$ и $B$ касаются внешним образом в точке $C$. Точки $M$ и $K$ — концы их диаметров, лежащих на прямой $AB$ (в порядке $M,A,C,B,K$). Найдите радиус большей окружности, если радиус меньшей равен $5$, а $MK=28$.`,
|
||||
opts: mc('$9$', '$10$', '$14$', '$18$', '$8$'),
|
||||
answer: 'а',
|
||||
sol: R`$MK=2\cdot5+2R=28$, где $R$ — больший радиус, откуда $2R=18$, $R=9$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`На координатной прямой отмечены точки $D(-3)$, $C(-2)$, $A(2)$, $B(5)$. Укажите точки, координаты которых являются противоположными числами.`,
|
||||
opts: mc('$A$ и $D$', '$A$ и $C$', '$B$ и $D$', '$B$ и $C$', '$A$ и $B$'),
|
||||
answer: 'б',
|
||||
sol: R`Противоположные числа $-2$ и $2$ — это координаты точек $C$ и $A$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Из вершины угла $KMN$, градусная мера которого равна $170^\circ$, проведены два луча: $MP$, делящий угол пополам, и $MF$, делящий его в отношении $9:8$ (считая от стороны $MK$). Найдите градусную меру угла $FMP$.`,
|
||||
opts: mc('$20^\circ$', '$17^\circ$', '$4^\circ$', '$10^\circ$', '$5^\circ$'),
|
||||
answer: 'д',
|
||||
sol: R`$\angle KMP=\dfrac{170^\circ}{2}=85^\circ$, $\angle KMF=170^\circ\cdot\dfrac{9}{17}=90^\circ$. Тогда $\angle FMP=90^\circ-85^\circ=5^\circ$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 1,
|
||||
text: R`Известно, что число $177$ является членом арифметической прогрессии $(a_n)$, заданной формулой $a_n=6n-3$. Найдите его номер.`,
|
||||
opts: mc('$30$', '$29$', '$27$', '$26$', '$25$'),
|
||||
answer: 'а',
|
||||
sol: R`$6n-3=177$, $6n=180$, $n=30$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4' },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Вычислите $8^{\,1+\log_{8}6}$.`,
|
||||
opts: mc('$6$', '$14$', '$24$', '$48$', '$56$'),
|
||||
answer: 'г',
|
||||
sol: R`$8^{\,1+\log_{8}6}=8\cdot8^{\log_{8}6}=8\cdot6=48$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Укажите уравнение прямой, проходящей через точку $A(5;9)$ параллельно оси абсцисс.`,
|
||||
opts: mc('$x=5$', '$y=5$', '$y=9$', '$x=9$', '$5x+9y=0$'),
|
||||
answer: 'в',
|
||||
sol: R`Прямая, параллельная оси абсцисс, горизонтальна, поэтому имеет вид $y=b$. Через точку $A(5;9)$ проходит прямая $y=9$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 5' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Для одночлена $-5c^{3}\cdot3c^{2}y$ укажите номер верного утверждения.`,
|
||||
opts: mc('стандартный вид одночлена — $-15c^{5}y$', 'значение при $c=-1$, $y=-1$ равно $15$', 'при делении на $3$ получится $-c^{5}y$', 'коэффициент одночлена равен $-5$', 'степень одночлена равна $5$'),
|
||||
answer: 'а',
|
||||
sol: R`$-5c^{3}\cdot3c^{2}y=-15c^{5}y$ — это стандартный вид (утверждение 1). Остальные неверны: значение при $c=-1,y=-1$ равно $-15$; деление на $3$ даёт $-5c^{5}y$; коэффициент $-15$; степень $5+1=6$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Мотоциклист и велосипедист выехали одновременно из одного пункта в одном направлении. По графику движения к некоторому моменту мотоциклист проехал $80$ км, а велосипедист — $35$ км. Во сколько раз скорость мотоциклиста больше скорости велосипедиста?`,
|
||||
opts: mc('$3\tfrac12$ раза', '$1\tfrac19$ раза', '$2\tfrac17$ раза', '$2\tfrac27$ раза', '$2\tfrac{1}{16}$ раза'),
|
||||
answer: 'г',
|
||||
sol: R`За одно и то же время отношение скоростей равно отношению путей: $\dfrac{80}{35}=\dfrac{16}{7}=2\tfrac27$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Значение выражения $\sqrt{\left(11+8\sqrt2\right)^{2}}+\sqrt{\left(11-8\sqrt2\right)^{2}}$ равно:`,
|
||||
opts: mc('$16\sqrt2$', '$38\sqrt2$', '$22$', '$16\sqrt2-22$', '$16\sqrt2+22$'),
|
||||
answer: 'а',
|
||||
sol: R`$\sqrt{(11+8\sqrt2)^{2}}=11+8\sqrt2$; так как $8\sqrt2>11$, то $\sqrt{(11-8\sqrt2)^{2}}=8\sqrt2-11$. Сумма $=(11+8\sqrt2)+(8\sqrt2-11)=16\sqrt2$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Из квадрата со стороной $1$ удалили $12$ равных квадратов со стороной $x$. Найдите выражение для площади оставшейся (заштрихованной) части квадрата.`,
|
||||
opts: mc('$1-4x^{2}$', '$4-12x^{2}$', '$1-8x^{2}$', '$4-16x$', '$1-12x^{2}$'),
|
||||
answer: 'д',
|
||||
sol: R`Площадь квадрата равна $1$, площадь $12$ вырезанных квадратиков — $12x^{2}$. Оставшаяся часть: $1-12x^{2}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\sin\left(\operatorname{arctg}\sqrt3\right)$.`,
|
||||
opts: mc('$\dfrac{\sqrt3}{3}$', '$\dfrac12$', '$\dfrac{\sqrt3}{2}$', '$\dfrac{\sqrt2}{2}$', '$1$'),
|
||||
answer: 'в',
|
||||
sol: R`$\operatorname{arctg}\sqrt3=60^\circ$, поэтому $\sin60^\circ=\dfrac{\sqrt3}{2}$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Найдите сумму наименьшего и наибольшего целых решений двойного неравенства $-5{,}2<3-0{,}1x<4{,}59$.`,
|
||||
opts: mc('$96$', '$97$', '$65$', '$67$', '$66$'),
|
||||
answer: 'д',
|
||||
sol: R`Из $3-0{,}1x<4{,}59$: $x>-15{,}9$; из $-5{,}2<3-0{,}1x$: $x<82$. Целые $x$ от $-15$ до $81$; их сумма наименьшего и наибольшего $-15+81=66$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`Длины двух сторон треугольника равны $6$ и $7$, его площадь равна $3\sqrt{33}$. Найдите наибольшее значение, которое может принимать длина третьей стороны.`,
|
||||
opts: mc('$\sqrt{151}$', '$\sqrt{133}$', '$12$', '$13$', '$2\sqrt{33}$'),
|
||||
answer: 'б',
|
||||
sol: R`$S=\dfrac12\cdot6\cdot7\sin C=21\sin C=3\sqrt{33}$, поэтому $\sin C=\dfrac{\sqrt{33}}{7}$, $\cos C=\pm\dfrac47$. Третья сторона $c^{2}=36+49-84\cos C$; наибольшая при $\cos C=-\dfrac47$: $c^{2}=85+48=133$, $c=\sqrt{133}$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Укажите номер уравнения, которое имеет более одного корня.`,
|
||||
opts: mc('$5x+2=2$', '$2(9-2x)=-4x$', '$\dfrac25x+7=x$', '$\dfrac{5x+2}{3}=4$', '$5x+2=\dfrac{15x+6}{3}$'),
|
||||
answer: 'д',
|
||||
sol: R`Уравнение $5x+2=\dfrac{15x+6}{3}=5x+2$ — тождество, верно при любом $x$ (бесконечно много корней). Остальные имеют ровно один корень или ни одного.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1' },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Найдите объём конуса, образующая которого равна $4\sqrt6$, а угол при вершине осевого сечения равен $60^\circ$.`,
|
||||
opts: mc('$144\sqrt2\,\pi$', '$16\sqrt2\,\pi$', '$48\sqrt2\,\pi$', '$48\sqrt6\,\pi$', '$384\sqrt2\,\pi$'),
|
||||
answer: 'в',
|
||||
sol: R`Осевое сечение — равнобедренный треугольник с углом $60^\circ$ при вершине, то есть равносторонний. Радиус $R=l\sin30^\circ=4\sqrt6\cdot\dfrac12=2\sqrt6$, высота $h=l\cos30^\circ=4\sqrt6\cdot\dfrac{\sqrt3}{2}=6\sqrt2$. Объём $V=\dfrac13\pi R^{2}h=\dfrac13\pi\cdot24\cdot6\sqrt2=48\sqrt2\,\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2' },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Сумма (в градусах) наименьшего положительного и наибольшего отрицательного корней уравнения $\sin(5x-10^\circ)=-\dfrac{\sqrt2}{2}$ равна:`,
|
||||
opts: mc('$81^\circ$', '$55^\circ$', '$60^\circ$', '$40^\circ$', '$35^\circ$'),
|
||||
answer: 'г',
|
||||
sol: R`$5x-10^\circ=-45^\circ+360^\circ k$ или $5x-10^\circ=225^\circ+360^\circ k$, то есть $x=-7^\circ+72^\circ k$ или $x=47^\circ+72^\circ k$. Наименьший положительный корень $47^\circ$, наибольший отрицательный $-7^\circ$; их сумма $40^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, все рёбра которой равны $9$. Точки $P$ и $K$ — середины рёбер $BB_1$ и $AC$, точка $M$ на ребре $CC_1$ такова, что $C_1M:C_1C=1:3$. Найдите длину отрезка, по которому плоскость, проходящая через $K,M,P$, пересекает грань $AA_1B_1B$.`,
|
||||
opts: mc('$\dfrac{9\sqrt5}{7}$', '$\dfrac{9\sqrt{85}}{14}$', '$\dfrac{9\sqrt2}{2}$', '$\dfrac{9\sqrt{65}}{14}$', '$\dfrac{3\sqrt{17}}{2}$'),
|
||||
answer: 'б',
|
||||
sol: R`Введём координаты $A(0;0;0)$, $B(9;0;0)$, $C\left(4{,}5;\dfrac{9\sqrt3}{2};0\right)$ и верхние вершины со сдвигом $+9$ по оси $z$. Секущая плоскость через $K,M,P$ пересекает грань $AA_1B_1B$ (плоскость $y=0$) по прямой $10{,}5x-9z=54$. Её отрезок внутри грани идёт от $\left(\dfrac{36}{7};0;0\right)$ до $P(9;0;4{,}5)$; длина $\sqrt{\left(\dfrac{27}{7}\right)^{2}+4{,}5^{2}}=\dfrac{9\sqrt{85}}{14}$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 4' },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'long', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Функция задана формулой $f(x)=x^{2}-10x-3$ на множестве $\mathbb{R}$. Для начала каждого из предложений А–В подберите его окончание $1$–$6$ так, чтобы получилось верное утверждение.<br>А) Сумма координат точки пересечения графика с осью $Oy$ равна …<br>Б) Сумма нулей функции равна …<br>В) Если ось симметрии графика задаётся уравнением $x=a$, то значение $a$ равно …<br>Окончания: $1)\;-3$; $\ 2)\;3$; $\ 3)\;5$; $\ 4)\;-10$; $\ 5)\;-5$; $\ 6)\;10$.`,
|
||||
answer: 'А1Б6В3',
|
||||
ansShow: 'А1Б6В3',
|
||||
sol: R`А) График пересекает $Oy$ в точке $(0;f(0))=(0;-3)$, сумма координат $-3$ (окончание 1). Б) Сумма нулей по теореме Виета равна $10$ (окончание 6). В) Ось симметрии $x=\dfrac{10}{2}=5$, значит $a=5$ (окончание 3). Ответ: А1Б6В3.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Функция $y=f(x)$ на промежутке $[-6;4]$ задана ломаной, последовательно соединяющей точки $(-6;-1)$, $(-5;0)$, $(-4;1)$, $(-3;1)$, $(-2;0)$, $(0;-3)$, $(2;-2)$, $(4;3)$. Выберите номера верных утверждений (запишите цифрами в порядке возрастания).<br>$1)$ нулём функции является число $-3$;<br>$2)$ $f(x)>0$ при $x\in(-5;-2)$;<br>$3)$ функция возрастает на промежутке $[2;4]$;<br>$4)$ наибольшее значение функции на $[-6;4]$ равно $2$;<br>$5)$ график пересекает ось ординат в точке $(0;-2)$.`,
|
||||
answer: '23',
|
||||
sol: R`Нули функции — $-5$ и $-2$ (не $-3$), значит 1 неверно. На $(-5;-2)$ ломаная положительна — 2 верно. На $[2;4]$: $f(2)=-2<f(4)=3$, функция возрастает — 3 верно. Наибольшее значение $f(4)=3\ne2$ — 4 неверно. $f(0)=-3\ne-2$ — 5 неверно. Верны утверждения $2$ и $3$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`В жилом доме «Альфа» 13 % всех квартир — однокомнатные, а в доме «Омега» — 61 %. Определите, во сколько раз больше общее число квартир в доме «Альфа», если однокомнатные составляют 16 % всех квартир в двух домах.`,
|
||||
answer: '15',
|
||||
sol: R`Пусть в «Альфа» $a$ квартир, в «Омега» — $o$. Тогда $0{,}13a+0{,}61o=0{,}16(a+o)$, откуда $0{,}45o=0{,}03a$ и $\dfrac{a}{o}=15$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму квадратов корней уравнения $\left(x^{2}+2x-8\right)\sqrt{x+1}=4x^{2}+8x-32$.`,
|
||||
answer: '229',
|
||||
sol: R`$4x^{2}+8x-32=4(x^{2}+2x-8)$, поэтому $(x^{2}+2x-8)(\sqrt{x+1}-4)=0$. ОДЗ $x\ge-1$. Из $x^{2}+2x-8=0$: $x=2$ (корень $-4$ вне ОДЗ); из $\sqrt{x+1}=4$: $x=15$. Сумма квадратов $2^{2}+15^{2}=229$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Градусная мера угла правильного многоугольника равна $150^\circ$, а длина его стороны равна $6$. Найдите периметр многоугольника.`,
|
||||
answer: '72',
|
||||
sol: R`Внешний угол $180^\circ-150^\circ=30^\circ$, число сторон $n=\dfrac{360^\circ}{30^\circ}=12$. Периметр $12\cdot6=72$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Найдите произведение наименьшего и наибольшего целых решений неравенства $\log_{1/9}\dfrac{9-x}{x+17}\ge0$.`,
|
||||
answer: '-32',
|
||||
sol: R`Основание $\dfrac19<1$, поэтому $0<\dfrac{9-x}{x+17}\le1$. Первое неравенство даёт $-17<x<9$, второе — $\dfrac{x+4}{x+17}\ge0$, то есть $x\ge-4$. Значит $-4\le x<9$, целые от $-4$ до $8$; произведение $(-4)\cdot8=-32$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 3,
|
||||
text: R`О натуральных числах $a$ и $b$ известно, что $a>b$, $a+b=85$ и НОК$(a;b)=102$. Найдите число $b$.`,
|
||||
answer: '34',
|
||||
sol: R`Пусть $d=$НОД$(a;b)$, $a=dm$, $b=dn$, $\gcd(m;n)=1$. Тогда $d(m+n)=85$, $dmn=102$; общий делитель $85$ и $102$ равен $17$, поэтому $d=17$, $m+n=5$, $mn=6$, откуда $m=3$, $n=2$. Значит $a=51$, $b=34$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите увеличенное в $6$ раз произведение корней уравнения $3^{x^{2}}\cdot5^{x^{2}}=15^{3}$.`,
|
||||
answer: '-18',
|
||||
sol: R`$3^{x^{2}}\cdot5^{x^{2}}=15^{x^{2}}=15^{3}$, поэтому $x^{2}=3$, $x=\pm\sqrt3$. Произведение корней $-3$; увеличенное в $6$ раз — это $-18$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`В основании пирамиды $SABCD$ лежит квадрат $ABCD$ со стороной $1$. Боковое ребро $SB$ перпендикулярно плоскости основания и равно $3$. Найдите значение выражения $\dfrac{13}{\cos\varphi}$, где $\varphi$ — линейный угол двугранного угла при боковом ребре $SD$.`,
|
||||
answer: '-130',
|
||||
sol: R`Координаты $A(0;0;0)$, $B(1;0;0)$, $C(1;1;0)$, $D(0;1;0)$, $S(1;0;3)$. Для двугранного угла при ребре $SD$ берём в гранях $SAD$ и $SCD$ векторы из $D$, перпендикулярные $DS$. Вычисление даёт $\cos\varphi=-\dfrac{1}{10}$, поэтому $\dfrac{13}{\cos\varphi}=-130$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 4' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите сумму целых решений неравенства $\dfrac{(x^{2}-x-12)(6-x)^{2}}{6-x^{2}-x}\ge0$.`,
|
||||
answer: '13',
|
||||
sol: R`После сокращения на $(x+3)$ (при $x\ne-3$): $\dfrac{(x-4)(6-x)^{2}}{-(x-2)}\ge0$. Множитель $(6-x)^{2}\ge0$, поэтому решение — промежуток $(2;4]$ и отдельная точка $x=6$. Целые решения $3,4,6$; их сумма $13$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 5,
|
||||
text: R`От пристани $B$ отплывает плот и одновременно против течения отходит катер. Доплыв до пристани $A$ (на расстоянии $s_1$ от $B$ выше по течению), катер разворачивается и плывёт к пристани $C$ (на расстоянии $s_2$ ниже по течению от $B$). Найдите наибольшее возможное значение скорости катера (в км/ч) в стоячей воде, при которой он прибудет к $C$ не раньше плота, если скорость течения равна $4$ км/ч и $s_1:s_2=7:2$.`,
|
||||
answer: '32',
|
||||
sol: R`Пусть $u$ — скорость катера, $s_1=7k$, $s_2=2k$. Время плота $\dfrac{2k}{4}=\dfrac{k}{2}$, время катера $\dfrac{7k}{u-4}+\dfrac{9k}{u+4}$. Наибольшему $u$ отвечает равенство $\dfrac{7}{u-4}+\dfrac{9}{u+4}=\dfrac12$, откуда $u^{2}-32u=0$, $u=32$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`В правильной треугольной пирамиде $SABC$ с вершиной $S$ проведена медиана $CM$ треугольника $SBC$ ($M$ — середина $SB$); $BC=2\sqrt7$, $SB=\sqrt{85}$. Через середину $K$ ребра $SC$ проведена прямая $KD$, параллельная ребру $AB$. Через точку $A$ проведена прямая, пересекающая прямые $CM$ и $KD$ в точках $P$ и $T$ соответственно. Найдите увеличенную в $18$ раз длину отрезка $PT$.`,
|
||||
answer: '45',
|
||||
sol: R`Введём координаты основания (сторона $2\sqrt7$) и вершины $S$ (из $SB=\sqrt{85}$ высота $h^{2}=\dfrac{227}{3}$). Прямая через $A$, пересекающая $CM$ и $KD$, определяется условием коллинеарности: $P$ делит $CM$ так, что $\lambda=\dfrac23$, а $T$ лежит на $KD$. Тогда $PT^{2}=\left(\dfrac{2\sqrt7}{3}\right)^{2}+\left(\dfrac{2\sqrt{21}}{9}\right)^{2}+\left(\dfrac{h}{6}\right)^{2}=\dfrac{2025}{324}$, откуда $PT=\dfrac{45}{18}=\dfrac52$. Увеличенная в $18$ раз длина — $45$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2018_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_ct2018_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 → «Варианты» → «ЦТ-2018».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,380 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_ct2019_v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: Централизованное тестирование (ЦТ) по математике, 2019, Вариант 1.
|
||||
Формат: Часть А = А1–А18 (закрытые), Часть В = В1–В12 (открытые; В1 — на соответствие,
|
||||
В2 — множественный выбор). Всего 30 заданий. Перенабрано вручную в KaTeX по PDF:
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\ЦТ-ЦЭ\ЦТ 2019.pdf (10 вариантов, табл. ответов стр.45).
|
||||
|
||||
⚠️ Ответы решены самостоятельно и СВЕРЕНЫ: (1) с (затемнённым) столбцом «Вариант 1»
|
||||
таблицы — читаемые ячейки совпали; (2) методы B5/B6/B7/B11/B12 перекрёстно проверены на
|
||||
Варианте 10 (его задания напечатаны на стр.43–44, ответы читаемы): дали ровно табличные
|
||||
81/56/-1071/624/540 → метод верен. variant=115 (после ЦТ-2018 = 114).
|
||||
|
||||
Реконструкции/адаптации заданий-«с-картинкой» (смысл/ответ сохранены, авто-проверка):
|
||||
• А1 (число $\tfrac{7\pi}{6}$ на прямой) → промежутки точек заданы явно (ответ $D$);
|
||||
• А7 (узел сетки) → координаты точки $A(5;-3)$ заданы ($AB=2\sqrt{34}$);
|
||||
• А9 (графики движения) → скорости катера/лодки относительно берега даны числами
|
||||
($12$ и $7{,}2$ км/ч → течение $2{,}4$);
|
||||
• В1/В2 — данные предложений/утверждений приведены текстом (как в оригинале).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_ct2019_v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_ct2019_v1.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 115;
|
||||
const PROV = 'ЦТ–2019, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А18 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`На координатной прямой отмечены точки (слева направо) $F,A,B,D,C$, лежащие в единичных промежутках: $F\in(0;1)$, $A\in(1;2)$, $B\in(2;3)$, $D\in(3;4)$, $C\in(4;5)$. Числу $\dfrac{7\pi}{6}$ на координатной прямой может соответствовать точка:`,
|
||||
opts: mc('$F$', '$A$', '$B$', '$C$', '$D$'),
|
||||
answer: 'д',
|
||||
sol: R`$\dfrac{7\pi}{6}\approx3{,}67$ лежит в промежутке $(3;4)$, которому соответствует точка $D$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Укажите номер системы неравенств, равносильной системе $\begin{cases}x>3,\\ x\le5.\end{cases}$<br>$1)\ \begin{cases}x-2>1,\\ x+1\le6;\end{cases}$ $\ 2)\ \begin{cases}2x>3,\\ x\le5;\end{cases}$ $\ 3)\ \begin{cases}x>3,\\ x+2\le3;\end{cases}$ $\ 4)\ \begin{cases}x+1>2,\\ x\le5;\end{cases}$ $\ 5)\ \begin{cases}x>3,\\ -x\le5.\end{cases}$`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'а',
|
||||
sol: R`В системе 1: $x-2>1\Rightarrow x>3$ и $x+1\le6\Rightarrow x\le5$ — это и есть данная система.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Укажите номер верного утверждения.<br>$1)\ 11^{16}=121^{4}$; $\ 2)\ -\dfrac37>-\dfrac47$; $\ 3)\ \sqrt{79}>9$; $\ 4)\ 0{,}72<0{,}702$; $\ 5)\ 6^{1/5}=6^{-5}$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'б',
|
||||
sol: R`$-\dfrac37>-\dfrac47$ — верно (утверждение 2). Остальные неверны: $121^{4}=11^{8}\ne11^{16}$; $\sqrt{79}<9$; $0{,}72>0{,}702$; $6^{1/5}\ne6^{-5}$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'trigonometry', subtopic: 'trig-circle', diff: 1,
|
||||
text: R`Найдите градусную меру угла, смежного с углом, радианная мера которого равна $\dfrac{11\pi}{15}$.`,
|
||||
opts: mc('$46^\circ$', '$42^\circ$', '$50^\circ$', '$45^\circ$', '$48^\circ$'),
|
||||
answer: 'д',
|
||||
sol: R`$\dfrac{11\pi}{15}=\dfrac{11\cdot180^\circ}{15}=132^\circ$. Смежный угол равен $180^\circ-132^\circ=48^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Результат разложения многочлена $cx+cy-(x+y)^{2}$ на множители имеет вид:`,
|
||||
opts: mc('$(x+y)(2c-x+y)$', '$(x+y)(c-x+y)$', '$(x+y)(c-x-y)$', '$(x+y)(c-2)$', '$(x+y)(c-1)$'),
|
||||
answer: 'в',
|
||||
sol: R`$cx+cy-(x+y)^{2}=c(x+y)-(x+y)^{2}=(x+y)\bigl(c-(x+y)\bigr)=(x+y)(c-x-y)$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2' },
|
||||
|
||||
{ idx: 6, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Окружность задана уравнением $(x-3)^{2}+(y+4)^{2}=14$. Укажите номер верного утверждения.<br>$1)$ точка $A(-4;3)$ лежит на окружности;<br>$2)$ центром окружности является точка $O(-3;4)$;<br>$3)$ диаметр окружности равен $14$;<br>$4)$ прямая $y=2x-10$ проходит через центр окружности;<br>$5)$ радиус окружности равен $7$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'г',
|
||||
sol: R`Центр окружности $(3;-4)$. Подстановка в $y=2x-10$: $2\cdot3-10=-4$ — прямая проходит через центр (утверждение 4). Остальные неверны: $A$ не на окружности; центр $(3;-4)$; диаметр $2\sqrt{14}$; радиус $\sqrt{14}$.`,
|
||||
ref: 'Геометрия, 8 класс, разд. 5' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Точка $A(5;-3)$ — узел координатной сетки. Точка $B$ симметрична точке $A$ относительно начала координат. Найдите длину отрезка $AB$.`,
|
||||
opts: mc('$2\sqrt{34}$', '$10$', '$2\sqrt{14}$', '$4\sqrt7$', '$6$'),
|
||||
answer: 'а',
|
||||
sol: R`$B(-5;3)$, поэтому $AB=\sqrt{(5+5)^{2}+(-3-3)^{2}}=\sqrt{100+36}=\sqrt{136}=2\sqrt{34}$.`,
|
||||
ref: 'Математика, 6 класс, гл. 7' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`Через точку $A$ к окружности с центром $O$ проведены две касательные $AB$ и $AC$ ($B,C$ — точки касания). Найдите градусную меру угла $BAC$, если $\angle OBC=33^\circ$.`,
|
||||
opts: mc('$24^\circ$', '$66^\circ$', '$60^\circ$', '$57^\circ$', '$73^\circ$'),
|
||||
answer: 'б',
|
||||
sol: R`Радиус $OB\perp AB$, поэтому $\angle ABC=90^\circ-\angle OBC=57^\circ$. Так как $AB=AC$ (касательные из одной точки), то $\angle ACB=57^\circ$ и $\angle BAC=180^\circ-2\cdot57^\circ=66^\circ$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Катер плывёт по течению реки, а моторная лодка — против течения; их собственные скорости равны. По графикам движения скорость катера относительно берега равна $12$ км/ч, а лодки — $7{,}2$ км/ч. Найдите скорость течения реки.`,
|
||||
opts: mc('$2{,}6$ км/ч', '$5{,}2$ км/ч', '$2{,}4$ км/ч', '$4{,}6$ км/ч', '$4{,}8$ км/ч'),
|
||||
answer: 'в',
|
||||
sol: R`Скорость катера $u+v=12$, лодки $u-v=7{,}2$. Вычитая, $2v=4{,}8$, $v=2{,}4$ км/ч.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'equations', subtopic: 'eq-quadratic', diff: 2,
|
||||
text: R`Пусть $x_1$ и $x_2$ — корни уравнения $x^{2}-3x+q=0$. Найдите число $q$, при котором выполняется равенство $x_1^{2}+x_2^{2}=25$.`,
|
||||
opts: mc('$-8$', '$-3$', '$8$', '$3$', '$-5$'),
|
||||
answer: 'а',
|
||||
sol: R`По теореме Виета $x_1+x_2=3$, $x_1x_2=q$. Тогда $x_1^{2}+x_2^{2}=(x_1+x_2)^{2}-2x_1x_2=9-2q=25$, откуда $q=-8$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Сумма первых четырёх членов геометрической прогрессии равна $60$, знаменатель прогрессии равен $2$. Найдите второй член прогрессии.`,
|
||||
opts: mc('$5$', '$16$', '$6$', '$4$', '$8$'),
|
||||
answer: 'д',
|
||||
sol: R`$S_4=b_1\cdot\dfrac{2^{4}-1}{2-1}=15b_1=60$, откуда $b_1=4$. Второй член $b_2=b_1\cdot2=8$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4' },
|
||||
|
||||
{ idx: 12, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В треугольнике $ABC$ $\angle ACB=90^\circ$, $AB=8$, $\operatorname{ctg}\angle BAC=\sqrt{15}$. Найдите длину стороны $CB$.`,
|
||||
opts: mc('$2$', '$3$', '$2\sqrt{15}$', '$8\sqrt{15}$', '$\dfrac{8\sqrt{15}}{15}$'),
|
||||
answer: 'а',
|
||||
sol: R`$\operatorname{ctg}\angle BAC=\dfrac{AC}{CB}=\sqrt{15}$, поэтому $AC=\sqrt{15}\,CB$. Из $AC^{2}+CB^{2}=AB^{2}$: $15CB^{2}+CB^{2}=64$, $CB^{2}=4$, $CB=2$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 13, type: 'mc', topic: 'equations', subtopic: 'eq-quadratic', diff: 2,
|
||||
text: R`Укажите номера уравнений, которые не имеют действительных корней.<br>$1)\ x^{2}=49$; $\ 2)\ \dfrac{1}{x^{2}-49}=0$; $\ 3)\ x^{2}+49=0$; $\ 4)\ x^{2}+49x=0$; $\ 5)\ x^{2}+x-49=0$.`,
|
||||
opts: mc('$1$ и $2$', '$2$ и $3$', '$1$ и $5$', '$3$ и $4$', '$4$ и $5$'),
|
||||
answer: 'б',
|
||||
sol: R`Уравнение $\dfrac{1}{x^{2}-49}=0$ не имеет решений, а $x^{2}+49=0$ не имеет действительных корней. Остальные корни имеют. Значит уравнения $2$ и $3$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2' },
|
||||
|
||||
{ idx: 14, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`В ботаническом саду разбили клумбу треугольной формы. Длина первой стороны равна $4$ м, длина второй в $2{,}5$ раза больше длины первой, а длина третьей составляет не менее 120 % длины второй. Какому условию должен удовлетворять периметр $P$ (в метрах) этой клумбы?`,
|
||||
opts: mc('$26<P\le28$', '$P\le28$', '$26\le P<28$', '$P>26$', '$26\le P\le28$'),
|
||||
answer: 'в',
|
||||
sol: R`Первая сторона $4$ м, вторая $10$ м, третья $c\ge12$ м. По неравенству треугольника $c<4+10=14$. Значит $12\le c<14$, и $P=14+c$, то есть $26\le P<28$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 15, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Найдите сумму всех натуральных чисел $n$, для которых выполняется равенство НОК$(n;63)=63$.`,
|
||||
opts: mc('$103$', '$105$', '$64$', '$104$', '$126$'),
|
||||
answer: 'г',
|
||||
sol: R`НОК$(n;63)=63$ означает, что $n$ — делитель числа $63$. Делители: $1,3,7,9,21,63$; их сумма $104$.`,
|
||||
ref: 'Математика, 6 класс, гл. 1' },
|
||||
|
||||
{ idx: 16, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Секущая плоскость пересекает сферу по окружности, радиус которой равен $2$. Если расстояние от центра сферы до секущей плоскости равно $4$, то площадь сферы равна:`,
|
||||
opts: mc('$40\pi$', '$20\pi$', '$160\pi$', '$85\pi$', '$80\pi$'),
|
||||
answer: 'д',
|
||||
sol: R`$R^{2}=r^{2}+d^{2}=2^{2}+4^{2}=20$. Площадь сферы $4\pi R^{2}=80\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3' },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Сумма наибольшего отрицательного и наименьшего положительного корней уравнения $\cos(3\pi x)\cdot\cos\left(3\pi x+\dfrac{\pi}{2}\right)=\dfrac12$ равна:`,
|
||||
opts: mc('$\dfrac{1}{2}$', '$\dfrac{7}{12}$', '$\dfrac{1}{6}$', '$-\dfrac{1}{12}$', '$\dfrac14$'),
|
||||
answer: 'в',
|
||||
sol: R`$\cos\left(3\pi x+\dfrac{\pi}{2}\right)=-\sin(3\pi x)$, поэтому $-\sin(3\pi x)\cos(3\pi x)=\dfrac12$, то есть $\sin(6\pi x)=-1$, $x=-\dfrac{1}{12}+\dfrac{k}{3}$. Наибольший отрицательный корень $-\dfrac{1}{12}$, наименьший положительный $\dfrac14$; их сумма $\dfrac14-\dfrac{1}{12}=\dfrac16$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, все рёбра которой равны $24\sqrt3$. Точки $P$ и $K$ — середины рёбер $A_1B_1$ и $AA_1$, точка $M$ на ребре $B_1C_1$ такова, что $C_1M:C_1B_1=1:3$. Найдите длину отрезка, по которому плоскость, проходящая через $M,P,K$, пересекает грань $BB_1C_1C$.`,
|
||||
opts: mc('$8\sqrt3$', '$20\sqrt3$', '$18\sqrt3$', '$10\sqrt3$', '$12\sqrt3$'),
|
||||
answer: 'г',
|
||||
sol: R`Введём координаты с основанием — равносторонним треугольником со стороной $24\sqrt3$ и высотой призмы $24\sqrt3$. Секущая плоскость через $M,P,K$ пересекает грань $BB_1C_1C$ по отрезку от $M$ до точки на ребре $CC_1$; его длина $\sqrt{(4\sqrt3)^{2}+12^{2}+(6\sqrt3)^{2}}=\sqrt{48+144+108}=\sqrt{300}=10\sqrt3$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 4' },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 19, type: 'long', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание $1$–$5$ так, чтобы получилось верное утверждение.<br>А) Значение выражения $2^{-8}:2^{0}$ равно …<br>Б) Значение выражения $(-2)^{-11}\cdot8$ равно …<br>В) Значение выражения $20^{4}:(-5)^{4}$ равно …<br>Окончания: $1)\;256$; $\ 2)\;-256$; $\ 3)\;-\dfrac{1}{256}$; $\ 4)\;\dfrac{1}{256}$; $\ 5)\;32$.`,
|
||||
answer: 'А4Б3В1',
|
||||
ansShow: 'А4Б3В1',
|
||||
sol: R`А) $2^{-8}:2^{0}=2^{-8}=\dfrac{1}{256}$ (окончание 4). Б) $(-2)^{-11}\cdot8=-\dfrac{1}{2048}\cdot8=-\dfrac{1}{256}$ (окончание 3). В) $20^{4}:(-5)^{4}=\left(\dfrac{20}{5}\right)^{4}=4^{4}=256$ (окончание 1). Ответ: А4Б3В1.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 3,
|
||||
text: R`Прямая $a$ перпендикулярна плоскости $\alpha$ и пересекает её в точке $O$. Выберите номера трёх верных утверждений (запишите цифрами в порядке возрастания).<br>$1)$ любая прямая, перпендикулярная плоскости $\alpha$, параллельна прямой $a$;<br>$2)$ любая прямая, перпендикулярная прямой $a$, лежит в плоскости $\alpha$;<br>$3)$ прямая $a$ перпендикулярна любой прямой плоскости $\alpha$;<br>$4)$ через прямую $a$ проходит единственная плоскость, перпендикулярная плоскости $\alpha$;<br>$5)$ существует множество плоскостей, перпендикулярных прямой $a$;<br>$6)$ существует единственная прямая, параллельная прямой $a$ и перпендикулярная плоскости $\alpha$.`,
|
||||
answer: '135',
|
||||
sol: R`Верны утверждения $1$ (все прямые, перпендикулярные $\alpha$, параллельны между собой), $3$ ($a\perp\alpha$ означает перпендикулярность любой прямой плоскости) и $5$ (плоскостей, перпендикулярных $a$, бесконечно много). Утверждения $2,4,6$ неверны.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 2' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`В двух сосудах содержится $57$ л жидкости. Если 5 % жидкости из первого сосуда перелить во второй, то в обоих сосудах окажется одинаковое количество жидкости. Сколько литров жидкости было во втором сосуде первоначально?`,
|
||||
answer: '27',
|
||||
sol: R`Пусть в первом сосуде $a$ л, во втором $b$ л, $a+b=57$. После переливания: $0{,}95a=b+0{,}05a$, то есть $0{,}9a=b$. Тогда $a+0{,}9a=57$, $a=30$, $b=27$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму корней (корень, если он единственный) уравнения $\sqrt{x^{2}-9x+8}-\sqrt{23-11x}=0$.`,
|
||||
answer: '-5',
|
||||
sol: R`$\sqrt{x^{2}-9x+8}=\sqrt{23-11x}$, поэтому $x^{2}-9x+8=23-11x$, $x^{2}+2x-15=0$, $x=3$ или $x=-5$. ОДЗ ($x\le\dfrac{23}{11}$ и $x^{2}-9x+8\ge0$) удовлетворяет лишь $x=-5$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`В трапеции $ABCD$ с основаниями $AD>BC$ точка пересечения её диагоналей делит диагональ $AC$ на отрезки длиной $6$ и $4$. Найдите площадь трапеции $ABCD$, если площадь треугольника $ABC$ равна $20$.`,
|
||||
answer: '50',
|
||||
sol: R`Из подобия $\dfrac{AD}{BC}=\dfrac{AO}{OC}=\dfrac64=\dfrac32$. Площадь $ABC=\dfrac12\,BC\cdot h=20$ ($h$ — высота трапеции), значит $BC\cdot h=40$. Площадь трапеции $\dfrac12(AD+BC)h=\dfrac12\left(\dfrac32 BC+BC\right)h=\dfrac54\,BC\cdot h=\dfrac54\cdot40=50$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 4' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите произведение наибольшего целого решения на количество всех целых решений неравенства $\dfrac{x^{2}-x-20}{(x^{2}+4x)^{2}}\le0$.`,
|
||||
answer: '40',
|
||||
sol: R`После сокращения $\dfrac{(x-5)(x+4)}{x^{2}(x+4)^{2}}=\dfrac{x-5}{x^{2}(x+4)}\le0$. Так как $x^{2}>0$, знак определяет $\dfrac{x-5}{x+4}\le0$, то есть $-4<x\le5$, $x\ne0$. Целые решения $-3,-2,-1,1,2,3,4,5$ (всего $8$); наибольшее $5$. Произведение $5\cdot8=40$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 5,
|
||||
text: R`Функция $y=f(x)$ определена на $\mathbb{R}$, нечётна, периодична с периодом $T=10$ и при $x\in[0;5]$ задаётся формулой $f(x)=3x^{2}-15x$. Найдите произведение абсцисс точек пересечения прямой $y=12$ и графика функции на промежутке $[-13;7]$.`,
|
||||
answer: '-264',
|
||||
sol: R`На $[0;5]$ значения $f$ не больше $0$, а на $[-5;0]$ (по нечётности $f(x)=-3x^{2}-15x$) уравнение $f(x)=12$ даёт $x=-1$ и $x=-4$. С учётом периода $10$ на $[-13;7]$ корни: $-11,-4,-1,6$. Произведение $(-11)(-4)(-1)\cdot6=-264$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`В основании пирамиды лежит прямоугольный треугольник, длина гипотенузы которого равна $6$, острый угол равен $30^\circ$. Каждая боковая грань пирамиды наклонена к плоскости основания под углом $\arccos\dfrac{\sqrt3}{10}$. Найдите площадь боковой поверхности пирамиды.`,
|
||||
answer: '45',
|
||||
sol: R`Катеты основания равны $3$ и $3\sqrt3$, площадь основания $S=\dfrac12\cdot3\cdot3\sqrt3=\dfrac{9\sqrt3}{2}$. При равном наклоне всех боковых граней площадь боковой поверхности равна $\dfrac{S}{\cos\varphi}=\dfrac{9\sqrt3/2}{\sqrt3/10}=45$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите увеличенную в $3$ раза сумму квадратов корней уравнения $\sqrt[5]{5^{\,2x^{2}+3x-5}}-\left(\sqrt{6-2\sqrt5}+1\right)^{2x}=0$.`,
|
||||
answer: '18',
|
||||
sol: R`$\sqrt{6-2\sqrt5}=\sqrt{(\sqrt5-1)^{2}}=\sqrt5-1$, поэтому $\left(\sqrt5-1+1\right)^{2x}=5^{x}$. Уравнение принимает вид $5^{(2x^{2}+3x-5)/5}=5^{x}$, то есть $2x^{2}-2x-5=0$. Сумма квадратов корней $(x_1+x_2)^{2}-2x_1x_2=1+5=6$; увеличенная в $3$ раза — $18$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 4,
|
||||
text: R`Найдите сумму всех целых чисел из области определения функции $y=\dfrac{\sqrt[4]{56+9x-2x^{2}}}{\log_{\sqrt7}(x-3)}$.`,
|
||||
answer: '26',
|
||||
sol: R`Под корнем $56+9x-2x^{2}\ge0$: $-3{,}5\le x\le8$. Знаменатель: $x-3>0$ и $\log_{\sqrt7}(x-3)\ne0$, то есть $x>3$, $x\ne4$. Итого $x\in(3;8]$, $x\ne4$. Целые $5,6,7,8$; их сумма $26$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 5,
|
||||
text: R`Двое рабочих различной квалификации выполнили работу. Первый проработал $3$ ч, прежде чем к нему присоединился второй. Если бы сначала второй работал $3$ ч, а затем к нему присоединился первый, то работа была бы закончена на $36$ мин позже. Первый рабочий шестую часть работы выполняет на $2$ ч быстрее, чем второй выполняет третью часть. Сколько минут заняло выполнение всей работы?`,
|
||||
answer: '288',
|
||||
sol: R`Пусть производительности $r_1,r_2$. Разность сценариев $\dfrac{3(r_1-r_2)}{r_1+r_2}=0{,}6$ даёт $r_1=1{,}5r_2$; условие $\dfrac{1/3}{r_2}-\dfrac{1/6}{r_1}=2$ даёт $r_2=\dfrac19$, $r_1=\dfrac16$. Тогда первый за $3$ ч выполнит половину, остальное вместе за $\dfrac{1/2}{5/18}=1{,}8$ ч; всего $4{,}8$ ч $=288$ мин.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
|
||||
text: R`Прямоугольный треугольник, длина гипотенузы которого равна $10$, а высота, проведённая к ней, равна $3$, вращается вокруг прямой, перпендикулярной гипотенузе и проходящей в плоскости треугольника через вершину большего острого угла. Найдите объём $V$ тела вращения и в ответ запишите значение выражения $\dfrac{V}{\pi}$.`,
|
||||
answer: '110',
|
||||
sol: R`Высота делит гипотенузу на отрезки $1$ и $9$ ($AD\cdot DB=3^{2}$). Больший острый угол — у вершины при меньшем отрезке. В координатах $A(0;0)$, $C(1;3)$, $B(10;0)$; ось вращения — прямая $x=0$ через $A$. По теореме Гульдина $V=2\pi\,x_{c}\,S$, где $x_{c}=\dfrac{0+1+10}{3}=\dfrac{11}{3}$, $S=15$. Тогда $V=2\pi\cdot\dfrac{11}{3}\cdot15=110\pi$, и $\dfrac{V}{\pi}=110$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_ct2019_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_ct2019_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 → «Варианты» → «ЦТ-2019».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -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$ укажите номера верных утверждений.<br>$1)$ число $0$ не является решением неравенства;<br>$2)$ неравенство равносильно неравенству $|x|\le8$;<br>$3)$ количество всех целых решений неравенства равно $12$;<br>$4)$ неравенство верно при $x\in[-2;3]$;<br>$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<OB$. Составьте формулу для радиуса $r$ окружности, проходящей через точки $A$, $B$ и касающейся другой стороны угла.`,
|
||||
opts: mc('$r=\dfrac{a+1{,}7}{2}$', '$r=\dfrac{a-1{,}7}{2}$', '$r=a+1{,}7$', '$r=\dfrac{a+3{,}4}{2}$', '$r=2a-1{,}7$'),
|
||||
answer: 'а',
|
||||
sol: R`Проекция центра окружности на сторону с точками $A,B$ — середина $AB$, на расстоянии $\dfrac{1{,}7+a}{2}$ от $O$. Касание другой стороны прямого угла даёт радиус, равный этому расстоянию: $r=\dfrac{a+1{,}7}{2}$.` },
|
||||
|
||||
{ idx: 17, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 3,
|
||||
text: R`Число $A=5{,}43$ является результатом округления числа $B$ до сотых. Если $|A-B|=5\cdot10^{-3}$, то число $B$ равно:`,
|
||||
opts: mc('$5{,}48$', '$5{,}4295$', '$5{,}425$', '$5{,}435$', '$5{,}4305$'),
|
||||
answer: 'в',
|
||||
sol: R`$|A-B|=0{,}005$, поэтому $B=5{,}425$ или $B=5{,}435$. До сотых к $5{,}43$ округляется только $5{,}425$ (число $5{,}435$ округлилось бы до $5{,}44$).` },
|
||||
|
||||
{ idx: 18, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Высота цилиндра в $3$ раза больше радиуса его основания. Найдите объём цилиндра, если радиус основания равен $\sqrt6$.`,
|
||||
opts: mc('$6\sqrt6\,\pi$', '$54\sqrt6\,\pi$', '$9\sqrt6\,\pi$', '$18\pi$', '$18\sqrt6\,\pi$'),
|
||||
answer: 'д',
|
||||
sol: R`$r=\sqrt6$, $h=3\sqrt6$. Объём $V=\pi r^{2}h=\pi\cdot6\cdot3\sqrt6=18\sqrt6\,\pi$.` },
|
||||
|
||||
{ idx: 19, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Найдите произведение наименьшего целого решения на количество всех целых решений неравенства $\left|x^{2}+9x\right|\le10$.`,
|
||||
opts: mc('$90$', '$-54$', '$60$', '$-60$', '$-90$'),
|
||||
answer: 'г',
|
||||
sol: R`$-10\le x^{2}+9x\le10$. Правое неравенство даёт $-10\le x\le1$; левое — $x\le\dfrac{-9-\sqrt{41}}{2}$ или $x\ge\dfrac{-9+\sqrt{41}}{2}$. Решение $[-10;-7{,}7\ldots]\cup[-1{,}3\ldots;1]$. Целые решения $-10,-9,-8,-1,0,1$ (всего $6$); наименьшее $-10$. Произведение $-10\cdot6=-60$.` },
|
||||
|
||||
{ idx: 20, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`$SABCD$ — правильная четырёхугольная пирамида, все рёбра которой равны $37$. Точка $M$ — середина ребра $SA$, точка $N$ на ребре $SD$ такова, что $DN:NS=1:3$. Найдите длину отрезка, по которому плоскость, проходящая через точки $N$, $M$, $B$, пересекает основание $ABCD$.`,
|
||||
opts: mc('$\dfrac{37\sqrt{13}}{3}$', '$46\tfrac14$', '$\dfrac{37\sqrt{10}}{3}$', '$\dfrac{37\sqrt{17}}{4}$', '$\dfrac{37\sqrt5}{2}$'),
|
||||
answer: 'а',
|
||||
sol: R`В координатах с центром основания (рёбра единичные) секущая плоскость пересекает основание по прямой $3x+2y=18{,}5$. Внутри квадрата отрезок идёт от вершины $B$ до точки на стороне $DC$; его длина равна $\dfrac{37\sqrt{13}}{3}$.` },
|
||||
|
||||
// ── Часть B: В1–В12 ──────────────────────────────────────────────────────
|
||||
{ idx: 21, type: 'long', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 3,
|
||||
text: R`Дана арифметическая прогрессия $(a_n)$, у которой $a_9-a_5=12$, $a_{10}=14$. Для начала каждого из предложений А–В подберите его окончание $1$–$6$.<br>А) Разность этой прогрессии равна …<br>Б) Первый член этой прогрессии равен …<br>В) Сумма первых восьми членов этой прогрессии равна …<br>Окончания: $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$ (запишите цифрами в порядке возрастания).<br>$1)$ $\sin(\alpha+23^\circ)=0$;<br>$2)$ $\operatorname{tg}\alpha>0$;<br>$3)$ $\operatorname{ctg}\alpha<0$;<br>$4)$ $\alpha$ — угол первой четверти;<br>$5)$ $\sin^{2}\alpha+\cos^{2}\alpha=1$;<br>$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}$ или $0<x<\frac{-3+5\sqrt5}{2}$. Наибольшее целое отрицательное $-8$, наибольшее целое положительное $4$; произведение $-8\cdot4=-32$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 5,
|
||||
text: R`Найдите произведение корней (корень, если он единственный) уравнения $\sqrt[4]{x^{2}+3x-40}\cdot\sqrt[3]{x^{2}-3x-40}=0$.`,
|
||||
answer: '-320',
|
||||
sol: R`ОДЗ: $x^{2}+3x-40\ge0$. Из $\sqrt[4]{x^{2}+3x-40}=0$: $x=-8$ или $x=5$. Из $\sqrt[3]{x^{2}-3x-40}=0$: $x=8$ или $x=-5$, но $x=-5$ не входит в ОДЗ. Корни $-8,5,8$; произведение $-8\cdot5\cdot8=-320$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, у которой $AB=5$, $AA_1=5$. Точки $P$ и $Q$ — середины рёбер $AB$ и $A_1C_1$ соответственно. Найдите значение выражения $\dfrac{36}{\cos^{2}\varphi}$, где $\varphi$ — угол между прямыми $PQ$ и $AB_1$.`,
|
||||
answer: '160',
|
||||
sol: R`В координатах $\vec{PQ}=\left(-1{,}25;\tfrac{5\sqrt3}{4};5\right)$, $\vec{AB_1}=(5;0;5)$. Тогда $\cos^{2}\varphi=\dfrac{(\vec{PQ}\cdot\vec{AB_1})^{2}}{|\vec{PQ}|^{2}\,|\vec{AB_1}|^{2}}=\dfrac{18{,}75^{2}}{31{,}25\cdot50}=0{,}225$, и $\dfrac{36}{\cos^{2}\varphi}=160$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 5,
|
||||
text: R`Найдите сумму квадратов корней (корень, если он единственный) уравнения $\log_{18}(17-x)^{2}=2-2\log_{18}x$.`,
|
||||
answer: '577',
|
||||
sol: R`ОДЗ: $x>0$, $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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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();
|
||||
@@ -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<a<\dfrac38$ имеет вид:`,
|
||||
opts: mc('$-6$', '$2a+6$', '$-2a-6$', '$6-2a$', '$6$'),
|
||||
answer: 'г',
|
||||
sol: R`При $0<a<6$: $|a|=a$, $|a-6|=6-a$. Тогда $|a-6|-|a|=(6-a)-a=6-2a$.` },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Значение выражения $\log_7 98-\log_7 8+\log_7\dfrac47$ равно:`,
|
||||
opts: mc('$1$', '$2$', '$\log_7 2$', '$0$', '$3$'),
|
||||
answer: 'а',
|
||||
sol: R`$\log_7\dfrac{98\cdot4}{8\cdot7}=\log_7\dfrac{392}{56}=\log_7 7=1$.` },
|
||||
|
||||
{ idx: 10, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`В первый день велосипедист проехал $52$ км, а во второй день — на 15 % меньше, чем в первый. Сколько километров проехал велосипедист за два дня?`,
|
||||
opts: mc('$102{,}4$', '$96{,}2$', '$89$', '$88{,}4$', '$98{,}2$'),
|
||||
answer: 'б',
|
||||
sol: R`Второй день: $52\cdot0{,}85=44{,}2$ км. Всего $52+44{,}2=96{,}2$ км.` },
|
||||
|
||||
{ idx: 11, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Найдите произведение координат точки пересечения прямых $6x-y=4$ и $y-18=0$.`,
|
||||
opts: mc('$4$', '$18$', '$72$', '$78$', '$66$'),
|
||||
answer: 'д',
|
||||
sol: R`$y=18$, $6x-18=4$, $x=\dfrac{11}{3}$. Произведение $\dfrac{11}{3}\cdot18=66$.` },
|
||||
|
||||
{ idx: 12, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Укажите номера функций, которые являются чётными (запишите цифрами в порядке возрастания).<br>$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$.<br>$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`Укажите номера уравнений, которые являются равносильными (запишите цифрами в порядке возрастания).<br>$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$.<br>$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$.<br>А) В какой день количество посещений было на $20$ больше, чем в предыдущий?<br>Б) В какой день количество посещений было на 35 % меньше, чем во вторник?<br>В) В какой день количество посещений было на 10 % больше, чем в предыдущий?<br>Окончания: 1) вторник; 2) среда; 3) четверг; 4) пятница; 5) суббота; 6) воскресенье.`,
|
||||
fig: R`<table class="task-fig" style="border-collapse:collapse;margin:6px 0"><tr><th ${TD}>День</th><th ${TD}>вт</th><th ${TD}>ср</th><th ${TD}>чт</th><th ${TD}>пт</th><th ${TD}>сб</th><th ${TD}>вс</th></tr><tr><td ${TD}>Посещений</td><td ${TD}>400</td><td ${TD}>440</td><td ${TD}>260</td><td ${TD}>300</td><td ${TD}>640</td><td ${TD}>660</td></tr></table>`,
|
||||
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`Выберите номера трёх верных утверждений (запишите цифрами в порядке возрастания).<br>$1)$ если $\cos(\arccos a)=\cos\left(\arccos\tfrac1{18}\right)$, то $a=\tfrac1{18}$;<br>$2)$ если $\cos\alpha=-\cos\tfrac{\pi}{18}$, то $\arccos(\cos\alpha)=-\tfrac{\pi}{18}$;<br>$3)$ если $\sin\alpha=\sin\tfrac{17\pi}{18}$, то $\arcsin(\sin\alpha)=\tfrac{17\pi}{18}$;<br>$4)$ если $\arccos a=\tfrac{\pi}{18}$, то $a=\cos\tfrac{\pi}{18}$;<br>$5)$ если $\sin\alpha=\sin\tfrac{\pi}{18}$, то $\alpha=-\tfrac{\pi}{18}$;<br>$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$. Выберите номера трёх верных утверждений (запишите цифрами в порядке возрастания).<br>$1)$ любая прямая, проходящая через $A$ и пересекающая $\alpha$, пересекает прямую $a$;<br>$2)$ существует единственная прямая, проходящая через $A$ и перпендикулярная плоскости $\alpha$;<br>$3)$ прямая, проходящая через $A$ и перпендикулярная $\beta$, перпендикулярна $\alpha$;<br>$4)$ любая точка прямой $a$ лежит в плоскостях $\alpha$ и $\beta$;<br>$5)$ любая прямая, лежащая в $\alpha$ и перпендикулярная прямой $a$, перпендикулярна $\beta$;<br>$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{,}1<x\le-6{,}59\ldots$. Целые решения $-8$ и $-7$; их сумма $-15$.` },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 5,
|
||||
text: R`$AC$ — общая гипотенуза прямоугольных треугольников $ABC$ и $ADC$. Плоскости этих треугольников взаимно перпендикулярны. Найдите квадрат длины отрезка $BD$, если $AB=9\sqrt3$, $BC=9\sqrt5$, $AD=DC$.`,
|
||||
answer: '324',
|
||||
sol: R`$AC^{2}=AB^{2}+BC^{2}=243+405=648$. В равнобедренном прямоугольном треугольнике $ADC$: $AD=DC=18$. Введя координаты (плоскости $ABC$ и $ADC$ перпендикулярны и содержат $AC$), получаем $BD^{2}=324$.` },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`Числовая последовательность $(a_n)$ задана формулой $n$-го члена $a_n=2n^{2}-15n$. Найдите наименьший член $a_m$ этой последовательности и его номер $m$. В ответ запишите значение выражения $m\cdot a_m$.`,
|
||||
answer: '-112',
|
||||
sol: R`Вершина параболы $a_n=2n^{2}-15n$ при $n=3{,}75$, поэтому минимум среди целых при $n=4$: $a_4=32-60=-28$. Значит $m=4$, $a_m=-28$, и $m\cdot a_m=-112$.` },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 5,
|
||||
text: R`Найдите увеличенную в $25$ раз сумму квадратов корней уравнения $10\sqrt{\dfrac{x^{2}}{14+5x-x^{2}}}-2\sqrt{\dfrac{14+5x-x^{2}}{x^{2}}}=19$.`,
|
||||
answer: '960',
|
||||
sol: R`Пусть $t=\sqrt{\dfrac{x^{2}}{14+5x-x^{2}}}\ge0$. Тогда $10t-\dfrac2t=19$, $10t^{2}-19t-2=0$, $t=2$. Отсюда $x^{2}=4(14+5x-x^{2})$, то есть $5x^{2}-20x-56=0$. Сумма квадратов корней $4^{2}-2\cdot\left(-\tfrac{56}{5}\right)=\tfrac{192}{5}$; увеличенная в $25$ раз — $960$.` },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 5,
|
||||
text: R`Прямая, проходящая через вершину $K$ треугольника $KMN$, делит его медиану $MA$ в отношении $8:3$, считая от вершины $M$, и пересекает сторону $MN$ в точке $B$. Найдите площадь треугольника $KMN$, если площадь треугольника $KMB$ равна $16$.`,
|
||||
answer: '28',
|
||||
sol: R`В координатах $M(0;0)$, $N(1;0)$, $K(0;1)$ прямая $KB$ пересекает $MN$ в точке $B\left(\tfrac47;0\right)$. Тогда $\dfrac{[KMN]}{[KMB]}=\dfrac{1/2}{2/7}=\dfrac74$, поэтому $[KMN]=16\cdot\dfrac74=28$.` },
|
||||
|
||||
{ idx: 31, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 5,
|
||||
text: R`Петя записал на доске два различных натуральных числа. Затем он их сложил, перемножил, вычел из большего записанного числа меньшее и разделил большее на меньшее. Сложив четыре полученных результата, Петя получил число $1521$. Найдите все такие пары натуральных чисел. В ответ запишите их сумму.`,
|
||||
answer: '460',
|
||||
sol: R`Для чисел $a>b$ (с $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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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();
|
||||
@@ -0,0 +1,93 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Входная диагностика для курса «ЦЭ/ЦТ — Математика».
|
||||
* Собирает ОДИН test из РЕАЛЬНЫХ размеченных вопросов ЦТ-11 (banks 2011–2024):
|
||||
* по 1 заданию на ключевую тему, смесь уровней (single 🟢 → fill-blank 🔴).
|
||||
* Новых вопросов НЕ авторит — только группирует существующие.
|
||||
* ИДЕМПОТЕНТЕН: если test с таким title есть — не дублирует.
|
||||
* Запуск: node backend/scripts/seed_ctmath_diagnostic.js (применить)
|
||||
* node backend/scripts/seed_ctmath_diagnostic.js --dry (показать выбор)
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const DRY = process.argv.includes('--dry');
|
||||
const MATH_ID = 3;
|
||||
const TITLE = 'Диагностика ЦЭ/ЦТ — Математика';
|
||||
const DESC = 'Входная диагностика: задания по ключевым темам (от базовых до сложных) для определения уровня и приоритетных тем подготовки к ЦЭ/ЦТ.';
|
||||
|
||||
// Слоты: тема (по имени) + предпочтительный тип + уровень-зонд.
|
||||
// Исключаем набор year=2025 («Экзамен 9»): берём только размеченные ЦТ-11 (year<=2024).
|
||||
const SLOTS = [
|
||||
['Теория чисел', 'single', 'base'],
|
||||
['Арифметика и степени', 'single', 'base'],
|
||||
['Квадратные уравнения', 'single', 'base'],
|
||||
['Тригонометрия', 'single', 'base'],
|
||||
['Числовые промежутки', 'single', 'base'],
|
||||
['Словесные задачи', 'fill-blank', 'mid'],
|
||||
['Прогрессии', 'fill-blank', 'mid'],
|
||||
['Функции', 'fill-blank', 'mid'],
|
||||
['Геометрия', 'fill-blank', 'mid'],
|
||||
['Окружность и круг', 'single', 'mid'],
|
||||
['Стереометрия', 'fill-blank', 'mid'],
|
||||
['Логарифмы', 'fill-blank', 'hard'],
|
||||
['Неравенства', 'fill-blank', 'hard'],
|
||||
['Уравнения', 'fill-blank', 'hard'],
|
||||
['Показательные неравенства','fill-blank', 'hard'],
|
||||
];
|
||||
|
||||
function topicId(name) {
|
||||
const r = db.prepare('SELECT id FROM topics WHERE subject_id=? AND LOWER(name)=LOWER(?)').get(MATH_ID, name);
|
||||
return r && r.id;
|
||||
}
|
||||
function adminId() {
|
||||
const u = db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get();
|
||||
return u && u.id;
|
||||
}
|
||||
|
||||
// Кандидаты по теме: сперва предпочт. тип, потом любой; только размеченные ЦТ-11 (year<=2024 или not null),
|
||||
// исключая набор «Экзамен 9» (source_type='экзамен 9'); 2024 в приоритете, затем свежие.
|
||||
function candidates(tid, type) {
|
||||
const order = "ORDER BY (year=2024) DESC, year DESC, id";
|
||||
const base = `SELECT id, type, year, substr(text,1,70) AS t FROM questions
|
||||
WHERE subject_id=${MATH_ID} AND topic_id=${tid}
|
||||
AND (source_type IS NULL OR source_type <> 'экзамен 9')`;
|
||||
const pref = db.prepare(`${base} AND type=? ${order} LIMIT 8`).all(type);
|
||||
const any = db.prepare(`${base} ${order} LIMIT 8`).all();
|
||||
// предпочт. тип впереди, затем остальные (для фолбэка)
|
||||
const seen = new Set(pref.map(r => r.id));
|
||||
return [...pref, ...any.filter(r => !seen.has(r.id))];
|
||||
}
|
||||
|
||||
const used = new Set();
|
||||
const picks = [];
|
||||
for (const [name, type, level] of SLOTS) {
|
||||
const tid = topicId(name);
|
||||
if (!tid) { console.log(` [skip] нет темы: ${name}`); continue; }
|
||||
const cand = candidates(tid, type).find(r => !used.has(r.id));
|
||||
if (!cand) { console.log(` [skip] нет вопросов: ${name}`); continue; }
|
||||
used.add(cand.id);
|
||||
picks.push({ name, level, ...cand });
|
||||
}
|
||||
|
||||
console.log(DRY ? '[DRY-RUN] выбранные вопросы диагностики:' : '[APPLY] диагностика:');
|
||||
const mark = { base: 'базовый', mid: 'средний', hard: 'сложный' };
|
||||
picks.forEach((p, i) => console.log(
|
||||
` ${String(i + 1).padStart(2)}. [${mark[p.level]}] ${p.name} | qid ${p.id} (${p.type}, ${p.year || '—'}) — ${p.t.replace(/\s+/g, ' ')}…`
|
||||
));
|
||||
console.log(`\nВсего отобрано: ${picks.length} заданий.`);
|
||||
|
||||
const existing = db.prepare("SELECT id FROM tests WHERE subject_slug='math' AND title=?").get(TITLE);
|
||||
if (existing) {
|
||||
console.log(`\nТест «${TITLE}» уже существует (id ${existing.id}) — не дублирую.`);
|
||||
} else if (DRY) {
|
||||
console.log(`\nDRY-RUN: тест НЕ создан. Будет создан с ${picks.length} вопросами.`);
|
||||
} else {
|
||||
const by = adminId();
|
||||
const testId = db.prepare(
|
||||
'INSERT INTO tests (title, subject_slug, description, show_answers, time_limit, created_by) VALUES (?,?,?,?,?,?)'
|
||||
).run(TITLE, 'math', DESC, 1, 40, by).lastInsertRowid;
|
||||
const ins = db.prepare('INSERT INTO test_questions (test_id, question_id, order_index) VALUES (?,?,?)');
|
||||
picks.forEach((p, i) => ins.run(testId, p.id, i));
|
||||
console.log(`\nСоздан тест «${TITLE}» (id ${testId}, ${picks.length} вопросов, лимит 40 мин).`);
|
||||
console.log('Выдать классу/ученику: assignment с test_id=' + testId + ' (mode неважен, test_id перекрывает выбор).');
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Конвертер: размеченные вопросы ЦТ-11 из банка `questions` (subject_id=3)
|
||||
* → `exam_tasks` для отдельного модуля exam-prep (exam_key='ctmath').
|
||||
*
|
||||
* По умолчанию DRY (только чтение, печать выборки и статистики).
|
||||
* Запись ТОЛЬКО с флагом --apply (и только если применён трек 077).
|
||||
* node backend/scripts/seed_ctmath_exam_tasks.js # dry: выборка+статистика
|
||||
* node backend/scripts/seed_ctmath_exam_tasks.js --apply # запись
|
||||
*
|
||||
* Правила (сверены с exam-prep, см. plans/ct-math/BUILD_ON_QUESTIONS.md):
|
||||
* - тип: single/true_false → 'mc'; fill-blank/short_answer → 'open' (если ответ
|
||||
* числовой/дробь/пара) иначе 'long'; multi/multiple → пропуск (exam-prep mc = radio).
|
||||
* - opts_json: [["а","html"],...] кириллические метки; answer(mc)=метка верного.
|
||||
* - answer(open): очищенный числовой/дробь/пара; проверка на клиенте численная.
|
||||
* - математика: \( \) → $ , \[ \] → $$ (exam-prep KaTeX знает только $/$$).
|
||||
* - subtopic = slug из exam_topics(077); difficulty 1..3; variant=year.
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const MATH_ID = 3, EXAM_KEY = 'ctmath';
|
||||
const LABELS = ['а', 'б', 'в', 'г', 'д', 'е', 'ж', 'з'];
|
||||
|
||||
// flat topic name → [section slug, subtopic slug] (slugs из миграции 077)
|
||||
const TOPIC_MAP = {
|
||||
'Теория чисел': ['numbers', 'num-divisibility'],
|
||||
'Арифметика и степени': ['expressions', 'expr-powers-roots'],
|
||||
'Квадратные уравнения': ['equations', 'eq-quadratic'],
|
||||
'Тригонометрия': ['trigonometry', 'trig-identities'],
|
||||
'Тригонометрические уравнения':['trigonometry', 'trig-equations'],
|
||||
'Прогрессии': ['word-sequences', 'seq-progressions'],
|
||||
'Словесные задачи': ['word-sequences', 'word-problems'],
|
||||
'Неравенства': ['equations', 'eq-rational'],
|
||||
'Уравнения': ['equations', 'eq-rational'],
|
||||
'Функции': ['functions', 'fn-properties'],
|
||||
'Логарифмы': ['equations', 'eq-logarithmic'],
|
||||
'Показательные неравенства': ['equations', 'eq-exponential'],
|
||||
'Геометрия': ['planimetry', 'plan-triangles'],
|
||||
'Стереометрия': ['stereometry', 'ster-basics'],
|
||||
'Окружность и круг': ['planimetry', 'plan-circle'],
|
||||
'Числовые промежутки': ['equations', 'eq-linear'],
|
||||
'Подобные фигуры': ['planimetry', 'plan-quadrilaterals'],
|
||||
'Парабола': ['functions', 'fn-graphs'],
|
||||
'Статистика и диаграммы': ['advanced', 'adv-combined'],
|
||||
};
|
||||
|
||||
// \( \) → $ ; \[ \] → $$ (replacement-функции, чтобы $ не интерпретировался)
|
||||
function conv(s) {
|
||||
return String(s || '')
|
||||
.replace(/\\\(/g, () => '$').replace(/\\\)/g, () => '$')
|
||||
.replace(/\\\[/g, () => '$$').replace(/\\\]/g, () => '$$');
|
||||
}
|
||||
// численная проверяемость ответа (зеркало answer-check.js exam-prep)
|
||||
function isNumericAnswer(s) {
|
||||
if (s == null) return false;
|
||||
const t = String(s).trim().replace(/\$/g, '').replace(/\s+/g, '').replace(',', '.');
|
||||
if (/^-?\d+(?:\.\d+)?$/.test(t)) return true; // число
|
||||
if (/^-?\d+(?:\.\d+)?\/-?\d+(?:\.\d+)?$/.test(t)) return true; // дробь
|
||||
const parts = String(s).replace(/\$/g, '').split(/[;]|\sи\s/).map(x => x.trim()).filter(Boolean);
|
||||
if (parts.length === 2 && parts.every(p => /^-?\d+(?:[.,]\d+)?(?:\/-?\d+)?$/.test(p.replace(/\s/g, '')))) return true; // пара
|
||||
return false;
|
||||
}
|
||||
function cleanAnswer(s) { return String(s || '').trim().replace(/\$/g, '').replace(/\s+/g, ' ').trim(); }
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT q.id, q.text, q.type, q.difficulty, q.year, q.explanation, q.image, q.correct_text,
|
||||
COALESCE(t.name,'') AS topic_name
|
||||
FROM questions q LEFT JOIN topics t ON t.id = q.topic_id
|
||||
WHERE q.subject_id = ? AND q.topic_id IS NOT NULL
|
||||
AND (q.source_type IS NULL OR q.source_type <> 'экзамен 9')
|
||||
ORDER BY q.year, q.id
|
||||
`).all(MATH_ID);
|
||||
|
||||
const optStmt = db.prepare('SELECT text, is_correct, order_index FROM options WHERE question_id=? ORDER BY order_index, id');
|
||||
const out = [];
|
||||
const stat = { mc: 0, open: 0, long: 0, skip_multi: 0, skip_notopic: 0, skip_noopts: 0, open_demoted_long: 0 };
|
||||
const perVariant = {};
|
||||
|
||||
for (const q of rows) {
|
||||
const map = TOPIC_MAP[q.topic_name];
|
||||
if (!map) { stat.skip_notopic++; continue; }
|
||||
const [topic, subtopic] = map;
|
||||
const opts = optStmt.all(q.id);
|
||||
const variant = q.year || 0;
|
||||
let task_type, opts_json = null, answer = null;
|
||||
|
||||
if (q.type === 'single' || q.type === 'true_false') {
|
||||
if (!opts.length) { stat.skip_noopts++; continue; }
|
||||
task_type = 'mc';
|
||||
opts_json = JSON.stringify(opts.map((o, i) => [LABELS[i] || String(i + 1), conv(o.text)]));
|
||||
const ci = opts.findIndex(o => o.is_correct);
|
||||
answer = ci >= 0 ? (LABELS[ci] || String(ci + 1)) : null;
|
||||
stat.mc++;
|
||||
} else if (q.type === 'fill-blank' || q.type === 'short_answer') {
|
||||
const corr = (opts.find(o => o.is_correct) || {}).text || q.correct_text || '';
|
||||
if (isNumericAnswer(corr)) { task_type = 'open'; answer = cleanAnswer(corr); stat.open++; }
|
||||
else { task_type = 'long'; answer = null; stat.long++; stat.open_demoted_long++; }
|
||||
} else { // multi / multiple
|
||||
stat.skip_multi++; continue;
|
||||
}
|
||||
|
||||
// solution_html (NOT NULL): объяснение + строка ответа
|
||||
let sol = conv(q.explanation || '');
|
||||
if (answer && task_type !== 'long') sol += `<div class="sol-ans">Ответ: ${task_type === 'mc' ? answer + ')' : '$' + answer + '$'}</div>`;
|
||||
if (!sol.trim()) sol = '<div class="sol-ans">См. решение в источнике.</div>';
|
||||
|
||||
const figure = q.image ? `<img src="${String(q.image)}" alt="" loading="lazy" style="max-width:100%">` : null;
|
||||
perVariant[variant] = (perVariant[variant] || 0) + 1;
|
||||
out.push({
|
||||
exam_key: EXAM_KEY, variant, task_idx: perVariant[variant],
|
||||
task_type, text_html: conv(q.text), figure_html: figure, opts_json, answer,
|
||||
solution_html: sol, topic, subtopic, difficulty: q.difficulty || 1, _qid: q.id, _tn: q.topic_name,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(APPLY ? '[APPLY]' : '[DRY-RUN]', `вход: ${rows.length} размеченных вопросов; к вставке: ${out.length}`);
|
||||
console.log('Статистика типов:', JSON.stringify(stat));
|
||||
console.log('Заданий по годам (variant):', JSON.stringify(perVariant));
|
||||
const bySub = {};
|
||||
out.forEach(o => { bySub[o.subtopic] = (bySub[o.subtopic] || 0) + 1; });
|
||||
console.log('По подтемам:', JSON.stringify(bySub));
|
||||
|
||||
console.log('\n— Выборка (по одному mc / open / long) —');
|
||||
for (const tp of ['mc', 'open', 'long']) {
|
||||
const s = out.find(o => o.task_type === tp);
|
||||
if (!s) continue;
|
||||
console.log(`\n[${tp}] qid=${s._qid} тема="${s._tn}" → ${s.topic}/${s.subtopic} variant=${s.variant} diff=${s.difficulty}`);
|
||||
console.log(' text :', s.text_html.replace(/\s+/g, ' ').slice(0, 160));
|
||||
if (s.opts_json) console.log(' opts :', s.opts_json.slice(0, 200));
|
||||
console.log(' answ :', s.answer);
|
||||
console.log(' sol :', s.solution_html.replace(/\s+/g, ' ').slice(0, 140));
|
||||
}
|
||||
|
||||
if (!APPLY) { console.log('\nDRY-RUN: запись НЕ выполнялась. Для записи: --apply (после применения миграции 077).'); process.exit(0); }
|
||||
|
||||
// ── APPLY ──
|
||||
const track = db.prepare("SELECT exam_key FROM exam_tracks WHERE exam_key=?").get(EXAM_KEY);
|
||||
if (!track) { console.error('Нет трека ctmath (миграция 077 не применена). Сначала примените 077.'); process.exit(1); }
|
||||
const already = db.prepare("SELECT COUNT(*) c FROM exam_tasks WHERE exam_key=?").get(EXAM_KEY).c;
|
||||
if (already > 0) { console.error(`В exam_tasks уже есть ${already} задач ctmath — повторная вставка отменена (избегаем дублей).`); process.exit(1); }
|
||||
const ins = 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 (@exam_key,@variant,@task_idx,@task_type,@text_html,@figure_html,@opts_json,@answer,@solution_html,@topic,@subtopic,@difficulty)`);
|
||||
let n = 0;
|
||||
for (const o of out) { ins.run({ exam_key:o.exam_key, variant:o.variant, task_idx:o.task_idx, task_type:o.task_type, text_html:o.text_html, figure_html:o.figure_html, opts_json:o.opts_json, answer:o.answer, solution_html:o.solution_html, topic:o.topic, subtopic:o.subtopic, difficulty:o.difficulty }); n++; }
|
||||
// обновим метаданные трека
|
||||
const variants = Object.keys(perVariant).length;
|
||||
db.prepare("UPDATE exam_tracks SET variants_count=? WHERE exam_key=?").run(variants, EXAM_KEY);
|
||||
console.log(`\nВставлено ${n} задач в exam_tasks (ctmath). variants_count=${variants}.`);
|
||||
@@ -0,0 +1,87 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Колоды карточек формул для подготовки к ЦЭ/ЦТ (интервальное повторение).
|
||||
* flashcard_decks(user_id,title,description,color) + flashcard_cards(deck_id,front,back,order_idx).
|
||||
* Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
* Идемпотентно: колода с таким title у владельца не создаётся повторно.
|
||||
* node backend/scripts/seed_ctmath_flashcards.js [--dry]
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const DRY = process.argv.includes('--dry');
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Тригонометрия — формулы', color: '#9B5DE5', cards: [
|
||||
['Определения через единичную окружность', '$\\cos\\alpha=x,\\ \\sin\\alpha=y$ (координаты точки)'],
|
||||
['Основное тригонометрическое тождество', '$\\sin^2\\alpha+\\cos^2\\alpha=1$'],
|
||||
['$1+\\operatorname{tg}^2\\alpha$', '$\\dfrac{1}{\\cos^2\\alpha}$'],
|
||||
['$1+\\operatorname{ctg}^2\\alpha$', '$\\dfrac{1}{\\sin^2\\alpha}$'],
|
||||
['$\\sin(\\alpha\\pm\\beta)$', '$\\sin\\alpha\\cos\\beta\\pm\\cos\\alpha\\sin\\beta$'],
|
||||
['$\\cos(\\alpha\\pm\\beta)$', '$\\cos\\alpha\\cos\\beta\\mp\\sin\\alpha\\sin\\beta$'],
|
||||
['$\\sin 2\\alpha$', '$2\\sin\\alpha\\cos\\alpha$'],
|
||||
['$\\cos 2\\alpha$', '$\\cos^2\\alpha-\\sin^2\\alpha=1-2\\sin^2\\alpha=2\\cos^2\\alpha-1$'],
|
||||
['Понижение степени: $\\sin^2\\alpha$', '$\\dfrac{1-\\cos 2\\alpha}{2}$'],
|
||||
['Область значений $\\arcsin x$', '$\\left[-\\tfrac{\\pi}{2};\\tfrac{\\pi}{2}\\right]$'],
|
||||
['Область значений $\\arccos x$', '$[0;\\ \\pi]$'],
|
||||
['$\\sin x=a$ — корни', '$x=(-1)^n\\arcsin a+\\pi n$'],
|
||||
['$\\cos x=a$ — корни', '$x=\\pm\\arccos a+2\\pi n$'],
|
||||
['$\\operatorname{tg} x=a$ — корни', '$x=\\operatorname{arctg} a+\\pi n$'],
|
||||
['$\\sin x=0$', '$x=\\pi n$'],
|
||||
['$\\cos x=0$', '$x=\\tfrac{\\pi}{2}+\\pi n$'],
|
||||
]},
|
||||
{ title: 'ЦТ · Стереометрия — формулы', color: '#00BBF9', cards: [
|
||||
['$V$ призмы', '$S_{\\text{осн}}\\cdot h$'],
|
||||
['$V$ пирамиды', '$\\tfrac{1}{3}S_{\\text{осн}}\\cdot h$'],
|
||||
['$V$ цилиндра', '$\\pi R^2 h$'],
|
||||
['$V$ конуса', '$\\tfrac{1}{3}\\pi R^2 h$'],
|
||||
['$V$ шара', '$\\tfrac{4}{3}\\pi R^3$'],
|
||||
['$S$ сферы', '$4\\pi R^2$'],
|
||||
['$S_{\\text{бок}}$ цилиндра', '$2\\pi R h$'],
|
||||
['$S_{\\text{бок}}$ конуса', '$\\pi R l$'],
|
||||
['Сечение $\\parallel$ основанию: отношение площадей', '$k^2$, где $k$ — отношение высот от вершины'],
|
||||
['Угол между прямыми (векторы)', '$\\cos\\varphi=\\dfrac{|\\vec a\\cdot\\vec b|}{|\\vec a|\\,|\\vec b|}$'],
|
||||
['Скалярное произведение', '$a_xb_x+a_yb_y+a_zb_z$'],
|
||||
['Длина вектора', '$\\sqrt{a_x^2+a_y^2+a_z^2}$'],
|
||||
['Сфера касается плоскости', 'Радиус в точку касания $\\perp$ плоскости (далее Пифагор)'],
|
||||
['Расстояние между скрещивающимися прямыми', 'Длина их общего перпендикуляра'],
|
||||
]},
|
||||
{ title: 'ЦТ · Логарифмы и степени — формулы', color: '#F15BB5', cards: [
|
||||
['$\\log_a(xy)$', '$\\log_a x+\\log_a y$'],
|
||||
['$\\log_a\\dfrac{x}{y}$', '$\\log_a x-\\log_a y$'],
|
||||
['$\\log_a x^p$', '$p\\log_a x$'],
|
||||
['Переход к новому основанию', '$\\log_a x=\\dfrac{\\log_b x}{\\log_b a}$'],
|
||||
['$a^{\\log_a x}$', '$x$'],
|
||||
['$\\log_a a$ и $\\log_a 1$', '$1$ и $0$'],
|
||||
['$a^m\\cdot a^n$', '$a^{m+n}$'],
|
||||
['$(a^m)^n$', '$a^{mn}$'],
|
||||
['$a^{-n}$', '$\\dfrac{1}{a^n}$'],
|
||||
['$a^{m/n}$', '$\\sqrt[n]{a^m}$'],
|
||||
]},
|
||||
{ title: 'ЦТ · Производная — формулы', color: '#00F5D4', cards: [
|
||||
['$(x^n)\'$', '$n x^{n-1}$'],
|
||||
['$(\\sin x)\'$', '$\\cos x$'],
|
||||
['$(\\cos x)\'$', '$-\\sin x$'],
|
||||
['$(e^x)\'$', '$e^x$'],
|
||||
['$(\\ln x)\'$', '$\\dfrac{1}{x}$'],
|
||||
['$(uv)\'$', '$u\'v+uv\'$'],
|
||||
['$\\left(\\dfrac{u}{v}\\right)\'$', '$\\dfrac{u\'v-uv\'}{v^2}$'],
|
||||
['Монотонность по производной', '$f\'>0$ — возрастает; $f\'<0$ — убывает'],
|
||||
['Точка экстремума', '$f\'=0$ и меняет знак'],
|
||||
]},
|
||||
];
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(DRY ? '[DRY-RUN]' : '[APPLY]', 'владелец user_id=', owner);
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) { console.log(` есть колода: «${d.title}» (id ${ex.id}) — пропуск`); continue; }
|
||||
if (DRY) { console.log(` + колода «${d.title}» (${d.cards.length} карт)`); continue; }
|
||||
const did = insDeck.run(owner, d.title, 'Формулы для подготовки к ЦЭ/ЦТ. Интервальное повторение.', d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` + колода «${d.title}» (id ${did}, ${d.cards.length} карт)`);
|
||||
}
|
||||
console.log(DRY ? 'DRY-RUN: ничего не записано.' : 'Готово. Колоды формул добавлены (владелец — admin; раздать классу можно через доступ к колоде).');
|
||||
@@ -0,0 +1,147 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_flashcards_p2.js
|
||||
Ещё две колоды карточек для подготовки к ЦЭ/ЦТ (интервальное повторение):
|
||||
|
||||
1. «ЦТ · Планиметрия — формулы» — треугольники, четырёхугольники, окружность
|
||||
2. «ЦТ · Свойства функций» — чтение графика: D(y), E(y), нули,
|
||||
монотонность, экстремумы, f'(x), чётность, сдвиги
|
||||
|
||||
Источники (бесплатные материалы Кедр от Егора):
|
||||
• Свойства четырехугольников.pdf (параллелограмм/прямоугольник/ромб/квадрат)
|
||||
• Уравнение окружности _ Материал.pdf (уравнение, радиус, расстояние, прямая)
|
||||
• Шпора_по_свойствам_функций_ct_matem.pdf (графический разбор свойств)
|
||||
Формулы треугольника и площадей — базовый набор ЦТ (в шпорах Кедр их нет).
|
||||
|
||||
Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
Те же владелец/таблицы/стиль, что seed_ctmath_flashcards.js (Тригонометрия и т.д.).
|
||||
|
||||
Идемпотентность: колода ищется по (user_id, title). Если уже есть и наполнена —
|
||||
пропуск (не клобберит SR-прогресс). Пустую/новую — наполняет.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_flashcards_p2.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_flashcards_p2.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
|
||||
блокирует продакшн-записи). Без --apply ничего не пишется — только сводка.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Планиметрия — формулы', color: '#FB5607',
|
||||
descr: 'Треугольники, четырёхугольники, окружность. Формулы для планиметрических задач ЦЭ/ЦТ.',
|
||||
cards: [
|
||||
// ── Треугольник ──────────────────────────────────────────────────────
|
||||
['Сумма углов треугольника', '$\\angle A+\\angle B+\\angle C=180^\\circ$'],
|
||||
['Площадь треугольника через основание и высоту', '$S=\\dfrac12\\,a\\,h_a$'],
|
||||
['Площадь треугольника через две стороны и угол', '$S=\\dfrac12\\,ab\\sin C$'],
|
||||
['Теорема Пифагора (прямоугольный $\\triangle$)', '$c^2=a^2+b^2$, где $c$ — гипотенуза'],
|
||||
['Теорема косинусов', '$c^2=a^2+b^2-2ab\\cos C$'],
|
||||
['Теорема синусов', '$\\dfrac{a}{\\sin A}=\\dfrac{b}{\\sin B}=\\dfrac{c}{\\sin C}=2R$'],
|
||||
['Формула Герона', '$S=\\sqrt{p(p-a)(p-b)(p-c)}$, где $p=\\dfrac{a+b+c}{2}$'],
|
||||
['Радиус вписанной окружности ($\\triangle$)', '$r=\\dfrac{S}{p}$ ($p$ — полупериметр)'],
|
||||
['Радиус описанной окружности ($\\triangle$)', '$R=\\dfrac{abc}{4S}$'],
|
||||
['Площадь равностороннего треугольника', '$S=\\dfrac{a^2\\sqrt3}{4}$'],
|
||||
['Средняя линия треугольника', 'Параллельна стороне и равна её половине: $m=\\dfrac{a}{2}$'],
|
||||
['Медиана к гипотенузе (прямоугольный $\\triangle$)', 'Равна половине гипотенузы: $m_c=\\dfrac{c}{2}$'],
|
||||
// ── Четырёхугольники ─────────────────────────────────────────────────
|
||||
['Параллелограмм — определение', 'Четырёхугольник, у которого противоположные стороны попарно параллельны'],
|
||||
['Свойства параллелограмма', 'Противолежащие стороны и углы равны; сумма углов при одной стороне $180^\\circ$; диагонали точкой пересечения делятся пополам'],
|
||||
['Сумма квадратов диагоналей параллелограмма', '$d_1^2+d_2^2=2(a^2+b^2)$'],
|
||||
['Площадь параллелограмма', '$S=a\\,h_a=ab\\sin\\alpha$'],
|
||||
['Прямоугольник — отличие и диагонали', 'Параллелограмм со всеми прямыми углами; диагонали равны: $AC=BD$'],
|
||||
['Площадь прямоугольника', '$S=ab$'],
|
||||
['Ромб — отличие и диагонали', 'Параллелограмм со всеми равными сторонами; диагонали $\\perp$ и являются биссектрисами углов'],
|
||||
['Площадь ромба', '$S=\\dfrac12\\,d_1 d_2=a^2\\sin\\alpha$'],
|
||||
['Квадрат — диагональ и площадь', '$d=a\\sqrt2$; $S=a^2=\\dfrac12\\,d^2$'],
|
||||
['Средняя линия трапеции', '$m=\\dfrac{a+b}{2}$ — полусумма оснований'],
|
||||
['Площадь трапеции', '$S=\\dfrac{a+b}{2}\\cdot h$'],
|
||||
// ── Окружность ───────────────────────────────────────────────────────
|
||||
['Длина окружности', '$C=2\\pi R=\\pi D$'],
|
||||
['Площадь круга', '$S=\\pi R^2$'],
|
||||
['Длина дуги в $n^\\circ$', '$l=\\dfrac{\\pi R n}{180}$'],
|
||||
['Площадь сектора в $n^\\circ$', '$S=\\dfrac{\\pi R^2 n}{360}$'],
|
||||
['Вписанный угол', 'Равен половине центрального, опирающегося на ту же дугу'],
|
||||
['Угол, опирающийся на диаметр', 'Прямой: $90^\\circ$'],
|
||||
['Уравнение окружности', '$(x-x_0)^2+(y-y_0)^2=R^2$ — центр $(x_0;y_0)$, радиус $R$'],
|
||||
['Расстояние между точками', '$d=\\sqrt{(x_2-x_1)^2+(y_2-y_1)^2}$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Свойства функций', color: '#3A86FF',
|
||||
descr: 'Чтение графика функции: область определения и значений, нули, монотонность, экстремумы, производная, чётность, сдвиги.',
|
||||
cards: [
|
||||
["Область определения $D(y)$", "Множество всех значений $x$, при которых функция существует (проекция графика на ось $Ox$)"],
|
||||
["Множество значений $E(y)$", "Множество всех значений $y$, которые принимает функция (проекция графика на ось $Oy$)"],
|
||||
["Нули функции", "Значения $x$, при которых $f(x)=0$ — точки пересечения графика с осью $Ox$"],
|
||||
["Пересечение графика с осью $Oy$", "Точка $(0;\\,f(0))$ — подставляем $x=0$"],
|
||||
["Наибольшее и наименьшее значения функции", "Ордината $y$ самой высокой и самой низкой точек графика"],
|
||||
["Функция возрастает на промежутке", "Большему $x$ соответствует большее $y$ (график идёт вверх); признак: $f'(x)>0$"],
|
||||
["Функция убывает на промежутке", "Большему $x$ соответствует меньшее $y$ (график идёт вниз); признак: $f'(x)<0$"],
|
||||
["Промежутки знакопостоянства", "$y>0$ — где график выше оси $Ox$; $y<0$ — где ниже оси $Ox$"],
|
||||
["Точка максимума $x_{\\max}$", "Точка, в которой функция меняет возрастание на убывание ($f'$ меняет знак с $+$ на $-$)"],
|
||||
["Точка минимума $x_{\\min}$", "Точка, в которой функция меняет убывание на возрастание ($f'$ меняет знак с $-$ на $+$)"],
|
||||
["Экстремум функции", "Значение $y=f(x)$ в точке максимума или минимума (сама точка $x$ — точка экстремума)"],
|
||||
["Знак $f'(x)$ и монотонность", "$f'(x)>0$ → функция возрастает; $f'(x)<0$ → функция убывает"],
|
||||
["Условие $f'(x)=0$", "Точка, подозрительная на экстремум: экстремум есть, если $f'$ при переходе меняет знак"],
|
||||
["Чётная функция", "$f(-x)=f(x)$; график симметричен относительно оси $Oy$"],
|
||||
["Нечётная функция", "$f(-x)=-f(x)$; график симметричен относительно начала координат"],
|
||||
["Сдвиг $y=f(x)+a$ (при $a>0$)", "График сдвигается вверх на $a$"],
|
||||
["Сдвиг $y=f(x)-a$ (при $a>0$)", "График сдвигается вниз на $a$"],
|
||||
["Сдвиг $y=f(x+a)$ (при $a>0$)", "График сдвигается влево на $a$"],
|
||||
["Сдвиг $y=f(x-a)$ (при $a>0$)", "График сдвигается вправо на $a$"],
|
||||
]},
|
||||
];
|
||||
|
||||
/* ── self-check: чаще всего KaTeX ломают непарные $ или {} ─────────────────── */
|
||||
let bad = 0;
|
||||
for (const d of DECKS) {
|
||||
d.cards.forEach(([f, b], i) => {
|
||||
[['front', f], ['back', b]].forEach(([side, s]) => {
|
||||
const dollars = (s.match(/\$/g) || []).length;
|
||||
const braces = (s.match(/\{/g) || []).length - (s.match(/\}/g) || []).length;
|
||||
if (dollars % 2 !== 0) { console.error(`✗ непарный $ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (braces !== 0) { console.error(`✗ непарные {} — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (bad) { console.error(`\nСамопроверка: ${bad} проблем — исправь до записи.\n`); process.exit(1); }
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const countCard = db.prepare('SELECT COUNT(*) c FROM flashcard_cards WHERE deck_id=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(`\n=== seed_ctmath_flashcards_p2 (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log('владелец user_id =', owner, '\n');
|
||||
|
||||
let plannedDecks = 0, plannedCards = 0;
|
||||
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) {
|
||||
const have = countCard.get(ex.id).c;
|
||||
if (have > 0) { console.log(`• «${d.title}» (id ${ex.id}) — уже наполнена (${have} карт), пропуск`); continue; }
|
||||
// колода есть, но пустая → дольём карты
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» (id ${ex.id}) — пустая, долить ${d.cards.length} карт`);
|
||||
if (APPLY) d.cards.forEach(([f, b], i) => insCard.run(ex.id, f, b, i));
|
||||
continue;
|
||||
}
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» — новая колода, ${d.cards.length} карт`);
|
||||
if (APPLY) {
|
||||
const did = insDeck.run(owner, d.title, d.descr, d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` создана id ${did}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nИтого к ${APPLY ? 'записи' : 'добавлению'}: колод ${plannedDecks}, карт ${plannedCards}.`);
|
||||
if (!APPLY) console.log('DRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_flashcards_p2.js --apply\n');
|
||||
else console.log('Готово. Колоды добавлены (владелец — admin; раздать классу — через доступ к колоде).\n');
|
||||
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_flashcards_p3.js
|
||||
Ещё две колоды карточек для подготовки к ЦЭ/ЦТ (интервальное повторение):
|
||||
|
||||
1. «ЦТ · Прогрессии — формулы» — арифметическая + геометрическая
|
||||
2. «ЦТ · Двойные неравенства» — оценка a±b, a·b, a/b почленно (+ловушки)
|
||||
|
||||
Источники:
|
||||
• Прогрессии — канонический набор формул ЦТ (отдельной шпоры Кедр нет).
|
||||
• Двойные неравенства — Кедр «Операции_с_двойными_неравенствами.pdf»
|
||||
(сложение/умножение/вычитание/деление двойных неравенств; ловушки строгости
|
||||
и запрет почленного вычитания/деления).
|
||||
|
||||
Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
Те же владелец/таблицы/стиль, что seed_ctmath_flashcards.js / _p2.js.
|
||||
|
||||
Идемпотентность: колода ищется по (user_id, title). Если уже есть и наполнена —
|
||||
пропуск (не клобберит SR-прогресс). Пустую/новую — наполняет.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_flashcards_p3.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_flashcards_p3.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
|
||||
блокирует продакшн-записи). Без --apply ничего не пишется — только сводка.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Прогрессии — формулы', color: '#06D6A0',
|
||||
descr: 'Арифметическая и геометрическая прогрессии: n-й член, суммы, характеристические свойства.',
|
||||
cards: [
|
||||
// ── Арифметическая ───────────────────────────────────────────────────
|
||||
['Арифметическая прогрессия — $n$-й член', '$a_n=a_1+(n-1)d$'],
|
||||
['Разность арифметической прогрессии', '$d=a_{n+1}-a_n$ (постоянная)'],
|
||||
['Характеристическое свойство арифм. прогрессии', '$a_n=\\dfrac{a_{n-1}+a_{n+1}}{2}$ — каждый член есть среднее арифметическое соседних'],
|
||||
['Сумма $n$ членов арифм. прогрессии', '$S_n=\\dfrac{a_1+a_n}{2}\\cdot n$'],
|
||||
['Сумма арифм. прогрессии через $d$', '$S_n=\\dfrac{2a_1+(n-1)d}{2}\\cdot n$'],
|
||||
['Связь двух членов арифм. прогрессии', '$a_n=a_k+(n-k)d$'],
|
||||
// ── Геометрическая ───────────────────────────────────────────────────
|
||||
['Геометрическая прогрессия — $n$-й член', '$b_n=b_1\\,q^{\\,n-1}$'],
|
||||
['Знаменатель геометрической прогрессии', '$q=\\dfrac{b_{n+1}}{b_n}$ (постоянный, $q\\neq0$)'],
|
||||
['Характеристическое свойство геом. прогрессии', '$b_n^2=b_{n-1}\\cdot b_{n+1}$'],
|
||||
['Сумма $n$ членов геом. прогрессии ($q\\neq1$)', '$S_n=\\dfrac{b_1(q^{\\,n}-1)}{q-1}$'],
|
||||
['Сумма бесконечной убывающей геом. прогрессии ($|q|<1$)', '$S=\\dfrac{b_1}{1-q}$'],
|
||||
['Связь двух членов геом. прогрессии', '$b_n=b_k\\cdot q^{\\,n-k}$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Двойные неравенства', color: '#EF476F',
|
||||
descr: 'Оценка суммы, разности, произведения и частного через двойные неравенства. Ловушки вычитания и деления.',
|
||||
cards: [
|
||||
['Сложение неравенств', 'Складываем почленно ТОЛЬКО одинаково направленные. $5\\le a\\le6$, $7\\le b\\le9$ $\\Rightarrow$ $12\\le a+b\\le15$'],
|
||||
['Умножение неравенств', 'Перемножаем почленно (все части положительны). $5\\le a\\le6$, $7\\le b\\le9$ $\\Rightarrow$ $35\\le ab\\le54$'],
|
||||
['Ловушка строгости', 'Если хоть одно исходное неравенство строгое, результат строгий ($<$). Напр. $5\\le a<6$, $7<b<9$ $\\Rightarrow$ $12<a+b<15$'],
|
||||
['Оценка разности $a-b$ — приём', '$a-b=a+(-b)$: неравенство для $b$ умножаем на $-1$ (переворот): $7<b<9$ $\\Rightarrow$ $-9<-b<-7$, затем складываем с $a$'],
|
||||
['Пример: оценка $a-b$', '$5<a<6$, $7<b<9$: $(5<a<6)+(-9<-b<-7)$ $\\Rightarrow$ $-4<a-b<-1$'],
|
||||
['Почему нельзя вычитать неравенства почленно', 'Почленно складывать/умножать можно только одинаково направленные. Прямое $5-9<a-b<6-7$ даёт $-2<a-b<-3$ — бессмыслица'],
|
||||
['Оценка частного $\\dfrac{a}{b}$ — приём', '$\\dfrac{a}{b}=a\\cdot\\dfrac{1}{b}$: для $b>0$ берём обратные с переворотом: $7<b<9$ $\\Rightarrow$ $\\dfrac{1}{9}<\\dfrac{1}{b}<\\dfrac{1}{7}$, затем умножаем на $a$'],
|
||||
['Пример: оценка $\\dfrac{a}{b}$', '$5<a<6$, $7<b<9$: $(5<a<6)\\cdot\\left(\\dfrac{1}{9}<\\dfrac{1}{b}<\\dfrac{1}{7}\\right)$ $\\Rightarrow$ $\\dfrac{5}{9}<\\dfrac{a}{b}<\\dfrac{6}{7}$'],
|
||||
['Условие почленного умножения/деления', 'Все части неравенств положительны. Для отрицательных частей знаки переворачиваются — оценивать осторожно'],
|
||||
]},
|
||||
];
|
||||
|
||||
/* ── self-check: чаще всего KaTeX ломают непарные $ или {} ─────────────────── */
|
||||
let bad = 0;
|
||||
for (const d of DECKS) {
|
||||
d.cards.forEach(([f, b], i) => {
|
||||
[['front', f], ['back', b]].forEach(([side, s]) => {
|
||||
const dollars = (s.match(/\$/g) || []).length;
|
||||
const braces = (s.match(/\{/g) || []).length - (s.match(/\}/g) || []).length;
|
||||
if (dollars % 2 !== 0) { console.error(`✗ непарный $ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (braces !== 0) { console.error(`✗ непарные {} — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (bad) { console.error(`\nСамопроверка: ${bad} проблем — исправь до записи.\n`); process.exit(1); }
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const countCard = db.prepare('SELECT COUNT(*) c FROM flashcard_cards WHERE deck_id=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(`\n=== seed_ctmath_flashcards_p3 (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log('владелец user_id =', owner, '\n');
|
||||
|
||||
let plannedDecks = 0, plannedCards = 0;
|
||||
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) {
|
||||
const have = countCard.get(ex.id).c;
|
||||
if (have > 0) { console.log(`• «${d.title}» (id ${ex.id}) — уже наполнена (${have} карт), пропуск`); continue; }
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» (id ${ex.id}) — пустая, долить ${d.cards.length} карт`);
|
||||
if (APPLY) d.cards.forEach(([f, b], i) => insCard.run(ex.id, f, b, i));
|
||||
continue;
|
||||
}
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» — новая колода, ${d.cards.length} карт`);
|
||||
if (APPLY) {
|
||||
const did = insDeck.run(owner, d.title, d.descr, d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` создана id ${did}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nИтого к ${APPLY ? 'записи' : 'добавлению'}: колод ${plannedDecks}, карт ${plannedCards}.`);
|
||||
if (!APPLY) console.log('DRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_flashcards_p3.js --apply\n');
|
||||
else console.log('Готово. Колоды добавлены (владелец — admin; раздать классу — через доступ к колоде).\n');
|
||||
@@ -0,0 +1,112 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_flashcards_p4.js
|
||||
Ещё две колоды карточек для подготовки к ЦЭ/ЦТ (интервальное повторение):
|
||||
|
||||
1. «ЦТ · Системы уравнений» — методы подстановки/сложения, приёмы
|
||||
2. «ЦТ · Текстовые задачи» — проценты, сплавы/растворы, движение, работа
|
||||
|
||||
Источники:
|
||||
• Системы — Кедр «Материал по системам.pdf» (подстановка, сложение с
|
||||
домножением, пересечение графиков = система, проверка пары, x²−y²=(x+y)(x−y)).
|
||||
• Текстовые задачи — канонические приёмы ЦТ (отдельная шпора Кедр >20 МБ,
|
||||
Read не тянет; методы стандартные).
|
||||
|
||||
Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
⚠️ Кириллица ТОЛЬКО вне $…$ — в math-режиме KaTeX нет кириллических глифов.
|
||||
Те же владелец/таблицы/стиль, что seed_ctmath_flashcards.js / _p2 / _p3.
|
||||
|
||||
Идемпотентность: колода ищется по (user_id, title). Если уже есть и наполнена —
|
||||
пропуск (не клобберит SR-прогресс). Пустую/новую — наполняет.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_flashcards_p4.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_flashcards_p4.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
|
||||
блокирует продакшн-записи). Без --apply ничего не пишется — только сводка.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Системы уравнений', color: '#118AB2',
|
||||
descr: 'Методы решения систем: подстановка, алгебраическое сложение, пересечение графиков, проверка пары и приёмы.',
|
||||
cards: [
|
||||
['Метод подстановки', 'Выразить одну переменную из одного уравнения и подставить в другое → останется уравнение с одной переменной; решить и вернуть найденное в первое'],
|
||||
['Метод алгебраического сложения', 'Записать уравнения друг под другом; при необходимости домножить, чтобы коэффициенты при одной переменной стали равными или противоположными; сложить/вычесть — одна переменная уходит'],
|
||||
['Уравнивание коэффициентов', 'Домножить уравнения на подходящие числа, чтобы перед одной переменной совпали коэффициенты (напр., умножить на $3$ и на $2$, чтобы в обоих стало $6x$), затем сложить или вычесть'],
|
||||
['Пересечение графиков = система', 'Точки пересечения двух графиков — это решения системы из их уравнений. Записать оба уравнения в систему и решить'],
|
||||
['Приём при известной сумме $x+y$', 'Разложить разность квадратов: $x^2-y^2=(x+y)(x-y)$. Подставить известную сумму, найти разность $x-y$, затем сложить/вычесть с $x+y$'],
|
||||
['Проверка: пара — решение системы?', 'Подставить пару в КАЖДОЕ уравнение. Пара — решение только если во ВСЕХ уравнениях получились верные равенства (хотя бы одно неверно → не решение)'],
|
||||
['Симметричное выражение через корни', 'Найти обе точки пересечения $(x_1;y_1)$ и $(x_2;y_2)$, затем подставить их в искомое выражение (напр. $x_1 x_2+y_1 y_2$)'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Текстовые задачи', color: '#FF9F1C',
|
||||
descr: 'Приёмы текстовых задач: проценты, сплавы и растворы, движение, совместная работа.',
|
||||
cards: [
|
||||
['Процент от числа', '$p\\%$ от числа $a$ равно $\\dfrac{p}{100}\\cdot a$'],
|
||||
['Сколько процентов одно число от другого', 'Часть разделить на целое и умножить на $100$: $\\dfrac{a}{b}\\cdot100\\%$'],
|
||||
['Увеличить число на $p\\%$', 'Умножить на $\\left(1+\\dfrac{p}{100}\\right)$'],
|
||||
['Уменьшить число на $p\\%$', 'Умножить на $\\left(1-\\dfrac{p}{100}\\right)$'],
|
||||
['Концентрация вещества в растворе', 'Доля = масса чистого вещества разделить на массу всего раствора'],
|
||||
['Масса вещества при концентрации', 'Масса раствора умножить на концентрацию (долю)'],
|
||||
['Сплавы и растворы — что складывается', 'Складываются массы веществ и массы растворов (по отдельности); проценты-концентрации НЕ складываются — концентрацию смеси пересчитывают заново'],
|
||||
['Равномерное движение', '$S=v\\cdot t$ — путь равен скорости, умноженной на время'],
|
||||
['Скорость по течению и против', 'По течению $v+u$; против течения $v-u$ ($v$ — собственная скорость, $u$ — скорость течения)'],
|
||||
['Средняя скорость на всём пути', 'Весь путь разделить на всё время — $v=\\dfrac{S}{t}$ (НЕ среднее арифметическое скоростей!)'],
|
||||
['Производительность работы', 'Объём работы разделить на время. Весь объём обычно принимают за $1$'],
|
||||
['Совместная работа двоих', 'Производительности складываются: если поодиночке за $a$ и $b$ времени, то вместе за $\\dfrac{ab}{a+b}$'],
|
||||
]},
|
||||
];
|
||||
|
||||
/* ── self-check: чаще всего KaTeX ломают непарные $ или {} ─────────────────── */
|
||||
let bad = 0;
|
||||
for (const d of DECKS) {
|
||||
d.cards.forEach(([f, b], i) => {
|
||||
[['front', f], ['back', b]].forEach(([side, s]) => {
|
||||
const dollars = (s.match(/\$/g) || []).length;
|
||||
const braces = (s.match(/\{/g) || []).length - (s.match(/\}/g) || []).length;
|
||||
if (dollars % 2 !== 0) { console.error(`✗ непарный $ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (braces !== 0) { console.error(`✗ непарные {} — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (bad) { console.error(`\nСамопроверка: ${bad} проблем — исправь до записи.\n`); process.exit(1); }
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const countCard = db.prepare('SELECT COUNT(*) c FROM flashcard_cards WHERE deck_id=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(`\n=== seed_ctmath_flashcards_p4 (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log('владелец user_id =', owner, '\n');
|
||||
|
||||
let plannedDecks = 0, plannedCards = 0;
|
||||
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) {
|
||||
const have = countCard.get(ex.id).c;
|
||||
if (have > 0) { console.log(`• «${d.title}» (id ${ex.id}) — уже наполнена (${have} карт), пропуск`); continue; }
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» (id ${ex.id}) — пустая, долить ${d.cards.length} карт`);
|
||||
if (APPLY) d.cards.forEach(([f, b], i) => insCard.run(ex.id, f, b, i));
|
||||
continue;
|
||||
}
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» — новая колода, ${d.cards.length} карт`);
|
||||
if (APPLY) {
|
||||
const did = insDeck.run(owner, d.title, d.descr, d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` создана id ${did}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nИтого к ${APPLY ? 'записи' : 'добавлению'}: колод ${plannedDecks}, карт ${plannedCards}.`);
|
||||
if (!APPLY) console.log('DRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_flashcards_p4.js --apply\n');
|
||||
else console.log('Готово. Колоды добавлены (владелец — admin; раздать классу — через доступ к колоде).\n');
|
||||
@@ -0,0 +1,114 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_flashcards_p5.js
|
||||
Ещё две колоды карточек для подготовки к ЦЭ/ЦТ (интервальное повторение):
|
||||
|
||||
1. «ЦТ · Квадратные уравнения» — дискриминант, формула корней, Виета, неполные
|
||||
2. «ЦТ · Модуль» — определение, уравнения/неравенства с модулем
|
||||
|
||||
Источник: канонический набор ЦТ (фундаментальные высокочастотные темы; отдельных
|
||||
шпор Кедр нет, материал стандартный).
|
||||
|
||||
Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
⚠️ Кириллица ТОЛЬКО вне $…$ — в math-режиме KaTeX нет кириллических глифов.
|
||||
Те же владелец/таблицы/стиль, что seed_ctmath_flashcards.js / _p2 / _p3 / _p4.
|
||||
|
||||
Идемпотентность: колода ищется по (user_id, title). Если уже есть и наполнена —
|
||||
пропуск (не клобберит SR-прогресс). Пустую/новую — наполняет.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_flashcards_p5.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_flashcards_p5.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
|
||||
блокирует продакшн-записи). Без --apply ничего не пишется — только сводка.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Квадратные уравнения', color: '#8338EC',
|
||||
descr: 'Дискриминант, формула корней, теорема Виета, неполные уравнения, разложение на множители.',
|
||||
cards: [
|
||||
['Квадратное уравнение — общий вид', '$ax^2+bx+c=0$, где $a\\neq0$'],
|
||||
['Дискриминант', '$D=b^2-4ac$'],
|
||||
['Число корней по дискриминанту', '$D>0$ — два корня; $D=0$ — один (кратный); $D<0$ — действительных корней нет'],
|
||||
['Формула корней', '$x=\\dfrac{-b\\pm\\sqrt{D}}{2a}$'],
|
||||
['Теорема Виета', '$x_1+x_2=-\\dfrac{b}{a}$, $x_1 x_2=\\dfrac{c}{a}$'],
|
||||
['Виета для приведённого $x^2+px+q=0$', '$x_1+x_2=-p$, $x_1 x_2=q$'],
|
||||
['Разложение трёхчлена на множители', '$ax^2+bx+c=a(x-x_1)(x-x_2)$, где $x_1,x_2$ — корни'],
|
||||
['Неполное уравнение $ax^2+c=0$', '$x^2=-\\dfrac{c}{a}$; корни $x=\\pm\\sqrt{-\\dfrac{c}{a}}$ при $-\\dfrac{c}{a}\\ge0$, иначе их нет'],
|
||||
['Неполное уравнение $ax^2+bx=0$', 'Вынести $x$: $x(ax+b)=0$, откуда $x=0$ или $x=-\\dfrac{b}{a}$'],
|
||||
['Сокращённая формула при чётном $b=2k$', '$x=\\dfrac{-k\\pm\\sqrt{k^2-ac}}{a}$'],
|
||||
['Сумма квадратов корней через Виета', '$x_1^2+x_2^2=(x_1+x_2)^2-2x_1 x_2$'],
|
||||
['Знаки корней по Виета', 'Оба положительны при $x_1 x_2>0$ и $x_1+x_2>0$; корни разных знаков при $x_1 x_2<0$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Модуль', color: '#E63946',
|
||||
descr: 'Определение и свойства модуля, уравнения и неравенства с модулем, раскрытие по промежуткам.',
|
||||
cards: [
|
||||
['Определение модуля', '$|x|=x$ при $x\\ge0$ и $|x|=-x$ при $x<0$'],
|
||||
['Геометрический смысл модуля', '$|x|$ — расстояние от точки $x$ до нуля; $|x-a|$ — расстояние между $x$ и $a$'],
|
||||
['Уравнение $|x|=a$', 'При $a>0$: $x=\\pm a$; при $a=0$: $x=0$; при $a<0$ решений нет'],
|
||||
['Уравнение $|f|=|g|$', 'Равносильно совокупности $f=g$ или $f=-g$'],
|
||||
['Уравнение $|f|=g$', 'Нужно $g\\ge0$, и тогда $f=g$ или $f=-g$ (правую часть проверяем на знак)'],
|
||||
['Неравенство $|x|<a$ (при $a>0$)', 'Равносильно $-a<x<a$'],
|
||||
['Неравенство $|x|>a$ (при $a>0$)', 'Равносильно $x<-a$ или $x>a$'],
|
||||
['Неположительная правая часть', '$|x|<a$ при $a\\le0$ — решений нет; $|x|>a$ при $a<0$ — любое $x$'],
|
||||
['Свойства модуля произведения и дроби', '$|ab|=|a|\\cdot|b|$; $\\left|\\dfrac{a}{b}\\right|=\\dfrac{|a|}{|b|}$'],
|
||||
['Корень из квадрата', '$\\sqrt{x^2}=|x|$ — это модуль, а не просто $x$'],
|
||||
['Базовые свойства модуля', '$|a|\\ge0$ всегда; $|a|=|-a|$; $|a|\\ge a$'],
|
||||
['Раскрытие модулей по промежуткам', 'Найти нули подмодульных выражений, разбить числовую ось на промежутки и на каждом раскрыть модули по знаку'],
|
||||
]},
|
||||
];
|
||||
|
||||
/* ── self-check: чаще всего KaTeX ломают непарные $ или {} ─────────────────── */
|
||||
let bad = 0;
|
||||
for (const d of DECKS) {
|
||||
d.cards.forEach(([f, b], i) => {
|
||||
[['front', f], ['back', b]].forEach(([side, s]) => {
|
||||
const dollars = (s.match(/\$/g) || []).length;
|
||||
const braces = (s.match(/\{/g) || []).length - (s.match(/\}/g) || []).length;
|
||||
if (dollars % 2 !== 0) { console.error(`✗ непарный $ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (braces !== 0) { console.error(`✗ непарные {} — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (bad) { console.error(`\nСамопроверка: ${bad} проблем — исправь до записи.\n`); process.exit(1); }
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const countCard = db.prepare('SELECT COUNT(*) c FROM flashcard_cards WHERE deck_id=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(`\n=== seed_ctmath_flashcards_p5 (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log('владелец user_id =', owner, '\n');
|
||||
|
||||
let plannedDecks = 0, plannedCards = 0;
|
||||
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) {
|
||||
const have = countCard.get(ex.id).c;
|
||||
if (have > 0) { console.log(`• «${d.title}» (id ${ex.id}) — уже наполнена (${have} карт), пропуск`); continue; }
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» (id ${ex.id}) — пустая, долить ${d.cards.length} карт`);
|
||||
if (APPLY) d.cards.forEach(([f, b], i) => insCard.run(ex.id, f, b, i));
|
||||
continue;
|
||||
}
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» — новая колода, ${d.cards.length} карт`);
|
||||
if (APPLY) {
|
||||
const did = insDeck.run(owner, d.title, d.descr, d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` создана id ${did}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nИтого к ${APPLY ? 'записи' : 'добавлению'}: колод ${plannedDecks}, карт ${plannedCards}.`);
|
||||
if (!APPLY) console.log('DRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_flashcards_p5.js --apply\n');
|
||||
else console.log('Готово. Колоды добавлены (владелец — admin; раздать классу — через доступ к колоде).\n');
|
||||
@@ -0,0 +1,188 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_flashcards_p6.js
|
||||
Большой разноплановый батч — 8 колод по оставшимся темам ЦЭ/ЦТ:
|
||||
|
||||
1. «ЦТ · Формулы сокращённого умножения»
|
||||
2. «ЦТ · Иррациональные уравнения»
|
||||
3. «ЦТ · Показательные уравнения и неравенства»
|
||||
4. «ЦТ · Логарифмические уравнения и неравенства»
|
||||
5. «ЦТ · Метод интервалов»
|
||||
6. «ЦТ · Вектора на плоскости»
|
||||
7. «ЦТ · Теория вероятностей и комбинаторика»
|
||||
8. «ЦТ · Параметры»
|
||||
|
||||
Источник: канонический набор ЦТ (фундаментальные темы; материал стандартный).
|
||||
|
||||
Математика — KaTeX inline $…$ (страница флешкарт рендерит \( \), \[ \], $ $; НЕ $$).
|
||||
⚠️ Кириллица ТОЛЬКО вне $…$ — в math-режиме KaTeX нет кириллических глифов.
|
||||
Те же владелец/таблицы/стиль, что seed_ctmath_flashcards.js / _p2 … _p5.
|
||||
|
||||
Идемпотентность: колода ищется по (user_id, title). Если уже есть и наполнена —
|
||||
пропуск (не клобберит SR-прогресс). Пустую/новую — наполняет.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_flashcards_p6.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_flashcards_p6.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную (авто-режим Claude Code
|
||||
блокирует продакшн-записи). Без --apply ничего не пишется — только сводка.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const db = require('../src/db/db');
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
|
||||
const owner = (db.prepare("SELECT id FROM users WHERE role='admin' ORDER BY id LIMIT 1").get()
|
||||
|| db.prepare('SELECT id FROM users ORDER BY id LIMIT 1').get()).id;
|
||||
|
||||
const DECKS = [
|
||||
{ title: 'ЦТ · Формулы сокращённого умножения', color: '#FFBE0B',
|
||||
descr: 'Квадраты и кубы суммы/разности, разность квадратов, сумма и разность кубов.',
|
||||
cards: [
|
||||
['Квадрат суммы', '$(a+b)^2=a^2+2ab+b^2$'],
|
||||
['Квадрат разности', '$(a-b)^2=a^2-2ab+b^2$'],
|
||||
['Разность квадратов', '$a^2-b^2=(a-b)(a+b)$'],
|
||||
['Куб суммы', '$(a+b)^3=a^3+3a^2 b+3ab^2+b^3$'],
|
||||
['Куб разности', '$(a-b)^3=a^3-3a^2 b+3ab^2-b^3$'],
|
||||
['Сумма кубов', '$a^3+b^3=(a+b)(a^2-ab+b^2)$'],
|
||||
['Разность кубов', '$a^3-b^3=(a-b)(a^2+ab+b^2)$'],
|
||||
['Квадрат трёхчлена', '$(a+b+c)^2=a^2+b^2+c^2+2ab+2bc+2ac$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Иррациональные уравнения', color: '#2EC4B6',
|
||||
descr: 'Уравнения с корнями: возведение в степень, ОДЗ, обязательная проверка корней, замена.',
|
||||
cards: [
|
||||
['Главная идея решения', 'Изолировать корень и возвести обе части в степень корня, затем ОБЯЗАТЕЛЬНО проверить корни — могут появиться посторонние'],
|
||||
['ОДЗ арифметического корня (чётная степень)', 'Подкоренное выражение $\\ge0$; если корень равен выражению $g$, то нужно ещё $g\\ge0$'],
|
||||
['Уравнение $\\sqrt{f}=g$', 'Равносильно системе: $g\\ge0$ и $f=g^2$'],
|
||||
['Уравнение $\\sqrt{f}=\\sqrt{g}$', 'Равносильно: $f=g$ при $f\\ge0$ (тогда и $g\\ge0$)'],
|
||||
['Почему появляются посторонние корни', 'Возведение в чётную степень не равносильно (теряется знак), поэтому проверка подстановкой обязательна'],
|
||||
['Замена переменной', 'Удобна замена $t=\\sqrt[n]{f}$ при $t\\ge0$ — сводит к рациональному уравнению относительно $t$'],
|
||||
['Корень нечётной степени', '$\\sqrt[3]{f}=g$ равносильно $f=g^3$ без ограничений на знак (ОДЗ — вся ось)'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Показательные уравнения и неравенства', color: '#FF006E',
|
||||
descr: 'Приведение к равным основаниям, замена t=a^x, направление знака неравенства при a>1 и 0<a<1.',
|
||||
cards: [
|
||||
['Основной приём', '$a^{f}=a^{g}$ (при $a>0$, $a\\neq1$) равносильно $f=g$'],
|
||||
['Свойства степеней (напоминание)', '$a^m\\cdot a^n=a^{m+n}$; $\\dfrac{a^m}{a^n}=a^{m-n}$; $(a^m)^n=a^{mn}$'],
|
||||
['Замена $t=a^x$', 'При наличии $a^{2x}$ и $a^x$ удобна замена $t=a^x>0$ — сводит к квадратному (корни с $t\\le0$ отбрасываем)'],
|
||||
['Неравенство при $a>1$', '$a^{f}>a^{g}$ равносильно $f>g$ — знак сохраняется'],
|
||||
['Неравенство при $0<a<1$', '$a^{f}>a^{g}$ равносильно $f<g$ — знак переворачивается'],
|
||||
['Область значений', '$a^x>0$ всегда, поэтому $a^x=b$ при $b\\le0$ решений не имеет'],
|
||||
['Однородное уравнение', 'Разделить на $b^{2x}$ и свести к квадратному относительно $\\left(\\dfrac{a}{b}\\right)^x$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Логарифмические уравнения и неравенства', color: '#8AC926',
|
||||
descr: 'Определение и ОДЗ логарифма, потенцирование, направление знака при a>1 и 0<a<1, замена.',
|
||||
cards: [
|
||||
['Определение логарифма', '$\\log_a b=c$ означает $a^c=b$ (при $a>0$, $a\\neq1$, $b>0$)'],
|
||||
['ОДЗ логарифма', 'Аргумент $>0$, основание $>0$ и $\\neq1$. Записать ДО решения и проверить корни'],
|
||||
['Уравнение $\\log_a f=\\log_a g$', 'Равносильно $f=g$ при $f>0$ и $g>0$'],
|
||||
['Уравнение $\\log_a f=b$', 'Равносильно $f=a^{b}$ с учётом ОДЗ ($f>0$)'],
|
||||
['Неравенство при $a>1$', '$\\log_a f>\\log_a g$ равносильно $f>g>0$ — знак сохраняется'],
|
||||
['Неравенство при $0<a<1$', '$\\log_a f>\\log_a g$ равносильно $0<f<g$ — знак переворачивается'],
|
||||
['Свойства (напоминание)', '$\\log_a(xy)=\\log_a x+\\log_a y$; $\\log_a x^p=p\\log_a x$; $\\log_a x=\\dfrac{\\log_b x}{\\log_b a}$'],
|
||||
['Замена $t=\\log_a x$', 'Сводит уравнение с $\\log^2$ и $\\log$ к квадратному относительно $t$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Метод интервалов', color: '#4361EE',
|
||||
descr: 'Решение рациональных неравенств: нули числителя и знаменателя, расстановка знаков, кратность.',
|
||||
cards: [
|
||||
['Когда применяется', 'Для неравенств вида $\\dfrac{P(x)}{Q(x)}>0$ (и $<,\\ge,\\le$), где $P,Q$ разложены на множители'],
|
||||
['Шаг 1 — найти нули', 'Нули числителя (где выражение $=0$) и нули знаменателя (где не определено)'],
|
||||
['Шаг 2 — отметить на оси', 'Нули знаменателя ВСЕГДА выколотые; нули числителя при $\\ge,\\le$ закрашенные, при $>,<$ выколотые'],
|
||||
['Шаг 3 — расставить знаки', 'Определить знак выражения на каждом промежутке (подстановкой пробной точки) и выбрать промежутки с нужным знаком'],
|
||||
['Кратность корня', 'Через корень чётной кратности знак НЕ меняется; через корень нечётной кратности — меняется'],
|
||||
['Типичная ошибка', 'Нельзя умножать неравенство на $Q(x)$ — знак неизвестен. Всё переносим в одну часть и приводим к общему знаменателю'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Вектора на плоскости', color: '#6A4C93',
|
||||
descr: 'Координаты, длина, операции, скалярное произведение, перпендикулярность и коллинеарность.',
|
||||
cards: [
|
||||
['Координаты вектора по двум точкам', '$\\overrightarrow{AB}=(x_B-x_A;\\ y_B-y_A)$'],
|
||||
['Длина (модуль) вектора', '$|\\vec a|=\\sqrt{a_x^2+a_y^2}$'],
|
||||
['Сумма и разность векторов', '$\\vec a\\pm\\vec b=(a_x\\pm b_x;\\ a_y\\pm b_y)$'],
|
||||
['Умножение вектора на число', '$k\\vec a=(k a_x;\\ k a_y)$'],
|
||||
['Скалярное произведение (координаты)', '$\\vec a\\cdot\\vec b=a_x b_x+a_y b_y$'],
|
||||
['Скалярное произведение (через угол)', '$\\vec a\\cdot\\vec b=|\\vec a|\\,|\\vec b|\\cos\\varphi$'],
|
||||
['Косинус угла между векторами', '$\\cos\\varphi=\\dfrac{\\vec a\\cdot\\vec b}{|\\vec a|\\,|\\vec b|}$'],
|
||||
['Условие перпендикулярности', '$\\vec a\\perp\\vec b$ равносильно $\\vec a\\cdot\\vec b=0$'],
|
||||
['Условие коллинеарности', '$\\vec a\\parallel\\vec b$ равносильно $a_x b_y-a_y b_x=0$'],
|
||||
['Координаты середины отрезка', 'Середина $AB$: $\\left(\\dfrac{x_A+x_B}{2};\\ \\dfrac{y_A+y_B}{2}\\right)$'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Теория вероятностей и комбинаторика', color: '#F3722C',
|
||||
descr: 'Классическая вероятность, противоположное событие, перестановки, размещения, сочетания.',
|
||||
cards: [
|
||||
['Классическое определение вероятности', '$P=\\dfrac{m}{n}$ — благоприятные исходы делить на все равновозможные'],
|
||||
['Границы вероятности', '$0\\le P\\le1$; $P=0$ — невозможное событие, $P=1$ — достоверное'],
|
||||
['Вероятность противоположного события', '$P(\\bar A)=1-P(A)$'],
|
||||
['Факториал', '$n!=1\\cdot2\\cdot3\\cdots n$; по определению $0!=1$'],
|
||||
['Перестановки из $n$', '$P_n=n!$ — число способов упорядочить $n$ различных элементов'],
|
||||
['Размещения', '$A_n^k=\\dfrac{n!}{(n-k)!}$ — упорядоченные выборки $k$ из $n$'],
|
||||
['Сочетания', '$C_n^k=\\dfrac{n!}{k!\\,(n-k)!}$ — неупорядоченные выборки $k$ из $n$'],
|
||||
['Сложение вероятностей (несовместные)', '$P(A+B)=P(A)+P(B)$ для несовместных событий'],
|
||||
['Умножение вероятностей (независимые)', '$P(A\\cdot B)=P(A)\\cdot P(B)$ для независимых событий'],
|
||||
]},
|
||||
|
||||
{ title: 'ЦТ · Параметры', color: '#277DA1',
|
||||
descr: 'Что значит решить с параметром, разбор по случаям, дискриминант и графический метод.',
|
||||
cards: [
|
||||
['Что значит «решить с параметром»', 'Для КАЖДОГО значения параметра $a$ описать все решения (или их число). Ответ даётся по случаям относительно $a$'],
|
||||
['Линейное уравнение $ax=b$', 'При $a\\neq0$: единственное $x=\\dfrac{b}{a}$. При $a=0,\\ b=0$: любое $x$. При $a=0,\\ b\\neq0$: решений нет'],
|
||||
['Квадратное с параметром — число корней', 'Анализировать знак дискриминанта $D$ и условие $a\\neq0$: $D>0$ — два, $D=0$ — один, $D<0$ — нет'],
|
||||
['Графический метод', 'Выразить параметр: $a=f(x)$, построить график $f(x)$ и пересекать прямой $y=a$ — число пересечений равно числу корней'],
|
||||
['Условие «ровно один корень»', 'Часто $D=0$, либо один корень попадает в ОДЗ, а второй — вне. Граничные случаи проверять отдельно'],
|
||||
['Виета в задачах с параметром', 'Условия на корни ($x_1+x_2$, $x_1 x_2$, их знаки) выразить через параметр, не находя сами корни'],
|
||||
]},
|
||||
];
|
||||
|
||||
/* ── self-check: чаще всего KaTeX ломают непарные $ или {} ─────────────────── */
|
||||
let bad = 0;
|
||||
for (const d of DECKS) {
|
||||
d.cards.forEach(([f, b], i) => {
|
||||
[['front', f], ['back', b]].forEach(([side, s]) => {
|
||||
const dollars = (s.match(/\$/g) || []).length;
|
||||
const braces = (s.match(/\{/g) || []).length - (s.match(/\}/g) || []).length;
|
||||
const cyrInMath = (s.match(/\$[^$]*\$/g) || []).some(seg => /[Ѐ-ӿ]/.test(seg));
|
||||
if (dollars % 2 !== 0) { console.error(`✗ непарный $ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (braces !== 0) { console.error(`✗ непарные {} — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
if (cyrInMath) { console.error(`✗ кириллица в $…$ — «${d.title}» #${i + 1} (${side}): ${s}`); bad++; }
|
||||
});
|
||||
});
|
||||
}
|
||||
if (bad) { console.error(`\nСамопроверка: ${bad} проблем — исправь до записи.\n`); process.exit(1); }
|
||||
|
||||
const findDeck = db.prepare('SELECT id FROM flashcard_decks WHERE user_id=? AND title=?');
|
||||
const countCard = db.prepare('SELECT COUNT(*) c FROM flashcard_cards WHERE deck_id=?');
|
||||
const insDeck = db.prepare('INSERT INTO flashcard_decks (user_id,title,description,color) VALUES (?,?,?,?)');
|
||||
const insCard = db.prepare('INSERT INTO flashcard_cards (deck_id,front,back,order_idx) VALUES (?,?,?,?)');
|
||||
|
||||
console.log(`\n=== seed_ctmath_flashcards_p6 (${APPLY ? 'APPLY' : 'DRY-RUN'}) ===`);
|
||||
console.log('владелец user_id =', owner, '\n');
|
||||
|
||||
let plannedDecks = 0, plannedCards = 0;
|
||||
|
||||
for (const d of DECKS) {
|
||||
const ex = findDeck.get(owner, d.title);
|
||||
if (ex) {
|
||||
const have = countCard.get(ex.id).c;
|
||||
if (have > 0) { console.log(`• «${d.title}» (id ${ex.id}) — уже наполнена (${have} карт), пропуск`); continue; }
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» (id ${ex.id}) — пустая, долить ${d.cards.length} карт`);
|
||||
if (APPLY) d.cards.forEach(([f, b], i) => insCard.run(ex.id, f, b, i));
|
||||
continue;
|
||||
}
|
||||
plannedDecks++; plannedCards += d.cards.length;
|
||||
console.log(`+ «${d.title}» — новая колода, ${d.cards.length} карт`);
|
||||
if (APPLY) {
|
||||
const did = insDeck.run(owner, d.title, d.descr, d.color).lastInsertRowid;
|
||||
d.cards.forEach(([f, b], i) => insCard.run(did, f, b, i));
|
||||
console.log(` создана id ${did}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nИтого к ${APPLY ? 'записи' : 'добавлению'}: колод ${plannedDecks}, карт ${plannedCards}.`);
|
||||
if (!APPLY) console.log('DRY-RUN: ничего не записано. Для записи: node backend/scripts/seed_ctmath_flashcards_p6.js --apply\n');
|
||||
else console.log('Готово. Колоды добавлены (владелец — admin; раздать классу — через доступ к колоде).\n');
|
||||
@@ -0,0 +1,176 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Уроки остальных блоков курса «ЦЭ/ЦТ — Математика» (по PLAN.md, шаблон пилотов).
|
||||
* Числа · Преобразования · Уравнения(×2) · Функции · Прогрессии/текстовые ·
|
||||
* Планиметрия · Продвинутое. Форматы блоков — под рендер lesson.html
|
||||
* (text/heading/callout esc-only; математика $…$/$$…$$; callout.style). Идемпотентно.
|
||||
* node backend/scripts/seed_ctmath_lessons_rest.js [--dry]
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const DRY = process.argv.includes('--dry');
|
||||
const COURSE_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 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 = (simId, caption) => ['sim', { simId, caption }];
|
||||
const FC = (front, back) => ['flashcard', { front, back }];
|
||||
const QZ = (question, options, correctIndex) => ['quiz', { question, options, correctIndex }];
|
||||
const PR = () => CI('Тренажёр по теме — в модуле /exam-prep/ctmath (реальные задания ЦТ прошлых лет) и в практике по теме.');
|
||||
|
||||
const LESSONS = [
|
||||
{ section: 'Числа и вычисления', title: 'Числа, делимость и проценты', read: 8, blocks: [
|
||||
H('Числа, делимость и проценты'),
|
||||
P('Действительные числа на координатной прямой нужно уметь оценивать и сравнивать. Деление с остатком записывается формулой ниже.'),
|
||||
F('n = d\\cdot q + r,\\qquad 0\\le r\\lt d', 'Деление с остатком'),
|
||||
P('Проценты: $p\\%$ числа $a$ равно $\\dfrac{p}{100}\\cdot a$. Увеличение на $p\\%$ — умножение на $\\left(1+\\dfrac{p}{100}\\right)$, уменьшение — на $\\left(1-\\dfrac{p}{100}\\right)$.'),
|
||||
F('\\text{НОД}(a,b)\\cdot\\text{НОК}(a,b)=a\\cdot b', 'Связь НОД и НОК'),
|
||||
H('Разбор А4', 3),
|
||||
P('Делитель $15$, неполное частное $k$, остаток $7$. Тогда делимое $n=15k+7$.'),
|
||||
CS('Ответ: $n=15k+7$.'),
|
||||
H('Разбор (проценты)', 3),
|
||||
P('$15\\%$ числа равны $33$. Число $=33:0{,}15=220$, а $20\\%$ от него $=44$.'),
|
||||
CS('Ответ: $44$.'),
|
||||
FC('Деление с остатком', '$n=dq+r,\\ 0\\le r<d$'),
|
||||
FC('$\\text{НОД}\\cdot\\text{НОК}$', '$a\\cdot b$'),
|
||||
FC('$p\\%$ от $a$', '$\\dfrac{p}{100}\\,a$'),
|
||||
QZ('20% некоторого числа равны 40. Само число равно:', ['200', '80', '160', '20'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Алгебраические преобразования', title: 'Степени, корни, дроби', read: 9, blocks: [
|
||||
H('Преобразования выражений: степени, корни, дроби'),
|
||||
F('a^m\\cdot a^n=a^{m+n},\\quad (a^m)^n=a^{mn},\\quad a^{-n}=\\dfrac{1}{a^n},\\quad a^{m/n}=\\sqrt[n]{a^m}', 'Степени'),
|
||||
F('\\sqrt[n]{ab}=\\sqrt[n]{a}\\,\\sqrt[n]{b},\\qquad \\sqrt[n]{a^n}=|a|\\ (n\\text{ — чётное})', 'Корни'),
|
||||
F('(a\\pm b)^2=a^2\\pm2ab+b^2,\\qquad a^2-b^2=(a-b)(a+b)', 'Формулы сокращённого умножения'),
|
||||
P('ОДЗ выражения: под корнем чётной степени — неотрицательное число; знаменатель не равен нулю; аргумент логарифма положителен.'),
|
||||
CW('В задании А10 проверяют именно ОДЗ: при каком значении выражение имеет смысл.'),
|
||||
H('Разбор А10', 3),
|
||||
P('При $a=-4$ из $\\sqrt{a}$, $\\sqrt[3]{a}$, $\\dfrac{1}{a+4}$ смысл имеет только $\\sqrt[3]{a}$: корень нечётной степени из отрицательного определён; $\\sqrt{-4}$ — нет; $\\dfrac{1}{0}$ — деление на ноль.'),
|
||||
CS('Ответ: $\\sqrt[3]{a}$.'),
|
||||
FC('$a^m\\cdot a^n$', '$a^{m+n}$'),
|
||||
FC('$a^{m/n}$', '$\\sqrt[n]{a^m}$'),
|
||||
FC('$a^2-b^2$', '$(a-b)(a+b)$'),
|
||||
QZ('Значение $a^{1/2}$ при $a=9$:', ['3', '4,5', '81', '18'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Уравнения и неравенства', title: 'Квадратные, рациональные, модуль', read: 11, blocks: [
|
||||
H('Квадратные и рациональные уравнения и неравенства. Модуль'),
|
||||
F('x_{1,2}=\\dfrac{-b\\pm\\sqrt{D}}{2a},\\ \\ D=b^2-4ac;\\qquad x_1x_2=\\dfrac{c}{a},\\ \\ x_1+x_2=-\\dfrac{b}{a}', 'Квадратное уравнение и теорема Виета'),
|
||||
SIM('quadratic', 'Корни квадратного уравнения и дискриминант'),
|
||||
P('Метод интервалов: разложить на множители, отметить нули, расставить знаки по промежуткам. Учитывать кратность корня (при чётной кратности знак не меняется).'),
|
||||
F('|x|=a\\Rightarrow x=\\pm a\\ (a\\ge0);\\qquad |f(x)|\\lt a\\Leftrightarrow -a\\lt f(x)\\lt a', 'Модуль'),
|
||||
CI('Двойное неравенство $a\\le f(x)<b$ решают как систему; целые решения отбирают на полученном промежутке.'),
|
||||
H('Разбор А5', 3),
|
||||
P('Произведение действительных корней уравнения $x^2-5x+6=0$ по теореме Виета равно $6$ (корни $2$ и $3$).'),
|
||||
CS('Ответ: $6$.'),
|
||||
H('Разбор (целые решения)', 3),
|
||||
P('Сколько целых решений у неравенства $-4<2x-1\\le5$? Имеем $-1{,}5<x\\le3$, то есть $x\\in\\{-1,0,1,2,3\\}$ — пять решений.'),
|
||||
CS('Ответ: $5$.'),
|
||||
FC('Дискриминант', '$D=b^2-4ac$'),
|
||||
FC('Виет: сумма и произведение корней', '$x_1+x_2=-\\dfrac{b}{a},\\ x_1x_2=\\dfrac{c}{a}$'),
|
||||
FC('$|f(x)|<a$', '$-a<f(x)<a$'),
|
||||
QZ('Сумма корней уравнения $x^2-7x+12=0$:', ['7', '12', '-7', '3'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Уравнения и неравенства', title: 'Показательные, логарифмические, иррациональные', read: 12, blocks: [
|
||||
H('Показательные, логарифмические, иррациональные уравнения и неравенства'),
|
||||
F('a^{f}=a^{g}\\Leftrightarrow f=g;\\qquad \\log_a f=\\log_a g\\Leftrightarrow f=g\\gt 0', 'Равносильные переходы'),
|
||||
F('\\sqrt{f}=g\\ \\Leftrightarrow\\ \\begin{cases}g\\ge0\\\\ f=g^2\\end{cases}', 'Иррациональное уравнение'),
|
||||
CI('Метод рационализации (для неравенств): знак $\\log_a f-\\log_a g$ совпадает со знаком $(a-1)(f-g)$; знак $a^{f}-a^{g}$ — со знаком $(a-1)(f-g)$. Экономит время на сложных неравенствах.'),
|
||||
CW('В логарифмических всегда выписывайте ОДЗ: аргумент $>0$, основание $>0$ и $\\ne1$.'),
|
||||
H('Разбор В11', 3),
|
||||
P('$\\log_2^2 x-3\\log_2 x+2=0$. Замена $t=\\log_2 x$: $t^2-3t+2=0$, $t=1$ или $t=2$, откуда $x=2$ или $x=4$; их произведение $8$.'),
|
||||
CS('Ответ: $8$.'),
|
||||
H('Разбор В14', 3),
|
||||
P('Наименьшее целое решение неравенства $3^{x}>9$: так как основание $>1$, получаем $x>2$, наименьшее целое $x=3$.'),
|
||||
CS('Ответ: $3$.'),
|
||||
FC('$a^{f}=a^{g}$', '$f=g$'),
|
||||
FC('$\\log_a f=\\log_a g$', '$f=g>0$'),
|
||||
FC('Знак $\\log_a f-\\log_a g$ (рационализация)', 'как у $(a-1)(f-g)$'),
|
||||
QZ('$\\log_3 81$ равно:', ['4', '3', '27', '9'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Функции и производная', title: 'Функции, графики, производная', read: 11, blocks: [
|
||||
H('Функции: свойства, графики, производная'),
|
||||
P('Ключевые свойства: ОДЗ, чётность (если $f(-x)=f(x)$ — чётная, график симметричен относительно $Oy$; если $f(-x)=-f(x)$ — нечётная), монотонность, нули.'),
|
||||
SIM('graphtransform', 'Преобразования графиков: сдвиги и растяжения'),
|
||||
F('f\'\\gt 0\\Rightarrow\\text{возрастает};\\quad f\'\\lt 0\\Rightarrow\\text{убывает};\\quad f\'=0\\ \\text{со сменой знака}\\Rightarrow\\text{экстремум}', 'Производная и поведение функции'),
|
||||
H('Разбор В2 (квадратичная)', 3),
|
||||
P('$f(x)=x^2-6x+5$: нули $1$ и $5$ (их сумма $6$); $f(0)=5$; вершина при $x=3$, наименьшее значение $f(3)=-4$.'),
|
||||
CS('Сумма нулей $=6$; наименьшее значение $=-4$.'),
|
||||
H('Разбор В19 (производная)', 3),
|
||||
P('$f(x)=x^3-3x^2+5$: $f\'(x)=3x^2-6x=3x(x-2)$; функция возрастает на $(-\\infty;0]$ и $[2;+\\infty)$, убывает на $[0;2]$.'),
|
||||
CS('Промежутки возрастания: $(-\\infty;0]$ и $[2;+\\infty)$.'),
|
||||
FC('Чётная функция', '$f(-x)=f(x)$, симметрия относительно $Oy$'),
|
||||
FC('$(x^n)\'$', '$n x^{n-1}$'),
|
||||
FC('Признак возрастания', '$f\'(x)>0$'),
|
||||
QZ('Функция $y=x^2$ является:', ['чётной', 'нечётной', 'ни чётной, ни нечётной', 'периодической'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Прогрессии и текстовые задачи', title: 'Прогрессии и текстовые задачи', read: 10, blocks: [
|
||||
H('Прогрессии и текстовые задачи'),
|
||||
F('a_n=a_1+(n-1)d,\\qquad S_n=\\dfrac{a_1+a_n}{2}\\,n', 'Арифметическая прогрессия'),
|
||||
F('b_n=b_1 q^{\\,n-1},\\qquad S_n=\\dfrac{b_1(q^{n}-1)}{q-1}\\ (q\\ne1)', 'Геометрическая прогрессия'),
|
||||
P('Текстовые задачи: проценты; движение ($s=vt$); работа (производительность $=\\dfrac{1}{t}$); смеси и сплавы (масса вещества $=$ доля $\\times$ масса смеси).'),
|
||||
H('Разбор В6', 3),
|
||||
P('$b_3=12$, $b_5=48$ (знаменатель положителен): $q^2=\\dfrac{48}{12}=4$, $q=2$, $b_1=\\dfrac{12}{4}=3$. Сумма первых четырёх членов $3+6+12+24=45$.'),
|
||||
CS('Ответ: $45$.'),
|
||||
H('Разбор (сплавы)', 3),
|
||||
P('Сплав массой $200$ г содержит $30\\%$ меди. Масса меди $=0{,}3\\cdot200=60$ г. На таких долях строятся уравнения смесей.'),
|
||||
FC('$n$-й член арифм. прогрессии', '$a_n=a_1+(n-1)d$'),
|
||||
FC('Сумма геом. прогрессии', '$S_n=\\dfrac{b_1(q^n-1)}{q-1}$'),
|
||||
FC('Путь', '$s=v\\cdot t$'),
|
||||
QZ('В арифметической прогрессии $a_1=2$, $d=3$. Тогда $a_4$ равно:', ['11', '14', '8', '9'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Планиметрия', title: 'Треугольники, четырёхугольники, окружность', read: 11, blocks: [
|
||||
H('Планиметрия: треугольники, четырёхугольники, окружность'),
|
||||
F('S_\\triangle=\\tfrac12 a h_a=\\tfrac12 ab\\sin C;\\qquad \\dfrac{a}{\\sin A}=2R;\\qquad c^2=a^2+b^2-2ab\\cos C', 'Треугольник'),
|
||||
SIM('triangle', 'Геометрия треугольника'),
|
||||
P('Прямоугольный треугольник: гипотенуза $=2R$ (радиус описанной окружности). Правильный $n$-угольник связывает сторону, радиус описанной $R$ и вписанной $r$ окружностей.'),
|
||||
CI('Вписанный угол равен половине центрального, опирающегося на ту же дугу.'),
|
||||
H('Разбор В5', 3),
|
||||
P('В прямоугольном треугольнике радиус описанной окружности $R=13$, один катет $10$. Гипотенуза $=2R=26$, второй катет $=\\sqrt{26^2-10^2}=\\sqrt{576}=24$.'),
|
||||
CS('Ответ: $24$.'),
|
||||
H('Разбор В10 (правильный шестиугольник)', 3),
|
||||
P('У правильного шестиугольника со стороной $a$: $R=a$, $r=\\dfrac{\\sqrt3}{2}a$, площадь $S=\\dfrac{3\\sqrt3}{2}a^2$.'),
|
||||
FC('Площадь треугольника', '$\\tfrac12 ab\\sin C$'),
|
||||
FC('Теорема синусов', '$\\dfrac{a}{\\sin A}=2R$'),
|
||||
FC('Вписанный угол', 'половина центрального на ту же дугу'),
|
||||
QZ('Гипотенуза прямоугольного треугольника, вписанного в окружность радиуса 5, равна:', ['10', '5', '2,5', '25'], 0),
|
||||
PR(),
|
||||
]},
|
||||
{ section: 'Продвинутое и комбинированное', title: 'Параметры и комбинированные задачи', read: 10, blocks: [
|
||||
H('Задачи с параметрами и комбинированные задачи'),
|
||||
P('Параметр — буква, от которой зависит ответ. Два подхода: аналитический (исследовать решение по параметру) и графический (семейство графиков и их пересечения).'),
|
||||
CI('Частый приём: выразить параметр $a=\\varphi(x)$ и смотреть, сколько решений даёт горизонтальная прямая $y=a$ (число пересечений с графиком $\\varphi$).'),
|
||||
P('Комбинированные задачи смешивают темы (алгебра и геометрия, прогрессии и проценты). Стратегия: разбить на подзадачи, аккуратно следя за ОДЗ и единицами.'),
|
||||
CI('Продвинутый уровень подробно — в плане курса (Сканави, Высоцкий «Параметры», Прасолов). Здесь — общая стратегия и ориентиры.'),
|
||||
FC('Графический метод для параметра', '$a=\\varphi(x)$; число решений = число пересечений $y=a$ с графиком'),
|
||||
FC('Уравнение $x^2=a$: число решений', '$a>0$ — два, $a=0$ — одно, $a<0$ — нет'),
|
||||
QZ('При каком $a$ уравнение $x^2=a$ имеет ровно одно решение?', ['a=0', 'a>0', 'a<0', 'при любом'], 0),
|
||||
PR(),
|
||||
]},
|
||||
];
|
||||
|
||||
console.log(DRY ? '[DRY-RUN]' : '[APPLY]', 'курс id=', course.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 (?,?,?,?)');
|
||||
const secOrder = {};
|
||||
for (const L of LESSONS) {
|
||||
const sec = db.prepare('SELECT id FROM course_sections WHERE course_id=? AND title=?').get(course.id, L.section);
|
||||
if (!sec) { console.log(` [skip] нет секции «${L.section}»`); continue; }
|
||||
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}) — пропуск`); continue; }
|
||||
secOrder[sec.id] = (secOrder[sec.id] || 0) + 1;
|
||||
if (DRY) { console.log(` + [${L.section}] «${L.title}» (${L.blocks.length} блоков)`); continue; }
|
||||
const lid = insLesson.run(course.id, L.title, secOrder[sec.id], sec.id, L.read).lastInsertRowid;
|
||||
L.blocks.forEach(([type, data], bi) => insBlock.run(lid, type, bi, JSON.stringify(data)));
|
||||
console.log(` + [${L.section}] «${L.title}» (id ${lid}, ${L.blocks.length} блоков)`);
|
||||
}
|
||||
console.log(DRY ? 'DRY-RUN: ничего не записано.' : 'Готово. Уроки остальных блоков добавлены (черновик курса).');
|
||||
@@ -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: ничего не записано.' : 'Готово. Уроки стереометрии добавлены (черновик курса).');
|
||||
@@ -0,0 +1,136 @@
|
||||
'use strict';
|
||||
/*
|
||||
* Уроки блока «Тригонометрия» курса «ЦЭ/ЦТ — Математика» (по PILOT_TRIGONOMETRY.md).
|
||||
* Создаёт 3 урока (круг → тождества → уравнения) в секции «Тригонометрия» курса.
|
||||
* Форматы блоков — РОВНО под рендер frontend/lesson.html (text/heading/callout
|
||||
* экранируются → только текст; математика через $...$ / $$...$$; callout.style
|
||||
* = info|warning|success|error). data хранится JSON-строкой (API её парсит).
|
||||
* ИДЕМПОТЕНТЕН: урок с тем же title в курсе не создаётся повторно.
|
||||
* Запуск: node backend/scripts/seed_ctmath_lessons_trig.js [--dry]
|
||||
*/
|
||||
const db = require('../src/db/db');
|
||||
const DRY = process.argv.includes('--dry');
|
||||
|
||||
const COURSE_TITLE = 'ЦЭ/ЦТ — Математика';
|
||||
const SECTION_TITLE = 'Тригонометрия';
|
||||
|
||||
const course = db.prepare("SELECT id FROM courses WHERE subject_slug='math' AND title=?").get(COURSE_TITLE);
|
||||
if (!course) { console.error('Нет курса «' + COURSE_TITLE + '». Сначала: node backend/scripts/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 + '» в курсе ' + course.id); process.exit(1); }
|
||||
|
||||
// helpers для краткости описания блоков
|
||||
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: 'trigcircle', caption }];
|
||||
const FC = (front, back) => ['flashcard', { front, back }];
|
||||
const QZ = (question, options, correctIndex) => ['quiz', { question, options, correctIndex }];
|
||||
const ORD= (question, items) => ['ordering', { question, items }];
|
||||
const MAT= (question, pairs) => ['matching', { question, pairs }];
|
||||
const ACC= (title, content) => ['accordion', { title, content }];
|
||||
const TBL= (header, rows) => ['table', { header, rows }];
|
||||
|
||||
// ── Урок 1: Тригонометрический круг и значения (А3, базовый) ──
|
||||
const L1 = [
|
||||
H('Тригонометрический круг: смысл синуса и косинуса'),
|
||||
P('Возьмём окружность радиуса 1 с центром в начале координат. При повороте на угол $\\alpha$ точка на этой окружности получает координаты $(\\cos\\alpha;\\ \\sin\\alpha)$. Это определение, из которого выводится вся тригонометрия: заучивать таблицы наизусть не нужно — нужно уметь «читать» круг.'),
|
||||
F('\\cos\\alpha = x,\\quad \\sin\\alpha = y,\\quad \\operatorname{tg}\\alpha=\\dfrac{y}{x},\\quad \\operatorname{ctg}\\alpha=\\dfrac{x}{y}', 'Определения через единичную окружность'),
|
||||
SIM('Покрутите угол и следите за координатами точки — это и есть $\\cos\\alpha$ и $\\sin\\alpha$'),
|
||||
CI('Знаки по четвертям: I (+,+), II (−,+), III (−,−), IV (+,−). Косинус — это абсцисса, синус — ордината.'),
|
||||
H('Значения для основных углов', 3),
|
||||
TBL(
|
||||
['$\\alpha$', '$0$', '$\\tfrac{\\pi}{6}$', '$\\tfrac{\\pi}{4}$', '$\\tfrac{\\pi}{3}$', '$\\tfrac{\\pi}{2}$'],
|
||||
[
|
||||
['$\\sin\\alpha$', '$0$', '$\\tfrac{1}{2}$', '$\\tfrac{\\sqrt2}{2}$', '$\\tfrac{\\sqrt3}{2}$', '$1$'],
|
||||
['$\\cos\\alpha$', '$1$', '$\\tfrac{\\sqrt3}{2}$', '$\\tfrac{\\sqrt2}{2}$', '$\\tfrac{1}{2}$', '$0$'],
|
||||
['$\\operatorname{tg}\\alpha$', '$0$', '$\\tfrac{\\sqrt3}{3}$', '$1$', '$\\sqrt3$', '—'],
|
||||
]
|
||||
),
|
||||
F('\\sin x = 0 \\iff x=\\pi k;\\qquad \\cos x = 0 \\iff x=\\tfrac{\\pi}{2}+\\pi k', 'Когда функция равна нулю'),
|
||||
CW('Типичная ошибка — путать, где ноль у синуса (при $0,\\ \\pi,\\ 2\\pi,\\dots$) и у косинуса (при $\\tfrac{\\pi}{2},\\ \\tfrac{3\\pi}{2},\\dots$). На круге это видно сразу: синус — высота, косинус — горизонталь.'),
|
||||
FC('$\\sin x = 0$ при каких $x$?', '$x = \\pi k,\\ k\\in\\mathbb{Z}$'),
|
||||
FC('$\\cos x = 0$ при каких $x$?', '$x = \\tfrac{\\pi}{2}+\\pi k$'),
|
||||
H('Разбор задания А3', 3),
|
||||
P('Типичное А3: среди нескольких значений аргумента указать то, при котором функция равна нулю.'),
|
||||
P('Пример. Среди $-\\tfrac{\\pi}{6};\\ \\tfrac{\\pi}{4};\\ \\tfrac{\\pi}{3};\\ -\\tfrac{3\\pi}{2};\\ -6\\pi$ укажите то, при котором $\\sin x = 0$.'),
|
||||
P('Решение. $\\sin x=0$ только когда $x$ кратно $\\pi$. Из списка кратно $\\pi$ лишь $-6\\pi$.'),
|
||||
CS('Ответ: $-6\\pi$.'),
|
||||
QZ('При каком значении аргумента cos x = 1?', ['π/2', 'π', '0', '3π/2'], 2),
|
||||
CI('Тренажёр по теме «Тригонометрия» (реальные задания А3 прошлых лет) — в практике курса. Цель освоения: не менее 90% на заданиях А3.'),
|
||||
];
|
||||
|
||||
// ── Урок 2: Тождества и формулы (А8, В4, средний) ──
|
||||
const L2 = [
|
||||
H('Тождества: как не учить 30 формул'),
|
||||
F('\\sin^2\\alpha+\\cos^2\\alpha=1', 'Основное тригонометрическое тождество'),
|
||||
P('Это теорема Пифагора для точки $(\\cos\\alpha;\\ \\sin\\alpha)$ на единичной окружности. Разделив его на $\\cos^2\\alpha$ и на $\\sin^2\\alpha$, получаем связи с тангенсом и котангенсом — их выводят на месте, а не заучивают.'),
|
||||
F('1+\\operatorname{tg}^2\\alpha=\\dfrac{1}{\\cos^2\\alpha},\\qquad 1+\\operatorname{ctg}^2\\alpha=\\dfrac{1}{\\sin^2\\alpha}'),
|
||||
ACC('Формулы сложения и двойного угла (раскрыть)', 'Сложение: $\\sin(\\alpha\\pm\\beta)=\\sin\\alpha\\cos\\beta\\pm\\cos\\alpha\\sin\\beta$; $\\cos(\\alpha\\pm\\beta)=\\cos\\alpha\\cos\\beta\\mp\\sin\\alpha\\sin\\beta$. Двойной угол: $\\sin 2\\alpha=2\\sin\\alpha\\cos\\alpha$; $\\cos 2\\alpha=\\cos^2\\alpha-\\sin^2\\alpha$. Все они следуют из формул сложения.'),
|
||||
CI('Обратные функции и их области значений (на них ловят в А8): $\\arcsin x\\in[-\\tfrac{\\pi}{2};\\tfrac{\\pi}{2}]$, $\\arccos x\\in[0;\\pi]$, $\\operatorname{arctg} x\\in(-\\tfrac{\\pi}{2};\\tfrac{\\pi}{2})$.'),
|
||||
MAT('Сопоставьте выражение и тождественно равное ему', [
|
||||
{ left: '$\\sin 2\\alpha$', right: '$2\\sin\\alpha\\cos\\alpha$' },
|
||||
{ left: '$\\cos 2\\alpha$', right: '$\\cos^2\\alpha-\\sin^2\\alpha$' },
|
||||
{ left: '$1-\\cos 2\\alpha$', right: '$2\\sin^2\\alpha$' },
|
||||
]),
|
||||
H('Разбор А8 (обратные функции и модуль)', 3),
|
||||
P('Пример. Найдите значение $\\dfrac{38}{\\pi}\\cdot\\arcsin(-1)-|-7|$.'),
|
||||
P('Решение. $\\arcsin(-1)=-\\tfrac{\\pi}{2}$, поэтому $\\dfrac{38}{\\pi}\\cdot\\left(-\\tfrac{\\pi}{2}\\right)=-19$; далее $-19-7=-26$.'),
|
||||
CS('Ответ: $-26$.'),
|
||||
H('Разбор В4 (тождество)', 3),
|
||||
P('Пример. Найдите $\\operatorname{ctg}^2\\alpha$, если $\\sin\\alpha=\\tfrac{1}{5}$.'),
|
||||
P('Решение. $\\cos^2\\alpha=1-\\tfrac{1}{25}=\\tfrac{24}{25}$, поэтому $\\operatorname{ctg}^2\\alpha=\\dfrac{\\cos^2\\alpha}{\\sin^2\\alpha}=\\dfrac{24/25}{1/25}=24$.'),
|
||||
CS('Ответ: $24$.'),
|
||||
FC('$1+\\operatorname{tg}^2\\alpha$', '$\\dfrac{1}{\\cos^2\\alpha}$'),
|
||||
FC('$\\cos 2\\alpha$', '$\\cos^2\\alpha-\\sin^2\\alpha=1-2\\sin^2\\alpha=2\\cos^2\\alpha-1$'),
|
||||
FC('Область значений $\\arccos x$', '$[0;\\ \\pi]$'),
|
||||
CI('Тренажёр: реальные задания А8 и В4 в практике курса. Цель освоения: не менее 85%.'),
|
||||
];
|
||||
|
||||
// ── Урок 3: Уравнения и отбор корней (В15, продвинутый) ──
|
||||
const L3 = [
|
||||
H('Тригонометрические уравнения и отбор корней'),
|
||||
F('\\sin x=a\\Rightarrow x=(-1)^n\\arcsin a+\\pi n;\\quad \\cos x=a\\Rightarrow x=\\pm\\arccos a+2\\pi n;\\quad \\operatorname{tg} x=a\\Rightarrow x=\\operatorname{arctg} a+\\pi n', 'Формулы корней простейших уравнений'),
|
||||
P('Стратегия В15: сначала свести уравнение к произведению или простейшему виду формулами преобразования; затем выписать общие формулы корней; затем отобрать корни, попадающие в заданный промежуток; и наконец выполнить требуемое (например, найти сумму корней).'),
|
||||
ORD('Расставьте шаги решения В15 по порядку', [
|
||||
'Преобразовать уравнение к произведению или простейшему виду',
|
||||
'Выписать общие формулы корней',
|
||||
'Подставить целые n и отобрать корни на заданном промежутке',
|
||||
'Сложить (или иначе обработать) отобранные корни',
|
||||
]),
|
||||
SIM('Отбор корней: отметьте промежуток и проверьте, какие корни в него попадают'),
|
||||
CW('Самая частая потеря баллов в В15 — неполный отбор корней и потеря ОДЗ (для $\\operatorname{tg}$ и $\\operatorname{ctg}$). Проверяйте оба семейства корней.'),
|
||||
H('Разбор простого примера', 3),
|
||||
P('Найдите (в градусах) сумму корней уравнения $\\cos 2x=0$ на промежутке $(0^\\circ;\\ 180^\\circ)$.'),
|
||||
P('Решение. $\\cos 2x=0\\Rightarrow 2x=90^\\circ+180^\\circ k\\Rightarrow x=45^\\circ+90^\\circ k$. На промежутке лежат $45^\\circ$ и $135^\\circ$. Их сумма равна $180$.'),
|
||||
CS('Ответ: $180$.'),
|
||||
ACC('Более сложный пример (В15 из ЦЭ-2024) — раскрыть', 'Найдите сумму различных корней уравнения $2\\sin 3x\\cos 3x-\\sin 6x\\sin 10x=0$ на промежутке $(-150^\\circ;-55^\\circ)$. Идея: $2\\sin 3x\\cos 3x=\\sin 6x$, выносим общий множитель: $\\sin 6x\\,(1-\\sin 10x)=0$. Дальше решаем $\\sin 6x=0$ или $\\sin 10x=1$ и отбираем корни на промежутке.'),
|
||||
FC('$\\sin x=a$ (корни)', '$x=(-1)^n\\arcsin a+\\pi n$'),
|
||||
FC('$\\cos x=a$ (корни)', '$x=\\pm\\arccos a+2\\pi n$'),
|
||||
FC('$\\operatorname{tg} x=a$ (корни)', '$x=\\operatorname{arctg} a+\\pi n$'),
|
||||
CI('Тренажёр: тема «Тригонометрические уравнения» (В15) в практике курса. Цель освоения: не менее 70%, отбор корней без потерь.'),
|
||||
];
|
||||
|
||||
const LESSONS = [
|
||||
{ title: 'Тригонометрический круг и значения', read: 9, blocks: L1 },
|
||||
{ title: 'Тождества и формулы', read: 10, blocks: L2 },
|
||||
{ title: 'Уравнения и отбор корней', read: 11, blocks: L3 },
|
||||
];
|
||||
|
||||
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, 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: ничего не записано.' : 'Готово. Уроки тригонометрии добавлены в курс (черновик; ученикам видны после публикации курса).');
|
||||
@@ -0,0 +1,400 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2223_e1v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2022/2023, Этап I, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
|
||||
Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\РТ\2022-2023\МАТ РТ-1 22_23 В1.pdf
|
||||
|
||||
variant=107 — РТ-2022/23 Этап I (этап II — 108, этап III — 109).
|
||||
Геометрия закодирована текстом. Адаптации заданий-«с-картинкой»:
|
||||
• А5 (выбор графика y=x²−4 среди 5 рисунков) → самодостаточный MC о
|
||||
расположении параболы (та же проверяемая идея, авто-проверка);
|
||||
• В1 (числовой промежуток ↔ изображение на прямой) → сопоставление с
|
||||
словесными описаниями промежутков (ответ тот же: А3Б6В2);
|
||||
• В3 (столбчатая диаграмма) → те же данные в таблице (figure_html),
|
||||
сопоставление вопрос↔месяц (ответ А1Б2В6).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2223_e1v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2223_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 = 107;
|
||||
const PROV = 'РТ–2022/2023, Этап I, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── figure_html для В3: данные диаграммы в виде таблицы (П — пробная версия,
|
||||
ПЛ — купившие лицензию), по месяцам янв–июнь. Только HTML, без KaTeX. */
|
||||
const FIG_B3 = `<table class="task-fig" style="border-collapse:collapse;margin:10px auto;font-size:14px;text-align:center">
|
||||
<tr>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Показатель</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Январь</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Февраль</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Март</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Апрель</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Май</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 10px">Июнь</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">П</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">58000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">66000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">64000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">62000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">50000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">56000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">ПЛ</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">22000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">16000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">14000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">12000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">10000</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 10px">14000</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А10 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Среди чисел $-\dfrac58$, $\ 2$, $\ \dfrac85$, $\ \dfrac58$, $\ -\dfrac85$ выберите положительное число, меньшее единицы.`,
|
||||
opts: mc('$-\dfrac58$', '$2$', '$\dfrac85$', '$\dfrac58$', '$-\dfrac85$'),
|
||||
answer: 'г',
|
||||
sol: R`Положительны числа $2$, $\dfrac85$ и $\dfrac58$. Из них меньше единицы только $\dfrac58$ (так как $2>1$ и $\dfrac85>1$).`,
|
||||
ref: 'Математика, 6 класс, гл. 4, § 3' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Среди данных утверждений укажите номер верного.<br>1) число $0$ — делитель числа $19$;<br>2) число $7$ — делитель числа $37$;<br>3) число $8$ — делитель числа $8$;<br>4) число $3$ — делитель числа $43$;<br>5) число $6$ — делитель числа $26$.`,
|
||||
opts: mc('утверждение 1', 'утверждение 2', 'утверждение 3', 'утверждение 4', 'утверждение 5'),
|
||||
answer: 'в',
|
||||
sol: R`Делитель числа делит его без остатка. $\ 1)$ деление на $0$ не имеет смысла — неверно. $\ 2)$ $37=5\cdot7+2$ — неверно. $\ 3)$ $8:8=1$ — верно. $\ 4)$ $43=14\cdot3+1$ — неверно. $\ 5)$ $26=4\cdot6+2$ — неверно.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 12' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Треугольники $ABC$ и $A_1B_1C_1$ подобны, причём $\angle A=105^\circ$ и $\angle C_1=30^\circ$. Найдите градусную меру угла $B$ треугольника $ABC$.`,
|
||||
opts: mc('$55^\circ$', '$45^\circ$', '$50^\circ$', '$35^\circ$', '$40^\circ$'),
|
||||
answer: 'б',
|
||||
sol: R`У подобных треугольников соответственные углы равны, поэтому $\angle C=\angle C_1=30^\circ$. По теореме о сумме градусных мер углов треугольника $\angle B=180^\circ-\angle A-\angle C=180^\circ-105^\circ-30^\circ=45^\circ$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1,
|
||||
text: R`Среди чисел $-1$, $\ 2$, $\ \dfrac13$, $\ 1$, $\ -\dfrac13$ укажите то, которое является корнем уравнения $1-3x=2$.`,
|
||||
opts: mc('$-1$', '$2$', '$\dfrac13$', '$1$', '$-\dfrac13$'),
|
||||
answer: 'д',
|
||||
sol: R`Из уравнения $1-3x=2$ получаем $-3x=1$, то есть $x=-\dfrac13$. Это число и есть корень.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 15' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`График функции $y=x^{2}-4$ — парабола. Укажите верное утверждение о её расположении на координатной плоскости.`,
|
||||
opts: mc('ветви вверх, вершина в точке $(0;-4)$', 'ветви вниз, вершина в точке $(0;-4)$', 'ветви вверх, вершина в точке $(0;4)$', 'ветви вниз, вершина в точке $(0;4)$', 'ветви вверх, вершина в точке $(4;0)$'),
|
||||
answer: 'а',
|
||||
sol: R`График $y=x^{2}-4$ получается из графика $y=x^{2}$ сдвигом на $4$ единицы вниз вдоль оси ординат. Это парабола с ветвями вверх и вершиной в точке $(0;-4)$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Укажите номера функций, для которых выполняется неравенство $f(0)<-3$.<br>1) $f(x)=x^{2}-6$;<br>2) $f(x)=|x-4|$;<br>3) $f(x)=|x|-3$;<br>4) $f(x)=x^{3}-4$;<br>5) $f(x)=(x-8)^{2}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '14', ansShow: '1, 4',
|
||||
sol: R`Найдём $f(0)$: $\ 1)$ $0-6=-6<-3$ — верно. $\ 2)$ $|-4|=4$ — нет. $\ 3)$ $|0|-3=-3$, не меньше $-3$ — нет. $\ 4)$ $0-4=-4<-3$ — верно. $\ 5)$ $(-8)^{2}=64$ — нет. Подходят функции 1 и 4.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Руда содержит $5\%$ чистого металла. Сколько тонн руды необходимо взять, чтобы получить $13$ т чистого металла?`,
|
||||
opts: mc('$130$', '$230$', '$160$', '$260$', '$390$'),
|
||||
answer: 'г',
|
||||
sol: R`Пусть нужно взять $x$ т руды. Составим пропорцию: $x$ т — $100\%$, $13$ т — $5\%$. Тогда $x=\dfrac{13\cdot100}{5}=260$ (т).`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 1–2' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 1,
|
||||
text: R`Найдите значение выражения $\dfrac{7\sqrt[3]{48}}{\sqrt[3]{6}}$.`,
|
||||
opts: mc('$7\sqrt[3]{6}$', '$14$', '$7\sqrt[3]{2}$', '$7$', '$28$'),
|
||||
answer: 'б',
|
||||
sol: R`$\dfrac{7\sqrt[3]{48}}{\sqrt[3]{6}}=7\sqrt[3]{\dfrac{48}{6}}=7\sqrt[3]{8}=7\cdot2=14$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 2,
|
||||
text: R`Дан прямоугольный параллелепипед $ABCDA_1B_1C_1D_1$. Найдите площадь его диагонального сечения $AA_1C_1C$, если диагональ основания $AC=2\sqrt6$, а диагональ $A_1C=7$.`,
|
||||
opts: mc('$12\sqrt3$', '$4\sqrt{37}$', '$9\sqrt6$', '$14\sqrt6$', '$10\sqrt6$'),
|
||||
answer: 'д',
|
||||
sol: R`Сечение $AA_1C_1C$ — прямоугольник со сторонами $AC$ и боковым ребром $A_1A$. По теореме Пифагора в треугольнике $A_1AC$: $A_1A=\sqrt{A_1C^{2}-AC^{2}}=\sqrt{49-24}=\sqrt{25}=5$. Площадь сечения $AC\cdot A_1A=2\sqrt6\cdot5=10\sqrt6$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 1' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Укажите номера выражений, областью определения которых является множество всех действительных чисел.<br>1) $\sqrt{x}$;<br>2) $\dfrac{1}{1+x}$;<br>3) $2x-1$;<br>4) $\sqrt[3]{x}$;<br>5) $\operatorname{tg}x$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '34', ansShow: '3, 4',
|
||||
sol: R`$1)$ $\sqrt{x}$ определено при $x\ge0$ — нет. $\ 2)$ $\dfrac{1}{1+x}$ не определено при $x=-1$ — нет. $\ 3)$ $2x-1$ определено при всех $x$ — да. $\ 4)$ $\sqrt[3]{x}$ определено при всех $x$ — да. $\ 5)$ $\operatorname{tg}x$ не определено при $x=\dfrac{\pi}{2}+\pi n$ — нет. Подходят 3 и 4.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 4' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'long', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Установите соответствие между числовым промежутком А–В и его описанием 1–6.<br><b>Промежуток:</b><br>А) $(-4{,}2;+\infty)$;<br>Б) $(-\infty;5]$;<br>В) $[-4{,}2;5)$.<br><b>Описание:</b><br>1) открытый луч, направленный влево от точки $5$;<br>2) полуинтервал от $-4{,}2$ (включая) до $5$ (не включая);<br>3) открытый луч, направленный вправо от точки $-4{,}2$;<br>4) отрезок от $-4{,}2$ до $5$ (обе границы включены);<br>5) луч, направленный вправо от точки $-4{,}2$, включая её;<br>6) луч, направленный влево от точки $5$, включая её.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А3Б6В2', ansShow: 'А3Б6В2',
|
||||
sol: R`А) $(-4{,}2;+\infty)$ — открытый луч вправо от $-4{,}2$ (граница не включена) — описание 3. Б) $(-\infty;5]$ — луч влево, точка $5$ включена — описание 6. В) $[-4{,}2;5)$ — от $-4{,}2$ (включая) до $5$ (не включая) — описание 2.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 5' },
|
||||
|
||||
{ idx: 12, type: 'open', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Выберите верные утверждения.<br>1) выражение $\dfrac27 a^{2}b^{5}$ является одночленом;<br>2) выражения $5ab^{4}$ и $5a^{5}b$ являются одночленами пятой степени;<br>3) коэффициент одночлена $x\cdot3^{2}$ равен $1$;<br>4) при $x=0{,}5$ и $y=-1$ значение одночлена $-6xy^{3}$ равно $3$;<br>5) степень одночлена $-3ab^{2}c^{4}$ равна $7$;<br>6) выражение $-6x^{0{,}5}y^{3}$ является одночленом.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '145', ansShow: '1, 4, 5',
|
||||
sol: R`$1)$ верно: $\dfrac27 a^{2}b^{5}$ — произведение числа и натуральных степеней переменных. $\ 2)$ неверно: степень $5ab^{4}$ равна $5$, а $5a^{5}b$ — равна $6$. $\ 3)$ неверно: $x\cdot3^{2}=9x$, коэффициент $9$. $\ 4)$ верно: $-6\cdot0{,}5\cdot(-1)^{3}=3$. $\ 5)$ верно: степень $-3ab^{2}c^{4}$ равна $1+2+4=7$. $\ 6)$ неверно: $x^{0{,}5}$ — не натуральная степень.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 13, type: 'long', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
fig: FIG_B3,
|
||||
text: R`В таблице приведено количество пользователей пробной версии (П) программного обеспечения и количество пользователей, купивших лицензию (ПЛ), за период шесть месяцев (с января по июнь). Установите соответствие между вопросами А–В и ответами 1–6.<br><b>Вопрос:</b><br>А) В каком месяце количество пользователей пробной версии составило $58000$?<br>Б) В каком месяце количество пользователей, купивших лицензию, равнялось $16000$?<br>В) В каком месяце количество пользователей, купивших лицензию, составило $25\%$ от количества пользователей пробной версии?<br><b>Ответ:</b><br>1) Январь; 2) Февраль; 3) Март; 4) Апрель; 5) Май; 6) Июнь.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А1Б2В6', ansShow: 'А1Б2В6',
|
||||
sol: R`А) Пробная версия равна $58000$ в январе — ответ 1. Б) Купивших лицензию $16000$ в феврале — ответ 2. В) Отношение купивших лицензию к пользователям пробной версии равно $25\%$ в июне: $\dfrac{14000}{56000}=0{,}25$ — ответ 6. (В остальных месяцах отношение иное: январь $\approx38\%$, май $20\%$ и т. д.)`,
|
||||
ref: 'Математика, 5 класс, ч. 2, гл. 3, § 16' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 2,
|
||||
text: R`Дана правильная призма, имеющая $12$ вершин. Выберите верные утверждения.<br>1) количество всех граней данной призмы равно $7$;<br>2) количество всех рёбер данной призмы равно $18$;<br>3) количество боковых граней данной призмы равно $6$;<br>4) градусная мера внутреннего угла основания данной призмы равна $120^\circ$;<br>5) количество боковых рёбер данной призмы равно $12$;<br>6) диагональным сечением данной призмы является шестиугольник.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '234', ansShow: '2, 3, 4',
|
||||
sol: R`$12$ вершин $\Rightarrow$ в основаниях правильные шестиугольники (по $6$ вершин). $\ 1)$ неверно: граней $6+2=8$. $\ 2)$ верно: рёбер $6+6+6=18$. $\ 3)$ верно: боковых граней $6$. $\ 4)$ верно: внутренний угол правильного шестиугольника $\dfrac{180^\circ\cdot4}{6}=120^\circ$. $\ 5)$ неверно: боковых рёбер $6$. $\ 6)$ неверно: диагональное сечение призмы — четырёхугольник.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 1' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В равнобедренном треугольнике $ABC$ с основанием $AC$ известно, что $\angle BAC=34^\circ$. Через вершину $A$ проведён луч $AD$ так, что $AD\parallel BC$ (точки $D$ и $B$ лежат по одну сторону от прямой $AC$). Угол $1$ образован лучами $AD$ и $AB$. Найдите градусную меру угла $1$.`,
|
||||
answer: '112',
|
||||
sol: R`Углы при основании равнобедренного треугольника равны: $\angle ACB=\angle BAC=34^\circ$. По теореме о сумме углов $\angle ABC=180^\circ-2\cdot34^\circ=112^\circ$. Угол $1$ и угол $ABC$ — накрест лежащие при $AD\parallel BC$ и секущей $AB$, поэтому $\angle 1=\angle ABC=112^\circ$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 2, § 11; гл. 3, § 17' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\dfrac{72}{\pi}\cdot\operatorname{arctg}\left(-\dfrac{\sqrt3}{3}\right)$.`,
|
||||
answer: '-12',
|
||||
sol: R`$\operatorname{arctg}\left(-\dfrac{\sqrt3}{3}\right)=-\operatorname{arctg}\dfrac{\sqrt3}{3}=-\dfrac{\pi}{6}$. Тогда $\dfrac{72}{\pi}\cdot\left(-\dfrac{\pi}{6}\right)=-12$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 7' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Найдите третий член арифметической прогрессии, у которой сумма $n$ первых членов выражается формулой $S_n=\dfrac{11-3n}{2}\cdot n$.`,
|
||||
answer: '-2',
|
||||
sol: R`$a_3=S_3-S_2$. $\ S_2=\dfrac{11-6}{2}\cdot2=5$; $\ S_3=\dfrac{11-9}{2}\cdot3=3$. Тогда $a_3=3-5=-2$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 15–16' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 3,
|
||||
text: R`Света купила $6$ ручек и $5$ карандашей, а Коля купил $4$ такие же по цене ручки и $8$ таких же по цене карандашей и заплатил на $1$ рубль $60$ копеек меньше, чем Света. Сколько копеек заплатила за покупку Света, если карандаш дешевле ручки на $90$ копеек?`,
|
||||
answer: '760',
|
||||
sol: R`Пусть ручка стоит $x$ коп., карандаш $y$ коп. Тогда $\begin{cases}(6x+5y)-(4x+8y)=160,\\x-y=90,\end{cases}$ то есть $\begin{cases}2x-3y=160,\\x-y=90.\end{cases}$ Отсюда $y=20$, $x=110$. Света заплатила $6\cdot110+5\cdot20=760$ (коп.).`,
|
||||
ref: 'Алгебра, 7 класс, гл. 4, § 25' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите произведение наибольшего целого отрицательного и наименьшего целого положительного решений неравенства $(0{,}2)^{\,2x^{2}-72}<1$.`,
|
||||
answer: '-49',
|
||||
sol: R`Так как $1=(0{,}2)^{0}$, а основание $0{,}2<1$ (функция убывает), неравенство равносильно $2x^{2}-72>0$, то есть $x^{2}>36$, откуда $x<-6$ или $x>6$. Наибольшее целое отрицательное решение $-7$, наименьшее целое положительное $7$; их произведение $-49$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
|
||||
text: R`Длина одной из сторон параллелограмма на $5$ больше длины другой стороны, а высоты, проведённые к этим сторонам, равны $8$ и $12$. Найдите площадь параллелограмма.`,
|
||||
answer: '120',
|
||||
sol: R`Пусть меньшая сторона равна $x$, тогда большая $x+5$. К большей стороне проведена меньшая высота $8$, к меньшей — большая $12$. Площадь $S=ah$ одна и та же: $(x+5)\cdot8=x\cdot12$, $8x+40=12x$, $x=10$. Тогда $S=x\cdot12=120$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 2,
|
||||
text: R`Найдите $f'(-1)$ для функции $f(x)=\dfrac{x^{3}}{3}-1{,}5x^{2}+13x-2022$.`,
|
||||
answer: '17',
|
||||
sol: R`$f'(x)=\dfrac13\cdot3x^{2}-1{,}5\cdot2x+13=x^{2}-3x+13$. Тогда $f'(-1)=(-1)^{2}-3\cdot(-1)+13=1+3+13=17$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 19' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите наименьшее целое решение неравенства $\dfrac{x-3}{(x+14)(x-6)}\ge0$.`,
|
||||
answer: '-13',
|
||||
sol: R`Нуль числителя $x=3$; при $x=-14$ и $x=6$ значения не существуют. Методом интервалов решение неравенства — множество $(-14;3]\cup(6;+\infty)$. Наименьшее целое решение равно $-13$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 4,
|
||||
text: R`Дана правильная четырёхугольная пирамида $SABCD$, у которой диагональ основания $AC=2\sqrt{10}$, а угол между боковым ребром и плоскостью основания $\angle SCO=60^\circ$ ($O$ — центр основания). Найдите значение выражения $S^{2}$, где $S$ — площадь боковой поверхности пирамиды.`,
|
||||
answer: '2800',
|
||||
sol: R`В основании квадрат $ABCD$ с диагональю $2\sqrt{10}$, значит сторона $AD=2\sqrt5$. Высота $SO=OC\cdot\operatorname{tg}60^\circ=\dfrac{AC}{2}\cdot\sqrt3=\sqrt{10}\cdot\sqrt3=\sqrt{30}$. Апофема $SK=\sqrt{SO^{2}+\left(\dfrac{AD}{2}\right)^{2}}=\sqrt{30+5}=\sqrt{35}$. Площадь боковой поверхности $S=\dfrac12\cdot P\cdot SK$, где $P=4\cdot AD$ — периметр основания: $S=2\cdot AD\cdot SK=2\cdot2\sqrt5\cdot\sqrt{35}=20\sqrt7$. Тогда $S^{2}=400\cdot7=2800$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений совокупности неравенств $\left[\begin{array}{l}x+6\le0,\\-2-x\ge0,\end{array}\right.$ принадлежащих промежутку $[-7;3]$.`,
|
||||
answer: '-27',
|
||||
sol: R`$x+6\le0\Rightarrow x\le-6$; $\ -2-x\ge0\Rightarrow x\le-2$. Объединение лучей — множество $(-\infty;-2]$. Пересечение с $[-7;3]$ — отрезок $[-7;-2]$. Целые решения $-7,-6,-5,-4,-3,-2$; их сумма равна $-27$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите сумму квадратов корней уравнения $\dfrac{x^{2}+7\sqrt3\,x-15}{(x-7)^{2}}=0$.`,
|
||||
answer: '177',
|
||||
sol: R`Дробь равна нулю, когда числитель равен нулю, а знаменатель отличен от нуля: $x^{2}+7\sqrt3\,x-15=0$ при $x\ne7$. По теореме Виета $x_1+x_2=-7\sqrt3$, $x_1x_2=-15$. Тогда $x_1^{2}+x_2^{2}=(x_1+x_2)^{2}-2x_1x_2=\left(7\sqrt3\right)^{2}+30=147+30=177$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2, § 9' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите (в градусах) корень уравнения $\operatorname{tg}5x=1$ на промежутке $(0^\circ;45^\circ)$.`,
|
||||
answer: '9',
|
||||
sol: R`$\operatorname{tg}5x=1\Rightarrow 5x=45^\circ+180^\circ n$, откуда $x=9^\circ+36^\circ n$. Промежутку $(0^\circ;45^\circ)$ принадлежит только корень $9^\circ$ (при $n=0$).`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Решите уравнение $\sqrt{x-1}+3\sqrt[4]{x-1}-18=0$. В ответ запишите его корень (произведение корней, если их несколько).`,
|
||||
answer: '82',
|
||||
sol: R`Пусть $t=\sqrt[4]{x-1}\ge0$, тогда $t^{2}=\sqrt{x-1}$ и уравнение примет вид $t^{2}+3t-18=0$, $t=3$ (корень $t=-6$ отброшен). Из $\sqrt[4]{x-1}=3$ получаем $x-1=81$, $x=82$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'planimetry', subtopic: 'plan-circle', diff: 4,
|
||||
text: R`Вписанный в окружность угол $KMN$, косинус которого равен $\dfrac34$, опирается на дугу $KN$. Радиус окружности равен $4$. Найдите значение выражения $S^{2}$, где $S$ — площадь треугольника $KON$ ($O$ — центр окружности).`,
|
||||
answer: '63',
|
||||
sol: R`Пусть $\angle KMN=\alpha$. Центральный угол $\angle KON=2\alpha$. Из $\cos\alpha=\dfrac34$ находим $\sin\alpha=\sqrt{1-\dfrac{9}{16}}=\dfrac{\sqrt7}{4}$, тогда $\sin2\alpha=2\sin\alpha\cos\alpha=2\cdot\dfrac{\sqrt7}{4}\cdot\dfrac34=\dfrac{3\sqrt7}{8}$. Площадь $S=\dfrac12\cdot OK\cdot ON\cdot\sin\angle KON=\dfrac12\cdot4^{2}\cdot\dfrac{3\sqrt7}{8}=3\sqrt7$. Тогда $S^{2}=9\cdot7=63$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 1, § 5; Алгебра, 10 класс, гл. 1, § 11' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 4,
|
||||
text: R`Катер в $10$ ч $30$ мин отправился по течению реки от пристани $A$ к пристани $B$. Пробыв $3$ ч у пристани $B$, катер отправился назад и прибыл к пристани $A$ не позднее $17$ ч $15$ мин того же дня. Найдите наименьшее возможное целое значение собственной скорости (в км/ч) катера, если скорость течения реки равна $3$ км/ч, а расстояние между пристанями равно $36$ км. (Собственная скорость катера не изменялась.)`,
|
||||
answer: '20',
|
||||
sol: R`Пусть собственная скорость $x$ км/ч. Время в пути и стоянка не превышают $17\dfrac14-10\dfrac12=6\dfrac34$ ч. Тогда $\dfrac{36}{x+3}+3+\dfrac{36}{x-3}\le\dfrac{27}{4}$, откуда $\dfrac{36}{x+3}+\dfrac{36}{x-3}\le\dfrac{15}{4}$. При $x>3$ это приводит к $5x^{2}-96x-45\ge0$, решение $x\ge\dfrac{48+3\sqrt{281}}{5}\approx19{,}7$. Наименьшее целое значение $x=20$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 10; § 13' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, у которой $CC_1=1$. Радиус окружности, описанной около основания $ABC$, равен $\sqrt2$. На ребре $BC$ взята точка $N$ так, что $BN:NC=1:3$. Найдите значение выражения $\dfrac{32}{\cos^{2}\varphi}$, где $\varphi$ — угол между прямыми $A_1N$ и $CC_1$.`,
|
||||
answer: '188',
|
||||
sol: R`Сторона основания $a$: из $R=\dfrac{a\sqrt3}{3}=\sqrt2$ получаем $a=\sqrt6$. Тогда $BN=\dfrac{\sqrt6}{4}$. Так как $CC_1\parallel AA_1$, угол между $A_1N$ и $CC_1$ равен $\angle NA_1A=\varphi$. По теореме косинусов в треугольнике $ABN$: $AN^{2}=BN^{2}+AB^{2}-2\cdot BN\cdot AB\cos60^\circ=\dfrac{6}{16}+6-\dfrac{6}{4}=\dfrac{78}{16}$. В прямоугольном треугольнике $A_1AN$: $A_1N^{2}=AA_1^{2}+AN^{2}=1+\dfrac{78}{16}=\dfrac{94}{16}$, $A_1N=\dfrac{\sqrt{94}}{4}$. Тогда $\cos\varphi=\dfrac{AA_1}{A_1N}=\dfrac{4}{\sqrt{94}}$, $\cos^{2}\varphi=\dfrac{16}{94}=\dfrac{8}{47}$, и $\dfrac{32}{\cos^{2}\varphi}=32\cdot\dfrac{47}{8}=188$.`,
|
||||
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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_rt2223_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), ' | с фигурой:', TASKS.filter(t => t.fig).length, '\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_rt2223_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 → «Варианты» → «РТ-2022/23 · этап I».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,390 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2223_e2v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2022/2023, Этап II, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
|
||||
Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\РТ\2022-2023\МАТ РТ-2 22_23 В1.pdf
|
||||
|
||||
variant=108 — РТ-2022/23 Этап II (этап I — 107, этап III — 109).
|
||||
Адаптации заданий-«с-картинкой» (та же проверяемая идея, авто-проверка):
|
||||
• А1 (термометр) → показание дано числом;
|
||||
• А3 (выбор прямоугольника) → MC о соотношении сторон для квадратного
|
||||
осевого сечения цилиндра;
|
||||
• А6 (графики, возрастающие на [-3;3]) → список функций (ответ 145);
|
||||
• В1 (столбчатая диаграмма) → те же данные в таблице (figure_html),
|
||||
сопоставление вопрос↔день (ответ А6Б4В3).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2223_e2v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2223_e2v1.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 = 108;
|
||||
const PROV = 'РТ–2022/2023, Этап II, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── figure_html для В1: данные диаграммы (количество заказов по дням). */
|
||||
const FIG_B1 = `<table class="task-fig" style="border-collapse:collapse;margin:10px auto;font-size:14px;text-align:center">
|
||||
<tr>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">День недели</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Пн</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Вт</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Ср</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Чт</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Пт</th>
|
||||
<th style="border:1px solid #cbd5e1;padding:5px 9px">Сб</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">Количество заказов</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">38</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">48</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">42</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">52</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">60</td>
|
||||
<td style="border:1px solid #cbd5e1;padding:5px 9px">66</td>
|
||||
</tr>
|
||||
</table>`;
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А10 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Термометр со шкалой от $-50\,$ до $50\,^\circ$C показывает температуру $-15\,^\circ$C. Какую температуру (в $^\circ$C) будет показывать термометр, если она увеличится на $20\,^\circ$C?`,
|
||||
opts: mc('$5^\circ$C', '$15^\circ$C', '$20^\circ$C', '$10^\circ$C', '$0^\circ$C'),
|
||||
answer: 'а',
|
||||
sol: R`Повышение температуры на $20\,^\circ$C означает прибавление: $-15+20=5\,(^\circ$C$)$.`,
|
||||
ref: 'Математика, 6 класс, гл. 4, § 1' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Среди чисел $62\cdot10^{-5}$, $\ 0{,}62\cdot10^{-4}$, $\ 6{,}2\cdot10^{-4}$, $\ 6{,}2\cdot10^{-5}$, $\ 0{,}62\cdot10^{-3}$ укажите то, которое является стандартным видом числа $0{,}00062$.`,
|
||||
opts: mc('$62\cdot10^{-5}$', '$0{,}62\cdot10^{-4}$', '$6{,}2\cdot10^{-4}$', '$6{,}2\cdot10^{-5}$', '$0{,}62\cdot10^{-3}$'),
|
||||
answer: 'в',
|
||||
sol: R`Стандартный вид числа — это $a\cdot10^{n}$, где $1\le a<10$. Число $0{,}00062=6{,}2\cdot10^{-4}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Прямоугольник $ABCD$ вращается вокруг стороны $BC$. При этом получается цилиндр, осевым сечением которого является квадрат. Укажите верное соотношение между сторонами прямоугольника.`,
|
||||
opts: mc('$BC=2AB$', '$BC=AB$', '$AB=2BC$', '$BC=4AB$', '$AB=4BC$'),
|
||||
answer: 'а',
|
||||
sol: R`При вращении вокруг $BC$ сторона $BC$ становится высотой цилиндра, а $AB$ — радиусом основания. Осевое сечение — прямоугольник со сторонами $2AB$ (диаметр) и $BC$ (высота). Это квадрат, когда $BC=2AB$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-exponential', diff: 2,
|
||||
text: R`Среди чисел $0$, $1$, $2$, $3$, $4$ укажите то, которое не является решением неравенства $2^{x}<16$.`,
|
||||
opts: mc('$0$', '$1$', '$2$', '$3$', '$4$'),
|
||||
answer: 'д',
|
||||
sol: R`$2^{x}<16=2^{4}$. Так как $2>1$, функция $y=2^{x}$ возрастает, поэтому $x<4$. Из данных чисел этому промежутку не принадлежит только $4$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 1,
|
||||
text: R`Найдите значение аргумента, при котором значение функции $f(x)=3-5x$ равно $2$.`,
|
||||
opts: mc('$0$', '$1$', '$0{,}1$', '$0{,}2$', '$5$'),
|
||||
answer: 'г',
|
||||
sol: R`Подставим значение функции: $2=3-5x$, откуда $5x=1$, $x=0{,}2$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Укажите номера функций, которые возрастают на промежутке $[-3;3]$.<br>1) $f(x)=2x-1$;<br>2) $f(x)=x^{2}$;<br>3) $f(x)=-x+4$;<br>4) $f(x)=x^{3}$;<br>5) $f(x)=2^{x}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '145', ansShow: '1, 4, 5',
|
||||
sol: R`$1)$ $2x-1$ — линейная с положительным угловым коэффициентом, возрастает. $\ 2)$ $x^{2}$ на $[-3;3]$ сначала убывает, потом возрастает — нет. $\ 3)$ $-x+4$ убывает. $\ 4)$ $x^{3}$ возрастает на всей оси. $\ 5)$ $2^{x}$ — показательная с основанием $>1$, возрастает. Подходят 1, 4, 5.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 6–7' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`От верёвки длиной $3$ м $15$ см отрезали часть так, что отношение оставшейся части к отрезанной равно $4:5$. Найдите (в сантиметрах) длину оставшейся части.`,
|
||||
opts: mc('$175$', '$140$', '$252$', '$63$', '$132$'),
|
||||
answer: 'б',
|
||||
sol: R`Длина верёвки $3$ м $15$ см $=315$ см. Пусть на одну часть приходится $k$ см, тогда $4k+5k=315$, $9k=315$, $k=35$. Оставшаяся часть равна $4k=4\cdot35=140$ (см).`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-fractions', diff: 2,
|
||||
text: R`Укажите номер выражения, которое показывает, за сколько часов был полностью наполнен бассейн, если за $a$ ч было заполнено $96\%$ объёма бассейна.`,
|
||||
opts: mc('$\dfrac{a}{96}$', '$\dfrac{26a}{25}$', '$\dfrac{a}{24}$', '$\dfrac{25a}{24}$', '$\dfrac{a}{25}$'),
|
||||
answer: 'г',
|
||||
sol: R`Чтобы найти всё число по его проценту, нужно данное число разделить на число процентов и умножить на $100$: $\dfrac{a}{96}\cdot100=\dfrac{100a}{96}=\dfrac{25a}{24}$ (ч).`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 2' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Площадь сечения шара плоскостью, проходящей через его центр, равна $81\pi$. Найдите объём шара.`,
|
||||
opts: mc('$364\pi$', '$108\pi$', '$972\pi$', '$243\pi$', '$729\pi$'),
|
||||
answer: 'в',
|
||||
sol: R`Сечение через центр — большой круг радиуса $R$: $\pi R^{2}=81\pi$, откуда $R=9$. Объём $V=\dfrac43\pi R^{3}=\dfrac43\pi\cdot729=972\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3, § 6' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'trigonometry', subtopic: 'trig-circle', diff: 2,
|
||||
text: R`Среди чисел $\dfrac{\pi}{6}$, $\ \dfrac{\pi}{12}$, $\ \dfrac{\pi}{2}$, $\ \dfrac{7\pi}{9}$, $\ \dfrac{5\pi}{6}$ выберите номера тех, которые принадлежат области определения выражения $\operatorname{tg}3x$.<br>1) $\dfrac{\pi}{6}$; 2) $\dfrac{\pi}{12}$; 3) $\dfrac{\pi}{2}$; 4) $\dfrac{7\pi}{9}$; 5) $\dfrac{5\pi}{6}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '24', ansShow: '2, 4',
|
||||
sol: R`$\operatorname{tg}3x$ определён при $3x\ne\dfrac{\pi}{2}+\pi n$, то есть $x\ne\dfrac{\pi}{6}+\dfrac{\pi n}{3}$. Проверим: $\dfrac{\pi}{6}$ — исключено; $\dfrac{\pi}{12}$ ($3x=\dfrac{\pi}{4}$) — годится; $\dfrac{\pi}{2}$ ($3x=\dfrac{3\pi}{2}$) — исключено; $\dfrac{7\pi}{9}$ ($3x=\dfrac{7\pi}{3}$) — годится; $\dfrac{5\pi}{6}$ ($3x=\dfrac{5\pi}{2}$) — исключено. Подходят 2 и 4.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 3' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'long', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
fig: FIG_B1,
|
||||
text: R`В таблице приведено количество заказов в интернет-магазине на протяжении недели (с понедельника по субботу). Установите соответствие между вопросами А–В и ответами 1–6.<br><b>Вопрос:</b><br>А) В какой день недели было сделано больше всего заказов?<br>Б) В какой день недели было сделано на $14$ заказов меньше, чем в субботу?<br>В) В какой день недели было сделано на $30\%$ меньше заказов, чем в пятницу?<br><b>Ответ:</b><br>1) Понедельник; 2) Вторник; 3) Среда; 4) Четверг; 5) Пятница; 6) Суббота.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А6Б4В3', ansShow: 'А6Б4В3',
|
||||
sol: R`А) Больше всего заказов в субботу ($66$) — ответ 6. Б) $66-14=52$ — это четверг — ответ 4. В) $30\%$ меньше, чем в пятницу: $60\cdot0{,}7=42$ — это среда — ответ 3.`,
|
||||
ref: 'Математика, 5 класс, ч. 2, гл. 3, § 16' },
|
||||
|
||||
{ idx: 12, type: 'long', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Остаток при делении числа $8756$ на $9$ равен …<br>Б) Наибольший остаток, который может получиться при делении натурального числа на $7$, равен …<br>В) Цифра, которую нужно подставить вместо звёздочки, чтобы трёхзначное натуральное число $\overline{37*}$ было кратно $3$, а при делении на $5$ давало в остатке $3$, равна …<br><b>Окончание:</b><br>1) $5$; 2) $7$; 3) $6$; 4) $2$; 5) $8$; 6) $9$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А5Б3В5', ansShow: 'А5Б3В5',
|
||||
sol: R`А) $8756=972\cdot9+8$, остаток $8$ — окончание 5. Б) при делении на $7$ наибольший остаток равен $6$ — окончание 3. В) кратность $3$ даёт $3+7+*$ кратно $3$, то есть $*\in\{2;5;8\}$; остаток $3$ при делении на $5$ даёт последнюю цифру $3$ или $8$. Общая цифра — $8$ — окончание 5.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 11; § 13' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Выберите верные утверждения.<br>1) значение выражения $\left(-\sqrt[4]{1{,}6}\right)^{4}$ равно $1{,}6$;<br>2) значение выражения $5-|-2{,}3|$ равно $7{,}3$;<br>3) значение выражения $\left(\dfrac12\right)^{\log_{0{,}5}3}$ равно $-3$;<br>4) значение выражения $\log_3\sqrt[4]{9}$ равно $2$;<br>5) значение выражения $\sqrt{32\sin\dfrac{\pi}{6}}$ равно $4$;<br>6) значение выражения $\sqrt{160^{2}-96^{2}}$ равно $128$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '156', ansShow: '1, 5, 6',
|
||||
sol: R`$1)$ $\left(-\sqrt[4]{1{,}6}\right)^{4}=1{,}6$ — верно. $\ 2)$ $5-2{,}3=2{,}7$, не $7{,}3$ — неверно. $\ 3)$ $\left(\dfrac12\right)^{\log_{0{,}5}3}=3$ — неверно. $\ 4)$ $\log_3 3^{1/2}=0{,}5$ — неверно. $\ 5)$ $\sqrt{32\cdot0{,}5}=\sqrt{16}=4$ — верно. $\ 6)$ $\sqrt{(160-96)(160+96)}=\sqrt{64\cdot256}=8\cdot16=128$ — верно. Подходят 1, 5, 6.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 3,
|
||||
text: R`Выберите верные утверждения, если известно, что прямая $a$ и плоскость $\alpha$ параллельны.<br>1) любая прямая, перпендикулярная прямой $a$, перпендикулярна плоскости $\alpha$;<br>2) любая прямая, перпендикулярная плоскости $\alpha$, перпендикулярна прямой $a$;<br>3) прямая $a$ не имеет общих точек ни с одной прямой, лежащей в плоскости $\alpha$;<br>4) прямая $a$ имеет общую точку с плоскостью $\alpha$;<br>5) через любую точку пространства можно провести прямую, параллельную прямой $a$;<br>6) любая прямая, параллельная плоскости $\alpha$, параллельна прямой $a$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '235', ansShow: '2, 3, 5',
|
||||
sol: R`$1)$ неверно. $\ 2)$ верно: прямая, перпендикулярная $\alpha$, перпендикулярна каждой прямой этой плоскости, а значит и параллельной ей прямой $a$. $\ 3)$ верно: $a\parallel\alpha$ означает, что $a$ не имеет общих точек с $\alpha$, поэтому и ни с одной прямой в $\alpha$. $\ 4)$ неверно. $\ 5)$ верно: через любую точку можно провести прямую, параллельную данной. $\ 6)$ неверно. Подходят 2, 3, 5.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 2, § 5' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В прямоугольном треугольнике $ACB$ угол $C$ равен $90^\circ$, а $CM$ — медиана, проведённая к гипотенузе, причём $CM=2\sqrt2$. Найдите квадрат длины гипотенузы.`,
|
||||
answer: '32',
|
||||
sol: R`Медиана прямоугольного треугольника, проведённая к гипотенузе, равна её половине, поэтому $AB=2\,CM=4\sqrt2$. Тогда $AB^{2}=\left(4\sqrt2\right)^{2}=32$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 15' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'expressions', subtopic: 'expr-fractions', diff: 3,
|
||||
text: R`Найдите значение выражения $\dfrac{(x-3)^{2}-4}{x^{2}-4x-5}$ при $x=-1\dfrac15$.`,
|
||||
answer: '11',
|
||||
sol: R`Числитель $(x-3)^{2}-4=(x-3-2)(x-3+2)=(x-5)(x-1)$; знаменатель $x^{2}-4x-5=(x-5)(x+1)$. Тогда дробь равна $\dfrac{x-1}{x+1}$. При $x=-\dfrac65$: $\dfrac{-\frac65-1}{-\frac65+1}=\dfrac{-\frac{11}{5}}{-\frac15}=11$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 1, § 1–2' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Найдите сумму бесконечно убывающей геометрической прогрессии, у которой первый член равен $-56$, а второй член равен $-12\dfrac49$.`,
|
||||
answer: '-72',
|
||||
sol: R`Знаменатель $q=\dfrac{b_2}{b_1}=\dfrac{-\frac{112}{9}}{-56}=\dfrac29$. Сумма $S=\dfrac{b_1}{1-q}=\dfrac{-56}{1-\frac29}=\dfrac{-56}{\frac79}=-56\cdot\dfrac97=-72$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 19' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`На покупку $39$ л краски для покраски стен выделено $390$ рублей. Краска продаётся в банках объёмом $3$ л (стоимость одной банки $31{,}50$ руб.) и $10$ л (стоимость одной банки $97{,}85$ руб.); расход краски во всех банках одинаков. Какая сумма (в копейках) останется после покупки $39$ л краски, если стоимость покупки не должна превышать выделенной суммы?`,
|
||||
answer: '195',
|
||||
sol: R`Выгодно купить $3$ банки по $10$ л и $3$ банки по $3$ л (ровно $39$ л). Стоимость в копейках: $3\cdot9785+3\cdot3150=29355+9450=38805$. Выделено $390$ руб $=39000$ коп. Останется $39000-38805=195$ (коп.).`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 2, § 7' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите значение выражения $36^{\,x_0}$, где $x_0$ — наибольший корень уравнения $36^{x}-10\cdot6^{x}+9=0$.`,
|
||||
answer: '81',
|
||||
sol: R`Пусть $t=6^{x}$, тогда $t^{2}-10t+9=0$, $t=1$ или $t=9$. Из $6^{x}=1$: $x=0$; из $6^{x}=9$: $x=\log_6 9$. Наибольший корень $x_0=\log_6 9$, поэтому $36^{x_0}=6^{2\log_6 9}=9^{2}=81$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`В равнобедренную трапецию вписана окружность, диаметр которой равен $3{,}5$. Острый угол трапеции равен $30^\circ$. Найдите значение выражения $4\cdot S$, где $S$ — площадь трапеции.`,
|
||||
answer: '98',
|
||||
sol: R`Высота трапеции равна диаметру вписанной окружности: $h=3{,}5$. Боковая сторона $=\dfrac{h}{\sin30^\circ}=7$. По свойству описанного четырёхугольника сумма оснований равна сумме боковых сторон: $BC+AD=AB+CD=14$. Площадь $S=\dfrac{BC+AD}{2}\cdot h=\dfrac{14}{2}\cdot3{,}5=24{,}5$. Тогда $4S=98$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 3,
|
||||
text: R`Найдите минимум функции $f(x)=2+3x-x^{2}-\dfrac{x^{3}}{3}$.`,
|
||||
answer: '-7',
|
||||
sol: R`$f'(x)=3-2x-x^{2}=-(x+3)(x-1)$. Нули $x=-3$ и $x=1$; смена знака $f'$ с минуса на плюс в точке $x=-3$ — это точка минимума. $f(-3)=2-9-9-\dfrac{-27}{3}=2-9-9+9=-7$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Найдите сумму всех натуральных решений совокупности неравенств $\left[\begin{array}{l}0{,}4x-2\le0,\\2-x>0.\end{array}\right.$`,
|
||||
answer: '15',
|
||||
sol: R`$0{,}4x-2\le0\Rightarrow x\le5$; $\ 2-x>0\Rightarrow x<2$. Объединение лучей — множество $(-\infty;5]$. Натуральные решения $1,2,3,4,5$; их сумма равна $15$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Объём цилиндра равен $28\pi$. Найдите площадь полной поверхности цилиндра $S$, если радиус его основания равен $2$. В ответ запишите значение выражения $\dfrac{S}{\pi}$.`,
|
||||
answer: '36',
|
||||
sol: R`Из $V=\pi r^{2}h$: $28\pi=\pi\cdot4\cdot h$, откуда $h=7$. Площадь полной поверхности $S=2\pi rh+2\pi r^{2}=2\pi\cdot2\cdot7+2\pi\cdot4=28\pi+8\pi=36\pi$. Тогда $\dfrac{S}{\pi}=36$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите произведение наименьшего целого положительного и наименьшего целого отрицательного решений неравенства $\dfrac{7}{x+6}>\dfrac{1}{x-1}$.`,
|
||||
answer: '-15',
|
||||
sol: R`Приведём к виду $\dfrac{6x-13}{(x+6)(x-1)}>0$. Нуль числителя $x=\dfrac{13}{6}$; при $x=-6$ и $x=1$ значения не существуют. Методом интервалов решение — $(-6;1)\cup\left(\dfrac{13}{6};+\infty\right)$. Наименьшее целое положительное решение $3$, наименьшее целое отрицательное $-5$; их произведение $-15$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите увеличенное в $3$ раза произведение наибольшего корня на количество всех корней уравнения $\sqrt[4]{4x^{4}-14x^{2}+8}=x$.`,
|
||||
answer: '12',
|
||||
sol: R`Уравнение равносильно системе $4x^{4}-14x^{2}+8=x^{4}$ при $x\ge0$, то есть $3x^{4}-14x^{2}+8=0$, $x\ge0$. Пусть $t=x^{2}$: $3t^{2}-14t+8=0$, $t=4$ или $t=\dfrac23$. Тогда $x=2$ или $x=\sqrt{\dfrac23}$ (неотрицательные). Корней $2$, наибольший корень $2$. Произведение $2\cdot2=4$; увеличенное в $3$ раза — $12$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 4,
|
||||
text: R`Апофема правильной четырёхугольной пирамиды равна $15$, а двугранный угол при ребре основания равен $\arccos\dfrac35$. Найдите объём пирамиды.`,
|
||||
answer: '1296',
|
||||
sol: R`Пусть $O$ — центр основания, $K$ — середина ребра основания, $SK=15$ — апофема, $\angle SKO=\arccos\dfrac35$. Тогда $OK=SK\cos\angle SKO=15\cdot\dfrac35=9$, поэтому сторона основания $AD=2\,OK=18$. Высота $SO=\sqrt{SK^{2}-OK^{2}}=\sqrt{225-81}=12$. Объём $V=\dfrac13\cdot AD^{2}\cdot SO=\dfrac13\cdot324\cdot12=1296$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $\cos^{2}\dfrac{15x}{4}-\sin^{2}\dfrac{15x}{4}=0$ на промежутке $(0^\circ;45^\circ)$.`,
|
||||
answer: '48',
|
||||
sol: R`По формуле косинуса двойного аргумента левая часть равна $\cos\dfrac{15x}{2}$. Уравнение $\cos\dfrac{15x}{2}=0$ даёт $\dfrac{15x}{2}=90^\circ+180^\circ n$, $x=12^\circ+24^\circ n$. Промежутку $(0^\circ;45^\circ)$ принадлежат $12^\circ$ и $36^\circ$; их сумма равна $48^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите произведение наименьшего целого решения на количество всех натуральных решений неравенства $\log_5^{2}(x^{2}-3)-3\log_5(x^{2}-3)\le0$.`,
|
||||
answer: '-110',
|
||||
sol: R`Пусть $t=\log_5(x^{2}-3)$, тогда $t^{2}-3t\le0$, откуда $0\le t\le3$. Значит, $1\le x^{2}-3\le125$, то есть $4\le x^{2}\le128$ и $x\in\left[-8\sqrt2;-2\right]\cup\left[2;8\sqrt2\right]$. Наименьшее целое решение $-11$ (так как $8\sqrt2\approx11{,}3$). Натуральных решений $2,3,\ldots,11$ — всего $10$. Произведение $-11\cdot10=-110$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Удвоенное произведение двух последовательных нечётных натуральных чисел на $262$ больше их суммы. Найдите эти числа. В ответ запишите сумму квадратов этих чисел.`,
|
||||
answer: '290',
|
||||
sol: R`Пусть числа $x$ и $x+2$. По условию $2x(x+2)=262+x+(x+2)$, $2x^{2}+4x=264+2x$, $x^{2}+x-132=0$, $x=11$ (корень $-12$ не подходит). Числа $11$ и $13$, сумма квадратов $121+169=290$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2, § 11' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`Объём правильной четырёхугольной призмы $ABCDA_1B_1C_1D_1$ равен $540$, а её высота равна $15$. Точка $P$ лежит на диагонали $BD$ так, что $BP:PD=1:2$. Через точки $P$ и $D_1$ параллельно диагонали $AC$ основания $ABCD$ проведена секущая плоскость. Найдите значение выражения $16na$, где $n$ — количество вершин многоугольника, полученного в сечении, $a$ — длина наименьшей стороны этого многоугольника.`,
|
||||
answer: '340',
|
||||
sol: R`Сторона основания $s$: из $V=s^{2}\cdot15=540$ получаем $s=6$. Через $P$ проводим прямую, параллельную $AC$; она пересекает $AB$ и $BC$, отсекая $BN=BK=4$. Сечение — пятиугольник $D_1MKNL$, значит $n=5$. Его наименьшие стороны $MK=LN$: из подобия находим $MA=LC=\dfrac{15}{4}$, $KA=NC=2$, тогда $MK=\sqrt{KA^{2}+MA^{2}}=\sqrt{4+\dfrac{225}{16}}=\dfrac{17}{4}$. Значит, $a=\dfrac{17}{4}$ и $16na=16\cdot5\cdot\dfrac{17}{4}=340$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 3' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_rt2223_e2v1 (${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), ' | с фигурой:', TASKS.filter(t => t.fig).length, '\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_rt2223_e2v1.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 → «Варианты» → «РТ-2022/23 · этап II».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,387 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2223_e3v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2022/2023, Этап III, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
|
||||
Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\РТ\2022-2023\МАТ РТ-3 22_23 В1.pdf
|
||||
|
||||
variant=109 — РТ-2022/23 Этап III (этап I — 107, этап II — 108).
|
||||
Геометрия закодирована текстом. Единственное задание с обязательным
|
||||
чертежом — А6 (чтение графика): кусочно-линейная функция на [-5;6]
|
||||
воспроизведена inline-SVG в figure_html (как у math9 и варианта 106);
|
||||
все 5 утверждений и ответ (134) согласованы с реконструкцией.
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2223_e3v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2223_e3v1.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 = 109;
|
||||
const PROV = 'РТ–2022/2023, Этап III, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── SVG-график для А6: кусочно-линейная функция на [-5;6] через точки
|
||||
(-5,3),(-2,-5),(4,5),(6,-1). Три нуля; min=-5 (x=-2), max=5 (x=4);
|
||||
возрастает на (-2;4) → целые с f'>0: -1,0,1,2,3 (сумма 5); f'(-4)<0.
|
||||
Цвета — только в SVG-стоки. */
|
||||
const FIG_A6 = `<svg class="task-fig" viewBox="0 0 360 244" width="360" height="244" xmlns="http://www.w3.org/2000/svg" style="max-width:360px;width:100%;height:auto;display:block;margin:10px auto">
|
||||
<g stroke="#e2e8f0" stroke-width="1">
|
||||
<line x1="37" y1="29" x2="37" y2="219"/><line x1="63" y1="29" x2="63" y2="219"/><line x1="89" y1="29" x2="89" y2="219"/><line x1="115" y1="29" x2="115" y2="219"/><line x1="141" y1="29" x2="141" y2="219"/><line x1="167" y1="29" x2="167" y2="219"/><line x1="193" y1="29" x2="193" y2="219"/><line x1="219" y1="29" x2="219" y2="219"/><line x1="245" y1="29" x2="245" y2="219"/><line x1="271" y1="29" x2="271" y2="219"/><line x1="297" y1="29" x2="297" y2="219"/><line x1="323" y1="29" x2="323" y2="219"/>
|
||||
<line x1="37" y1="29" x2="323" y2="29"/><line x1="37" y1="48" x2="323" y2="48"/><line x1="37" y1="67" x2="323" y2="67"/><line x1="37" y1="86" x2="323" y2="86"/><line x1="37" y1="105" x2="323" y2="105"/><line x1="37" y1="124" x2="323" y2="124"/><line x1="37" y1="143" x2="323" y2="143"/><line x1="37" y1="162" x2="323" y2="162"/><line x1="37" y1="181" x2="323" y2="181"/><line x1="37" y1="200" x2="323" y2="200"/><line x1="37" y1="219" x2="323" y2="219"/>
|
||||
</g>
|
||||
<line x1="24" y1="124" x2="348" y2="124" stroke="#334155" stroke-width="1.5"/>
|
||||
<line x1="167" y1="232" x2="167" y2="16" stroke="#334155" stroke-width="1.5"/>
|
||||
<polygon points="348,124 340,120 340,128" fill="#334155"/>
|
||||
<polygon points="167,16 163,24 171,24" fill="#334155"/>
|
||||
<polyline points="37,67 115,219 271,29 323,143" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<text x="350" y="120" font-size="13" font-style="italic" fill="#334155">x</text>
|
||||
<text x="172" y="26" font-size="13" font-style="italic" fill="#334155">y</text>
|
||||
<text x="155" y="138" font-size="12" fill="#334155">O</text>
|
||||
<text x="189" y="138" font-size="12" fill="#334155">1</text>
|
||||
<text x="151" y="109" font-size="12" fill="#334155">1</text>
|
||||
<text x="276" y="26" font-size="13" font-style="italic" fill="#2563eb">y=f(x)</text>
|
||||
</svg>`;
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А10 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
|
||||
text: R`Укажите номер выражения, являющегося разностью квадратов выражений $m$ и $7n$.`,
|
||||
opts: mc('$(m-7n)^{2}$', '$\left(\dfrac{m}{7n}\right)^{2}$', '$m^{2}-(7n)^{2}$', '$m-(7n)^{2}$', '$m^{2}-7n^{2}$'),
|
||||
answer: 'в',
|
||||
sol: R`Разность квадратов выражений $m$ и $7n$ — это $m^{2}-(7n)^{2}$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 12–13' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Из углов $180^\circ$, $240^\circ$, $225^\circ$, $210^\circ$, $270^\circ$ выберите тот, тангенс которого равен $\sqrt3$.`,
|
||||
opts: mc('$180^\circ$', '$240^\circ$', '$225^\circ$', '$210^\circ$', '$270^\circ$'),
|
||||
answer: 'б',
|
||||
sol: R`$\operatorname{tg}240^\circ=\operatorname{tg}(180^\circ+60^\circ)=\operatorname{tg}60^\circ=\sqrt3$. У остальных данных углов тангенс не равен $\sqrt3$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`Вписанный угол $MKN$ опирается на дугу $MN$, градусная мера которой (заключённой внутри этого угла) равна $88^\circ$. Найдите градусную меру угла $MKN$.`,
|
||||
opts: mc('$44^\circ$', '$24^\circ$', '$46^\circ$', '$88^\circ$', '$22^\circ$'),
|
||||
answer: 'а',
|
||||
sol: R`Вписанный угол равен половине дуги, на которую он опирается: $\angle MKN=\dfrac12\cdot88^\circ=44^\circ$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 4, § 27' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Укажите номер уравнения, корнем которого является число $-1$.`,
|
||||
opts: mc('$\dfrac{5}{x+1}=0$', '$x^{2}+1=0$', '$3^{\,x-1}=1$', '$\log_7(x+2)=0$', '$\sqrt{x-1}=0$'),
|
||||
answer: 'г',
|
||||
sol: R`Подставим $x=-1$: $\dfrac{5}{0}$ не имеет смысла; $\ (-1)^{2}+1=2\ne0$; $\ 3^{-2}\ne1$; $\ \log_7(-1+2)=\log_7 1=0$ — верно; $\ \sqrt{-2}$ не имеет смысла. Корень $-1$ имеет только уравнение под номером 4.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 15' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Среди значений аргумента $x$, равных $1{,}5$; $0{,}4$; $1{,}2$; $0{,}6$; $2{,}5$, укажите то, при котором значение функции $f(x)=\dfrac2x$ меньше $1$.`,
|
||||
opts: mc('$1{,}5$', '$0{,}4$', '$1{,}2$', '$0{,}6$', '$2{,}5$'),
|
||||
answer: 'д',
|
||||
sol: R`$f(x)=\dfrac2x<1$ при $x>2$ (для положительных $x$). Из данных чисел этому условию удовлетворяет только $2{,}5$: $\dfrac{2}{2{,}5}=0{,}8<1$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
fig: FIG_A6,
|
||||
text: R`На рисунке изображён график функции $y=f(x)$, определённой на промежутке $[-5;6]$. Укажите номера верных утверждений.<br>1) функция имеет три нуля;<br>2) $f'(-4)=0$;<br>3) максимум функции равен $5$;<br>4) сумма целых значений аргумента, при которых $f'(x)>0$, равна $5$;<br>5) наименьшее значение функции на промежутке $[-5;6]$ равно $-2$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '134', ansShow: '1, 3, 4',
|
||||
sol: R`$1)$ верно: график пересекает ось абсцисс в трёх точках. $\ 2)$ неверно: при $x=-4$ функция убывает, $f'(-4)<0$. $\ 3)$ верно: наибольшее (максимум) значение функции равно $5$. $\ 4)$ верно: функция возрастает на $(-2;4)$, целые значения с $f'(x)>0$ — это $-1,0,1,2,3$, их сумма $5$. $\ 5)$ неверно: наименьшее значение функции равно $-5$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 7; «Алгебра, 10 кл.», гл. 3, § 20' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Пять рабочих могут выполнить работу за $14$ дней. За сколько дней могут выполнить эту же работу $7$ рабочих?`,
|
||||
opts: mc('$20$', '$16$', '$12$', '$10$', '$9$'),
|
||||
answer: 'г',
|
||||
sol: R`Зависимость между числом рабочих и числом дней обратно пропорциональная: $\dfrac{5}{7}=\dfrac{x}{14}$, откуда $x=\dfrac{5\cdot14}{7}=10$ (дней).`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 4–5' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $6\cos\alpha$, если $\sin\alpha=\dfrac{\sqrt2}{3}$ и $\dfrac{\pi}{2}<\alpha<\pi$.`,
|
||||
opts: mc('$2\sqrt7$', '$-2\sqrt{11}$', '$-2\sqrt7$', '$2\sqrt{11}$', '$-2\sqrt2$'),
|
||||
answer: 'в',
|
||||
sol: R`Из $\sin^{2}\alpha+\cos^{2}\alpha=1$: $\cos^{2}\alpha=1-\dfrac29=\dfrac79$. Во второй четверти $\cos\alpha<0$, поэтому $\cos\alpha=-\dfrac{\sqrt7}{3}$. Тогда $6\cos\alpha=-2\sqrt7$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 4' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Прямоугольник, у которого длины сторон равны $3$ и $6$, вращается вокруг большей стороны. Найдите площадь боковой поверхности цилиндра, полученного в результате вращения.`,
|
||||
opts: mc('$54\pi$', '$18\pi$', '$108\pi$', '$45\pi$', '$36\pi$'),
|
||||
answer: 'д',
|
||||
sol: R`При вращении вокруг большей стороны ($6$) она становится высотой цилиндра, а меньшая ($3$) — радиусом основания. Площадь боковой поверхности $S=2\pi rh=2\pi\cdot3\cdot6=36\pi$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'trigonometry', subtopic: 'trig-circle', diff: 2,
|
||||
text: R`Среди данных утверждений укажите номера верных.<br>1) $\operatorname{arctg}(-1)=\dfrac{3\pi}{4}$;<br>2) $\sin\dfrac{\pi}{4}>\sin\dfrac{\pi}{6}$;<br>3) $\cos\dfrac{\pi}{3}>\cos\dfrac{\pi}{6}$;<br>4) $\operatorname{ctg}\dfrac{17\pi}{12}<0$;<br>5) $\arccos\left(-\dfrac12\right)=\dfrac{2\pi}{3}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '25', ansShow: '2, 5',
|
||||
sol: R`$1)$ неверно: $\operatorname{arctg}(-1)=-\dfrac{\pi}{4}$. $\ 2)$ верно: $\dfrac{\sqrt2}{2}>\dfrac12$. $\ 3)$ неверно: $\dfrac12<\dfrac{\sqrt3}{2}$. $\ 4)$ неверно: $\operatorname{ctg}\dfrac{17\pi}{12}=\operatorname{ctg}\dfrac{5\pi}{12}>0$. $\ 5)$ верно: $\arccos\left(-\dfrac12\right)=\dfrac{2\pi}{3}$. Подходят 2 и 5.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 7; § 9' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'long', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 3,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Сумма шестнадцати первых членов арифметической прогрессии $(a_n)$, у которой $a_1=-2$, $a_{16}=43$, равна …<br>Б) Сумма пяти первых членов геометрической прогрессии $(b_n)$, у которой $b_1=-4$, $q=2$, равна …<br>В) Сумма бесконечно убывающей геометрической прогрессии, у которой $b_1=-208$, $q=\dfrac15$, равна …<br><b>Окончание:</b><br>1) $-260$; 2) $-110$; 3) $328$; 4) $-832$; 5) $-124$; 6) $252$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А3Б5В1', ansShow: 'А3Б5В1',
|
||||
sol: R`А) $S_{16}=\dfrac{a_1+a_{16}}{2}\cdot16=\dfrac{-2+43}{2}\cdot16=41\cdot8=328$ — окончание 3. Б) $S_5=\dfrac{b_1(q^{5}-1)}{q-1}=\dfrac{-4(32-1)}{1}=-124$ — окончание 5. В) $S=\dfrac{b_1}{1-q}=\dfrac{-208}{1-\frac15}=-208\cdot\dfrac54=-260$ — окончание 1.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 16; § 18–19' },
|
||||
|
||||
{ idx: 12, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — прямоугольный параллелепипед. Выберите верные утверждения.<br>1) расстояние от точки $B$ до плоскости грани $A_1D_1C_1B_1$ равно длине отрезка $BB_1$;<br>2) расстояние между плоскостями граней $AA_1D_1D$ и $BB_1C_1C$ равно длине отрезка $AB$;<br>3) расстояние между прямой $D_1C_1$ и плоскостью грани $ABCD$ равно длине отрезка $DC_1$;<br>4) расстояние от точки $C$ до плоскости грани $AA_1D_1D$ равно длине отрезка $CC_1$;<br>5) расстояние между плоскостями граней $AA_1B_1B$ и $DD_1C_1C$ равно длине отрезка $B_1D$;<br>6) расстояние между прямой $DC_1$ и плоскостью грани $AA_1B_1B$ равно длине отрезка $B_1C_1$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '126', ansShow: '1, 2, 6',
|
||||
sol: R`Расстояние от точки (прямой) до плоскости — длина перпендикуляра. $\ 1)$ верно: $BB_1\perp$ верхней грани. $\ 2)$ верно: $AB$ — общий перпендикуляр параллельных граней. $\ 3)$ неверно: расстояние равно длине бокового ребра, а не диагонали $DC_1$. $\ 4)$ неверно: расстояние равно $CD$, а не $CC_1$. $\ 5)$ неверно: расстояние равно $AD$, а не диагонали $B_1D$. $\ 6)$ верно: $B_1C_1$ — перпендикуляр между $DC_1$ и гранью $AA_1B_1B$. Подходят 1, 2, 6.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 8' },
|
||||
|
||||
{ idx: 13, type: 'long', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`На рисунке изображён график функции $f(x)=|x|$ и отмечена точка $A(-2;2)$, принадлежащая этому графику. Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Если график функции $f(x)=|x|$ сдвинуть на $6$ единиц вправо вдоль оси абсцисс, то точка $A$ будет иметь координаты …<br>Б) Если график функции $f(x)=|x|$ сдвинуть на $8$ единиц вниз вдоль оси ординат, то точка $A$ будет иметь координаты …<br>В) Если график функции $f(x)=|x|$ сдвинуть на $2$ единицы влево вдоль оси абсцисс и на $3$ единицы вверх вдоль оси ординат, то точка $A$ будет иметь координаты …<br><b>Окончание:</b><br>1) $(-8;2)$; 2) $(0;-1)$; 3) $(-2;-6)$; 4) $(-2;10)$; 5) $(4;2)$; 6) $(-4;5)$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А5Б3В6', ansShow: 'А5Б3В6',
|
||||
sol: R`А) сдвиг на $6$ вправо: $(-2+6;2)=(4;2)$ — окончание 5. Б) сдвиг на $8$ вниз: $(-2;2-8)=(-2;-6)$ — окончание 3. В) сдвиг на $2$ влево и $3$ вверх: $(-2-2;2+3)=(-4;5)$ — окончание 6.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 9' },
|
||||
|
||||
{ idx: 14, type: 'long', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение, если известно, что $2023=7\cdot17\cdot17$.<br><b>Начало:</b><br>А) Наибольший простой делитель числа $2023$ равен …<br>Б) Количество различных натуральных делителей числа $2023$ равно …<br>В) Наибольший общий делитель чисел $117$ и $2023$ равен …<br><b>Окончание:</b><br>1) $5$; 2) $17$; 3) $7$; 4) $51$; 5) $1$; 6) $6$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А2Б6В5', ansShow: 'А2Б6В5',
|
||||
sol: R`А) простые делители числа $2023$ — это $7$ и $17$, наибольший $17$ — окончание 2. Б) делители $2023$: $1,7,17,119,289,2023$ — всего $6$ — окончание 6. В) $117=3^{2}\cdot13$, у чисел $117$ и $2023$ общих простых делителей нет, поэтому их наибольший общий делитель равен $1$ — окончание 5.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 12; § 14' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В треугольнике $ABC$ точка $M$ лежит на стороне $AC$, точка $N$ — на стороне $BC$, причём $MN\parallel AB$, $CM=24$, $CN=12$, $NB=3$. Найдите длину стороны $AC$.`,
|
||||
answer: '30',
|
||||
sol: R`Так как $MN\parallel AB$, треугольник $MNC$ подобен треугольнику $ABC$. Тогда $\dfrac{CM}{CA}=\dfrac{CN}{CB}$, где $CB=CN+NB=15$. Получаем $\dfrac{24}{CA}=\dfrac{12}{15}$, откуда $CA=\dfrac{24\cdot15}{12}=30$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Найдите значение выражения $\left(3b^{0{,}25}\right)^{2}+3b^{0{,}5}$ при $b=\log_{\sqrt2}256$.`,
|
||||
answer: '48',
|
||||
sol: R`Упростим: $\left(3b^{0{,}25}\right)^{2}+3b^{0{,}5}=9b^{0{,}5}+3b^{0{,}5}=12b^{0{,}5}$. Значение $b=\log_{\sqrt2}256=\log_{2^{1/2}}2^{8}=\dfrac{8}{1/2}=16$. Тогда $12\cdot16^{0{,}5}=12\cdot4=48$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 1, § 1' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Найдите произведение корней уравнения $\log_{\sqrt5}\left(x-3\sqrt7\right)+\log_{\sqrt5}\left(x+3\sqrt7\right)=0$ (корень, если он единственный).`,
|
||||
answer: '8',
|
||||
sol: R`По свойству логарифмов $\log_{\sqrt5}\left((x-3\sqrt7)(x+3\sqrt7)\right)=0$, то есть $x^{2}-63=1$, $x^{2}=64$, $x=\pm8$. Условию $x-3\sqrt7>0$ ($3\sqrt7\approx7{,}9$) удовлетворяет только $x=8$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 9' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Через электронный сервис Петя купил билет на спортивное мероприятие и заплатил $46$ рублей $25$ копеек. В эту сумму входит стоимость билета и сервисный сбор $2$ рубля $50$ копеек. За два дня до мероприятия Петя решил вернуть билет. По правилам организатора ему вернут $80\%$ стоимости билета. Какую сумму (в рублях) получит Петя, вернув билет?`,
|
||||
answer: '35',
|
||||
sol: R`Стоимость билета без сервисного сбора: $46{,}25-2{,}50=43{,}75$ рубля. Вернут $80\%$ от неё: $43{,}75\cdot0{,}8=35$ (рублей).`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 1–2' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\left(\dfrac37\right)^{\frac{x+18}{(x-2)^{2}}}\le\dfrac37$.`,
|
||||
answer: '23',
|
||||
sol: R`Так как $0<\dfrac37<1$, функция убывает, поэтому неравенство равносильно $\dfrac{x+18}{(x-2)^{2}}\ge1$, то есть $\dfrac{(x+2)(x-7)}{(x-2)^{2}}\le0$. Методом интервалов (нули $-2$ и $7$, $x\ne2$) решение — $[-2;2)\cup(2;7]$. Целые: $-2,-1,0,1,3,4,5,6,7$; их сумма равна $23$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`В трапеции $ABCD$ ($BC\parallel AD$) известно, что $\angle A=90^\circ$, $\angle C=120^\circ$, $AD=8\sqrt2$. Найдите значение выражения $\sqrt3\cdot S$, где $S$ — площадь трапеции $ABCD$, если высота трапеции равна $3\sqrt6$.`,
|
||||
answer: '117',
|
||||
sol: R`Пусть $CK$ — высота ($CK=3\sqrt6$). В прямоугольном треугольнике $CKD$ угол при $D$ равен $60^\circ$, поэтому $KD=\dfrac{CK}{\sqrt3}=3\sqrt2$. Тогда $AK=AD-KD=8\sqrt2-3\sqrt2=5\sqrt2$, а так как $ABCK$ — прямоугольник, $BC=AK=5\sqrt2$. Площадь $S=\dfrac{AD+BC}{2}\cdot CK=\dfrac{8\sqrt2+5\sqrt2}{2}\cdot3\sqrt6=39\sqrt3$. Тогда $\sqrt3\cdot S=\sqrt3\cdot39\sqrt3=117$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 3,
|
||||
text: R`Найдите наименьшее значение функции $f(x)=\dfrac{x^{2}}{x-4}$ на отрезке $[6;10]$.`,
|
||||
answer: '16',
|
||||
sol: R`$f'(x)=\dfrac{2x(x-4)-x^{2}}{(x-4)^{2}}=\dfrac{x^{2}-8x}{(x-4)^{2}}$. Нули $x=0$ и $x=8$; на $[6;10]$ лежит $x=8$. Сравним $f(6)=\dfrac{36}{2}=18$, $f(8)=\dfrac{64}{4}=16$, $f(10)=\dfrac{100}{6}=16\dfrac23$. Наименьшее значение $16$ (при $x=8$).`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 22' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Найдите значение выражения $\log_3\left(\dfrac{a}{9}\right)-\log_3\left(\dfrac{81}{b}\right)$, если $\log_3(ab)=17$.`,
|
||||
answer: '11',
|
||||
sol: R`$\log_3\dfrac{a}{9}-\log_3\dfrac{81}{b}=\log_3 a-\log_3 9-\log_3 81+\log_3 b=\log_3(ab)-2-4=\log_3(ab)-6$. Подставив $\log_3(ab)=17$, получим $17-6=11$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 7' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`$ABCA_1B_1C_1$ — правильная треугольная призма, длина стороны основания которой равна $7$, а бокового ребра — $\sqrt{29}$. Найдите периметр сечения призмы плоскостью, проходящей через прямую $A_1C_1$ и середину ребра $BB_1$.`,
|
||||
answer: '22',
|
||||
sol: R`Пусть $K$ — середина ребра $BB_1$. Сечение — равнобедренный треугольник $A_1KC_1$ с $A_1C_1=7$ и $KA_1=KC_1$. Из прямоугольного треугольника $KB_1A_1$: $KB_1=\dfrac{\sqrt{29}}{2}$, $A_1B_1=7$, поэтому $KA_1=\sqrt{49+\dfrac{29}{4}}=\sqrt{\dfrac{225}{4}}=\dfrac{15}{2}$. Периметр $P=A_1C_1+KA_1+KC_1=7+2\cdot\dfrac{15}{2}=22$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 3' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений системы неравенств $\begin{cases}x^{2}+8x+7\ge0,\\(x+9)(4-x)>0.\end{cases}$`,
|
||||
answer: '-10',
|
||||
sol: R`$x^{2}+8x+7\ge0\Rightarrow(x+7)(x+1)\ge0$, решение $(-\infty;-7]\cup[-1;+\infty)$. $\ (x+9)(4-x)>0\Rightarrow x\in(-9;4)$. Пересечение — $(-9;-7]\cup[-1;4)$. Целые: $-8,-7,-1,0,1,2,3$; их сумма равна $-10$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 16' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму квадратов корней уравнения $\sqrt{2x+6}-\sqrt{x+1}=2$.`,
|
||||
answer: '226',
|
||||
sol: R`Перепишем: $\sqrt{2x+6}=2+\sqrt{x+1}$. Возведя в квадрат: $2x+6=4+4\sqrt{x+1}+x+1$, $x+1=4\sqrt{x+1}$. Ещё раз в квадрат: $x^{2}-14x-15=0$, корни $-1$ и $15$ (оба проходят проверку). Сумма квадратов $(-1)^{2}+15^{2}=1+225=226$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 4,
|
||||
text: R`В прямоугольном треугольнике $KMN$ угол $M$ равен $90^\circ$, а $KN=6\sqrt2$. Точка $A$, не лежащая в плоскости треугольника $KMN$, удалена на расстояние $7$ от каждой вершины треугольника. Найдите значение выражения $21\sqrt2\cdot\cos\alpha$, где $\alpha$ — угол между прямой $AM$ и плоскостью $KMN$.`,
|
||||
answer: '18',
|
||||
sol: R`Так как точка $A$ равноудалена от вершин, основание $O$ перпендикуляра $AO$ — центр описанной около прямоугольного треугольника окружности, то есть середина гипотенузы $KN$, причём $MO=\dfrac{KN}{2}=3\sqrt2$. Угол между $AM$ и плоскостью — это $\angle AMO=\alpha$. В прямоугольном треугольнике $AOM$: $\cos\alpha=\dfrac{MO}{AM}=\dfrac{3\sqrt2}{7}$. Тогда $21\sqrt2\cdot\dfrac{3\sqrt2}{7}=\dfrac{21\cdot3\cdot2}{7}=18$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 9' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $\sqrt3\sin5x+\cos5x=0$ на промежутке $(-45^\circ;0^\circ)$.`,
|
||||
answer: '-48',
|
||||
sol: R`Разделив на $\cos5x$: $\sqrt3\operatorname{tg}5x+1=0$, $\operatorname{tg}5x=-\dfrac{\sqrt3}{3}$, откуда $5x=-30^\circ+180^\circ n$, $x=-6^\circ+36^\circ n$. Промежутку $(-45^\circ;0^\circ)$ принадлежат $-6^\circ$ и $-42^\circ$; их сумма равна $-48^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\dfrac{8}{1+\log_3 x}>-1+\log_3 x$.`,
|
||||
answer: '351',
|
||||
sol: R`Пусть $t=\log_3 x$, тогда $\dfrac{8}{1+t}>t-1$, что приводит к $\dfrac{(t-3)(t+3)}{t+1}<0$, решение $t<-3$ или $-1<t<3$. Тогда $0<x<\dfrac{1}{27}$ (целых нет) или $\dfrac13<x<27$ (целые $1,2,\ldots,26$). Сумма $1+2+\ldots+26=351$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`По углам листа картона прямоугольной формы вырезали четыре одинаковых квадрата со стороной $5$ см. Края заготовки загнули и получили коробку в форме прямоугольного параллелепипеда. Если бы длину каждой стороны листа картона уменьшили на $2$ см, то объём изготовленной коробки был бы на $0{,}12$ дм³ меньше. Найдите периметр исходного листа картона (в см).`,
|
||||
answer: '68',
|
||||
sol: R`Пусть длина листа $x$ см, ширина $y$ см. Объём коробки $5(x-10)(y-10)$ см³. После уменьшения сторон на $2$ см объём $5(x-12)(y-12)$ см³, и он на $120$ см³ ($0{,}12$ дм³) меньше: $5(x-10)(y-10)=5(x-12)(y-12)+120$. После преобразований $2(x+y)=68$, то есть периметр листа равен $68$ см.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 11' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`Сфера с радиусом $4$ касается всех сторон равнобедренного треугольника $KMN$, у которого длина основания $KM$ равна $10$, а длина боковой стороны $MN$ равна $13$. Найдите значение выражения $3\sqrt{11}\cdot V$, где $V$ — объём пирамиды $OKMN$ ($O$ — центр сферы).`,
|
||||
answer: '440',
|
||||
sol: R`Точки касания лежат в плоскости вписанной в треугольник окружности с центром $O_1$ (проекция $O$). Радиус вписанной окружности $r=\dfrac{S}{p}$, где $S$ — площадь, $p$ — полупериметр. Полупериметр $p=\dfrac{10+13+13}{2}=18$; по формуле Герона $S=\sqrt{18\cdot8\cdot5\cdot5}=60$. Тогда $r=\dfrac{60}{18}=\dfrac{10}{3}$. Высота $OO_1=\sqrt{R^{2}-r^{2}}=\sqrt{16-\dfrac{100}{9}}=\dfrac{2\sqrt{11}}{3}$. Объём $V=\dfrac13\cdot S\cdot OO_1=\dfrac13\cdot60\cdot\dfrac{2\sqrt{11}}{3}=\dfrac{40\sqrt{11}}{3}$. Тогда $3\sqrt{11}\cdot V=3\sqrt{11}\cdot\dfrac{40\sqrt{11}}{3}=40\cdot11=440$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3; разд. 3, § 5' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_rt2223_e3v1 (${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), ' | с фигурой:', TASKS.filter(t => t.fig).length, '\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_rt2223_e3v1.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 → «Варианты» → «РТ-2022/23 · этап III».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -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$. Укажите номера верных утверждений.<br>1) областью определения функции является множество всех действительных чисел;<br>2) функция возрастает на промежутке $(-\infty;0]$;<br>3) функция является чётной;<br>4) $f(-5)=-11$;<br>5) $f(3)<0$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
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`Укажите номера выражений, которые не имеют смысла.<br>1) $\arccos\dfrac{\sqrt3}{2}$;<br>2) $\arcsin\sqrt2$;<br>3) $\operatorname{ctg}\left(-\dfrac{3\pi}{2}\right)$;<br>4) $\operatorname{tg}\left(-\dfrac{3\pi}{2}\right)$;<br>5) $\operatorname{arctg}\dfrac{\sqrt3}{3}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
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 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Наибольший простой делитель числа $14$ равен …<br>Б) Наименьшее общее кратное чисел $5$ и $55$ равно …<br>В) Наибольший общий делитель чисел $16$ и $55$ равен …<br><b>Окончание:</b><br>1) $1$; 2) $110$; 3) $55$; 4) $5$; 5) $7$; 6) $2$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
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$ соответственно. Выберите верные утверждения.<br>1) прямая $C_1D_1$ перпендикулярна прямой $AD_1$;<br>2) прямая $B_1D_1$ параллельна прямой $BC$;<br>3) прямая $AD_1$ параллельна плоскости $BB_1C_1$;<br>4) прямая $B_1D_1$ перпендикулярна прямой $AD_1$;<br>5) прямая $AA_1$ параллельна прямой $B_1D_1$;<br>6) прямая $CC_1$ параллельна плоскости $BAA_1$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
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\dfrac56<x<23$. Наименьшее целое решение $-2$, наибольшее $22$; их сумма равна $20$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`Радиус окружности, описанной около прямоугольного треугольника $ABC$ ($\angle ABC=90^\circ$), равен $6$. Найдите значение выражения $\sqrt2\cdot S$, где $S$ — площадь треугольника $ABC$, если известно, что $\cos\angle ACB=\dfrac{\sqrt6}{3}$.`,
|
||||
answer: '48',
|
||||
sol: R`Гипотенуза $AC=2R=12$. Из $\cos\angle ACB=\dfrac{BC}{AC}=\dfrac{\sqrt6}{3}$ получаем $BC=4\sqrt6$. По теореме Пифагора $AB=\sqrt{AC^{2}-BC^{2}}=\sqrt{144-96}=4\sqrt3$. Площадь $S=\dfrac12\cdot AB\cdot BC=\dfrac12\cdot4\sqrt3\cdot4\sqrt6=24\sqrt2$. Тогда $\sqrt2\cdot S=24\cdot2=48$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 2, § 9' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите увеличенное в $8$ раз произведение корней уравнения $(0{,}9)^{4x^{2}-3x-56}=0{,}81$.`,
|
||||
answer: '-116',
|
||||
sol: R`Так как $0{,}81=(0{,}9)^{2}$, получаем $4x^{2}-3x-56=2$, то есть $4x^{2}-3x-58=0$. Дискриминант положителен, корни существуют. По теореме Виета их произведение равно $\dfrac{-58}{4}=-14{,}5$. Увеличенное в $8$ раз: $8\cdot(-14{,}5)=-116$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 2,
|
||||
text: R`Найдите тангенс угла наклона к оси абсцисс касательной, проведённой к графику функции $f(x)=36x^{3}-24x+2$ в точке с абсциссой $x_0=\dfrac16$.`,
|
||||
answer: '-21',
|
||||
sol: R`Тангенс угла наклона касательной равен значению производной: $f'(x)=108x^{2}-24$, $f'\!\left(\dfrac16\right)=108\cdot\dfrac{1}{36}-24=3-24=-21$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $32\cdot\bigl(\cos28^\circ\cos(-32^\circ)+\sin28^\circ\sin(-32^\circ)\bigr)$.`,
|
||||
answer: '16',
|
||||
sol: R`$\cos28^\circ\cos(-32^\circ)+\sin28^\circ\sin(-32^\circ)=\cos28^\circ\cos32^\circ-\sin28^\circ\sin32^\circ=\cos(28^\circ+32^\circ)=\cos60^\circ=\dfrac12$. Тогда $32\cdot\dfrac12=16$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 10' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
|
||||
text: R`Острый угол ромба $ABCD$ равен $30^\circ$. Через вершину $B$ тупого угла проведён отрезок $OB$, перпендикулярный плоскости ромба $ABCD$. Найдите площадь ромба $ABCD$, если $OB=6$ и расстояние от точки $O$ до стороны $AD$ ромба равно $8$.`,
|
||||
answer: '56',
|
||||
sol: R`Пусть $BK$ — высота ромба ($BK\perp AD$). По теореме о трёх перпендикулярах $OK\perp AD$, поэтому $OK=8$ — расстояние от $O$ до $AD$. Из прямоугольного треугольника $OBK$: $BK=\sqrt{OK^{2}-OB^{2}}=\sqrt{64-36}=2\sqrt7$. В прямоугольном треугольнике $AKB$ угол при $A$ равен $30^\circ$, поэтому $AB=2\,BK=4\sqrt7$. Площадь $S=AD\cdot BK=4\sqrt7\cdot2\sqrt7=56$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 15' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\dfrac{(x+7)^{2}(x-4)}{x^{3}(x+3)}\ge0$ на промежутке $[-9;9]$.`,
|
||||
answer: '29',
|
||||
sol: R`Методом интервалов: нули числителя $-7$ (чётной кратности) и $4$; нули знаменателя $0$ и $-3$ (выколоты). Решение неравенства: $\{-7\}\cup(-3;0)\cup[4;+\infty)$. На $[-9;9]$ целые решения: $-7,\ -2,\ -1,\ 4,\ 5,\ 6,\ 7,\ 8,\ 9$. Их сумма равна $29$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите увеличенную в $16$ раз сумму корней уравнения $\sqrt[4]{x^{4}-8x^{2}+13x-5}=x$.`,
|
||||
answer: '26',
|
||||
sol: R`Уравнение равносильно системе $x^{4}-8x^{2}+13x-5=x^{4}$ при $x\ge0$, то есть $8x^{2}-13x+5=0$, $x\ge0$. Корни $x=1$ и $x=\dfrac58$ (оба неотрицательны). Сумма корней $1+\dfrac58=\dfrac{13}{8}$. Увеличенная в $16$ раз: $16\cdot\dfrac{13}{8}=26$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 4,
|
||||
text: R`Площадь полной поверхности цилиндра равна $60\pi$, площадь боковой поверхности равна $24\pi$. Найдите значение выражения $S^{2}$, где $S$ — площадь сечения цилиндра плоскостью, параллельной его оси и проходящей на расстоянии $\sqrt6$ от неё.`,
|
||||
answer: '384',
|
||||
sol: R`Полная поверхность равна сумме боковой и двух оснований: $60\pi=24\pi+2S_0$, где $S_0$ — площадь основания. Тогда $S_0=18\pi$ и $\pi r^{2}=18\pi$, $r=3\sqrt2$. Из $2\pi r h=24\pi$ находим $h=2\sqrt2$. Сечение — прямоугольник со сторонами $h=2\sqrt2$ и хордой $AD$; перпендикуляр из центра основания к хорде равен $\sqrt6$, поэтому $\dfrac{AD}{2}=\sqrt{r^{2}-6}=\sqrt{12}=2\sqrt3$, $AD=4\sqrt3$. Площадь $S=AD\cdot h=4\sqrt3\cdot2\sqrt2=8\sqrt6$, откуда $S^{2}=64\cdot6=384$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите (в градусах) наименьший положительный корень уравнения $\sin3x\cos15^\circ+\cos3x\sin15^\circ=0$.`,
|
||||
answer: '55',
|
||||
sol: R`По формуле синуса суммы левая часть равна $\sin(3x+15^\circ)$. Уравнение $\sin(3x+15^\circ)=0$ даёт $3x+15^\circ=180^\circ n$, откуда $x=-5^\circ+60^\circ n$. Наименьший положительный корень при $n=1$: $x=55^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 10' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 4,
|
||||
text: R`Велосипедист, скорость движения которого $v$ км/ч, из пункта $A$ доехал в пункт $B$ без остановок и сразу поехал назад в пункт $A$. Он двигался по той же дороге с той же скоростью, а через час сделал остановку на $15$ мин, после чего продолжил путь, увеличив скорость на $1$ км/ч. Найдите наименьшее возможное целое значение скорости $v$ (в км/ч), при котором на обратный путь из $B$ в $A$ велосипедист затратит времени не меньше, чем на путь из $A$ в $B$, если расстояние между пунктами $42$ км.`,
|
||||
answer: '11',
|
||||
sol: R`Время из $A$ в $B$ равно $\dfrac{42}{v}$ ч. Обратный путь: первый час со скоростью $v$, остановка $\dfrac14$ ч, затем со скоростью $v+1$, поэтому время равно $1+\dfrac14+\dfrac{42-v}{v+1}$ ч. Условие $1+\dfrac14+\dfrac{42-v}{v+1}\ge\dfrac{42}{v}$ при $v>0$ приводит к $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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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();
|
||||
@@ -0,0 +1,364 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2324_e2v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2023/2024, Этап II, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
|
||||
Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\РТ\2023-2024\МАТ РТ-2 23_24 В1.pdf
|
||||
|
||||
variant=105 — Этап II РТ-2023/24 (этап I — 104, этап III — 106).
|
||||
Геометрия закодирована текстом (стандартная разметка фигур / углы словами) —
|
||||
отдельных чертежей не требуется (как у большинства существующих задач).
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2324_e2v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2324_e2v1.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 = 105;
|
||||
const PROV = 'РТ–2023/2024, Этап II, Вариант 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: 'stereometry', subtopic: 'ster-basics', diff: 2,
|
||||
text: R`Дана треугольная пирамида $SABC$. Точка $K$ принадлежит ребру $SC$. Среди прямых $SB$, $AK$, $SC$, $BK$, $BA$ укажите прямую, по которой пересекаются плоскости $BKA$ и $SBC$.`,
|
||||
opts: mc('$SB$', '$AK$', '$SC$', '$BK$', '$BA$'),
|
||||
answer: 'г',
|
||||
sol: R`Точка $K$ лежит на ребре $SC$, поэтому она принадлежит плоскости $SBC$. Точки $B$ и $K$ принадлежат обеим плоскостям $BKA$ и $SBC$, значит, плоскости пересекаются по прямой $BK$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Наименьшим целым числом, принадлежащим области определения функции $y=\log_5(x+6)$, является число:`,
|
||||
opts: mc('$-7$', '$-6$', '$-5$', '$-1$', '$0$'),
|
||||
answer: 'в',
|
||||
sol: R`Логарифмическая функция $y=\log_5 t$ определена при $t>0$, поэтому $x+6>0$, то есть $x>-6$ и $D(y)=(-6;+\infty)$. Наименьшее целое число из этого промежутка — $-5$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 8' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1,
|
||||
text: R`Укажите номер числового промежутка, который является решением неравенства $x\ge-10$.`,
|
||||
opts: mc('$(-10;+\infty)$', '$[-10;+\infty)$', '$(-\infty;-10]$', '$(-\infty;-10)$', '$[-10;0)$'),
|
||||
answer: 'б',
|
||||
sol: R`Решением неравенства $x\ge-10$ является числовой луч $[-10;+\infty)$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 5–6' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Укажите номер функции, графиком которой является гипербола.`,
|
||||
opts: mc('$f(x)=-x^{2}+7$', '$f(x)=x^{3}$', '$f(x)=|x|-7$', '$f(x)=-\dfrac{x}{7}$', '$f(x)=-\dfrac{7}{x}$'),
|
||||
answer: 'д',
|
||||
sol: R`Графиком обратной пропорциональности $y=\dfrac{k}{x}$ ($k\ne0$) является гипербола. Среди предложенных функций обратную пропорциональность задаёт только $f(x)=-\dfrac{7}{x}$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-rational', diff: 2,
|
||||
text: R`Укажите номер уравнения, равносильного уравнению $x=6$.`,
|
||||
opts: mc('$\sqrt{x-5}=1$', '$\log_6 x=0$', '$x^{2}=6$', '$x+6=0$', '$6^{x}=36$'),
|
||||
answer: 'а',
|
||||
sol: R`Уравнения равносильны, если имеют одно и то же множество корней. Уравнение $\sqrt{x-5}=1$ даёт $x-5=1$, то есть $x=6$ — то же множество корней. (Остальные: $\log_6 x=0\Rightarrow x=1$; $x^{2}=6\Rightarrow x=\pm\sqrt6$; $x+6=0\Rightarrow x=-6$; $6^{x}=36\Rightarrow x=2$.)`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 15' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Укажите номера верных неравенств.<br>1) $\sqrt5<2$;<br>2) $3<2\sqrt3$;<br>3) $\sqrt{(-4)^{2}}<-3$;<br>4) $0{,}82>\sqrt{0{,}81}$;<br>5) $\sqrt{(-5)^{2}}>\sqrt{(-3)^{2}}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '25', ansShow: '2, 5',
|
||||
sol: R`$1)$ неверно: возведя в квадрат, получим ложное $5<4$. $\ 2)$ верно: $9<12$. $\ 3)$ неверно: $\sqrt{(-4)^{2}}=4$, а $4<-3$ ложно. $\ 4)$ неверно: $\sqrt{0{,}81}=0{,}9$, а $0{,}82>0{,}9$ ложно. $\ 5)$ верно: $\sqrt{(-5)^{2}}=5$, $\sqrt{(-3)^{2}}=3$, и $5>3$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 1–4' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Найдите расстояние (в километрах) между двумя посёлками, если $\dfrac49$ этого расстояния на $10$ км меньше всего расстояния между ними.`,
|
||||
opts: mc('$22{,}5$', '$20$', '$15$', '$24$', '$18$'),
|
||||
answer: 'д',
|
||||
sol: R`Числу $10$ соответствует дробь $1-\dfrac49=\dfrac59$ всего расстояния. Тогда расстояние равно $10:\dfrac59=\dfrac{10\cdot9}{5}=18$ (км).`,
|
||||
ref: 'Математика, 5 класс, ч. 2, гл. 3, § 10' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Радиус основания конуса равен $7$. Если осевым сечением конуса является равносторонний треугольник, то образующая конуса равна:`,
|
||||
opts: mc('$7$', '$3{,}5$', '$14$', '$6$', '$7\sqrt2$'),
|
||||
answer: 'в',
|
||||
sol: R`Осевое сечение конуса — равнобедренный треугольник с основанием, равным диаметру, и боковыми сторонами, равными образующим. Если это равносторонний треугольник, то образующая равна диаметру основания, то есть $2\cdot7=14$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 4' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\sqrt6\cdot\cos(-135^\circ)$.`,
|
||||
opts: mc('$\dfrac{3\sqrt2}{2}$', '$-\sqrt3$', '$-\dfrac{3\sqrt2}{2}$', '$-\dfrac{\sqrt6}{2}$', '$\sqrt3$'),
|
||||
answer: 'б',
|
||||
sol: R`$\sqrt6\cdot\cos(-135^\circ)=\sqrt6\cdot\cos135^\circ=\sqrt6\cdot\cos(180^\circ-45^\circ)=-\sqrt6\cdot\cos45^\circ=-\sqrt6\cdot\dfrac{\sqrt2}{2}=-\sqrt3.$`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 2; § 9' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Укажите номера выражений, которые не имеют смысла при $x=-5$.<br>1) $\sqrt{x-5}$;<br>2) $\dfrac{1}{\sqrt{5-x}}$;<br>3) $\dfrac{1}{\sqrt{x+5}}$;<br>4) $\sqrt{-x-5}$;<br>5) $\sqrt{x+5}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '13', ansShow: '1, 3',
|
||||
sol: R`$1)$ $\sqrt{x-5}$ имеет смысл при $x\ge5$; число $-5$ не входит — нет смысла. $\ 2)$ $\dfrac{1}{\sqrt{5-x}}$ имеет смысл при $x<5$ — смысл есть. $\ 3)$ $\dfrac{1}{\sqrt{x+5}}$ имеет смысл при $x>-5$; число $-5$ не входит — нет смысла. $\ 4)$ $\sqrt{-x-5}$ имеет смысл при $x\le-5$ — смысл есть. $\ 5)$ $\sqrt{x+5}$ имеет смысл при $x\ge-5$ — смысл есть.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 4' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 3,
|
||||
text: R`Дан прямоугольный треугольник $ABC$ ($\angle ABC=90^\circ$), в котором катет $AB=\sqrt7$, катет $BC=\sqrt2$. Выберите верные утверждения.<br>1) площадь треугольника $ABC$ равна $\sqrt{14}$;<br>2) косинус угла $ACB$ равен $\dfrac{\sqrt2}{3}$;<br>3) синус угла $BAC$ равен $\dfrac{\sqrt7}{3}$;<br>4) тангенс угла $ACB$ равен $\dfrac{\sqrt{14}}{7}$;<br>5) котангенс угла $BAC$ равен $\dfrac{\sqrt{14}}{2}$;<br>6) радиус окружности, описанной около треугольника $ABC$, равен $1{,}5$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '256', ansShow: '2, 5, 6',
|
||||
sol: R`По теореме Пифагора $AC=\sqrt{AB^{2}+BC^{2}}=\sqrt{7+2}=3$. $\ 1)$ неверно: $S=\dfrac12\cdot\sqrt7\cdot\sqrt2=\dfrac{\sqrt{14}}{2}$. $\ 2)$ верно: $\cos\angle ACB=\dfrac{BC}{AC}=\dfrac{\sqrt2}{3}$. $\ 3)$ неверно: $\sin\angle BAC=\dfrac{BC}{AC}=\dfrac{\sqrt2}{3}$. $\ 4)$ неверно: $\operatorname{tg}\angle ACB=\dfrac{AB}{BC}=\dfrac{\sqrt7}{\sqrt2}=\dfrac{\sqrt{14}}{2}$. $\ 5)$ верно: $\operatorname{ctg}\angle BAC=\dfrac{AB}{BC}=\dfrac{\sqrt{14}}{2}$. $\ 6)$ верно: радиус описанной около прямоугольного треугольника окружности равен половине гипотенузы, то есть $1{,}5$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 15–16' },
|
||||
|
||||
{ idx: 12, type: 'long', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Если функция $y=f(x)$ является чётной и $f(6)=-5$, то $f(-6)$ равно …<br>Б) Если функция $y=g(x)$ является нечётной и $g(-3)=2$, то $g(3)$ равно …<br>В) Если функция $y=h(x)$ является чётной и $h(1)=2$, а функция $y=p(x)$ является нечётной и $p(1)=15$, то значение выражения $h(-1)\cdot p(-1)$ равно …<br><b>Окончание:</b><br>1) $30$; 2) $5$; 3) $-2$; 4) $2$; 5) $-30$; 6) $-5$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А6Б3В5', ansShow: 'А6Б3В5',
|
||||
sol: R`А) Для чётной функции $f(-x)=f(x)$, поэтому $f(-6)=f(6)=-5$ — окончание 6. Б) Для нечётной функции $g(-x)=-g(x)$, поэтому $g(3)=-g(-3)=-2$ — окончание 3. В) $h(-1)=h(1)=2$, $p(-1)=-p(1)=-15$, тогда $h(-1)\cdot p(-1)=2\cdot(-15)=-30$ — окончание 5.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 8' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Наибольшее натуральное число, которое при делении на $16$ с остатком даёт неполное частное, равное $6$, равно …`,
|
||||
answer: '111',
|
||||
sol: R`При делении с остатком на $16$ наибольший возможный остаток равен $15$. По формуле $a=q\cdot b+r$ получаем наибольшее число $16\cdot6+15=111$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 11' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Найдите значение выражения $5^{\,1+2\log_5 8}$.`,
|
||||
answer: '320',
|
||||
sol: R`$5^{\,1+2\log_5 8}=5\cdot\left(5^{\log_5 8}\right)^{2}=5\cdot8^{2}=5\cdot64=320.$`,
|
||||
ref: 'Алгебра, 11 класс, гл. 1, § 3; гл. 3, § 7' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`Длина ребра основания правильной треугольной призмы $ABCA_1B_1C_1$ относится к длине бокового ребра как $12:5$. Найдите длину $l$ замкнутой ломаной $BCA_1AB$, если длина бокового ребра равна $5\sqrt6$. В ответ запишите значение выражения $l\cdot\sqrt6$.`,
|
||||
answer: '252',
|
||||
sol: R`Боковое ребро $A_1A=5\sqrt6$, тогда из отношения $\dfrac{AB}{5\sqrt6}=\dfrac{12}{5}$ находим ребро основания $AB=BC=AC=12\sqrt6$. Диагональ боковой грани $CA_1=\sqrt{AC^{2}+A_1A^{2}}=\sqrt{(12\sqrt6)^{2}+(5\sqrt6)^{2}}=\sqrt{864+150}=\sqrt{1014}=13\sqrt6$. Замкнутая ломаная $BCA_1AB$ равна $BC+CA_1+A_1A+AB=12\sqrt6+13\sqrt6+5\sqrt6+12\sqrt6=42\sqrt6$, то есть $l=42\sqrt6$. Тогда $l\cdot\sqrt6=42\cdot6=252$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 1–2' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Проездной билет на месяц стоит $42$ рубля, а стоимость билета на одну поездку составляет $2\%$ от стоимости проездного билета на месяц. Какую сумму (в рублях) сэкономил Витя, если он купил проездной билет на месяц и сделал по нему за месяц $75$ поездок?`,
|
||||
answer: '21',
|
||||
sol: R`Стоимость билета на одну поездку равна $42\cdot0{,}02=0{,}84$ рубля. За $75$ поездок без проездного Витя заплатил бы $75\cdot0{,}84=63$ рубля. Таким образом, он сэкономил $63-42=21$ рубль.`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 1–2' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Найдите сумму бесконечно убывающей геометрической прогрессии, у которой второй член равен $14$, а знаменатель равен $\dfrac23$.`,
|
||||
answer: '63',
|
||||
sol: R`Первый член $b_1=\dfrac{b_2}{q}=\dfrac{14}{2/3}=21$. По формуле суммы бесконечно убывающей прогрессии $S=\dfrac{b_1}{1-q}=\dfrac{21}{1-\frac23}=\dfrac{21}{1/3}=63.$`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 19' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Найдите значение выражения $\left(0{,}6\sqrt{x}+\sqrt[4]{y}\right)^{2}-\left(0{,}6\sqrt{x}-\sqrt[4]{y}\right)^{2}$ при $x=300$, $y=9$.`,
|
||||
answer: '72',
|
||||
sol: R`По формуле разности квадратов выражение равно $\left(0{,}6\sqrt{x}+\sqrt[4]{y}+0{,}6\sqrt{x}-\sqrt[4]{y}\right)\left(0{,}6\sqrt{x}+\sqrt[4]{y}-0{,}6\sqrt{x}+\sqrt[4]{y}\right)=1{,}2\sqrt{x}\cdot2\sqrt[4]{y}=2{,}4\sqrt{x}\,\sqrt[4]{y}$. При $x=300$, $y=9$: $2{,}4\sqrt{300}\,\sqrt[4]{9}=2{,}4\cdot10\sqrt3\cdot\sqrt3=2{,}4\cdot10\cdot3=72.$`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 12–13' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Найдите наибольшее целое решение совокупности неравенств $\left[\begin{array}{l}3x+10<0,\\[2pt]-\dfrac12\,x-3>0.\end{array}\right.$`,
|
||||
answer: '-4',
|
||||
sol: R`$3x+10<0\Rightarrow x<-\dfrac{10}{3}=-3\dfrac13$; $\ -\dfrac12x-3>0\Rightarrow x<-6$. Объединение открытых лучей есть множество $x\in\left(-\infty;-3\dfrac13\right)$. Наибольшее целое решение равно $-4$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 3,
|
||||
text: R`В трапеции $ABCD$ основания $BC$ и $AD$ равны $16$ и $18$ соответственно. Диагонали трапеции пересекаются в точке $O$, причём $OA=9$. Найдите длину диагонали $AC$.`,
|
||||
answer: '17',
|
||||
sol: R`Треугольники $AOD$ и $COB$ подобны по двум углам ($\angle AOD=\angle COB$ как вертикальные, $\angle OAD=\angle OCB$ как накрест лежащие при $BC\parallel AD$ и секущей $AC$). Из подобия $\dfrac{BC}{AD}=\dfrac{CO}{AO}$, то есть $\dfrac{16}{18}=\dfrac{CO}{9}$, откуда $CO=8$. Тогда $AC=AO+CO=9+8=17.$`,
|
||||
ref: 'Геометрия, 8 класс, гл. 1, § 10–11; гл. 3, § 21' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Найдите сумму всех целых значений переменной $x$, при которых имеет смысл выражение $\sqrt{12x-x^{2}}+\dfrac{x}{\sqrt{(x+2)(9-x)}}$.`,
|
||||
answer: '36',
|
||||
sol: R`Выражение имеет смысл при $\begin{cases}12x-x^{2}\ge0,\\(x+2)(9-x)>0.\end{cases}$ Первое неравенство даёт $x\in[0;12]$, второе — $x\in(-2;9)$. Пересечение — полуинтервал $[0;9)$. Сумма всех целых из него: $0+1+2+\ldots+8=36.$`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 16' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 3,
|
||||
text: R`Катер прошёл $21$ км по течению реки за $1$ ч $30$ мин, а против течения реки за такое же время — только $18$ км. Найдите (в км/ч) собственную скорость катера, если она и скорость течения реки были постоянными.`,
|
||||
answer: '13',
|
||||
sol: R`Пусть собственная скорость катера $x$ км/ч, скорость течения $y$ км/ч. За $1{,}5$ ч: $(x+y)\cdot1{,}5=21$ и $(x-y)\cdot1{,}5=18$, то есть $\begin{cases}x+y=14,\\x-y=12.\end{cases}$ Сложив уравнения, получаем $2x=26$, $x=13.$`,
|
||||
ref: 'Алгебра, 7 класс, гл. 4, § 25' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`Длина ребра куба $ABCDA_1B_1C_1D_1$ равна $7\sqrt2$. Найдите значение выражения $\sqrt3\cdot S$, где $S$ — площадь сечения этого куба плоскостью, проходящей через точки $A_1$, $B$, $D$.`,
|
||||
answer: '147',
|
||||
sol: R`Сечение — треугольник $A_1BD$, стороны которого являются диагоналями граней куба: $A_1B=A_1D=BD=7\sqrt2\cdot\sqrt2=14$. Это равносторонний треугольник со стороной $14$, его площадь $S=\dfrac{14^{2}\sqrt3}{4}=49\sqrt3$. Тогда $\sqrt3\cdot S=\sqrt3\cdot49\sqrt3=147.$`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 3' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений неравенства $3^{\,2x+18}-10\cdot3^{\,x+9}+9\le0$.`,
|
||||
answer: '-24',
|
||||
sol: R`Пусть $t=3^{\,x+9}$, тогда неравенство примет вид $t^{2}-10t+9\le0$, откуда $1\le t\le9$. Значит, $3^{0}\le3^{\,x+9}\le3^{2}$, то есть $0\le x+9\le2$ и $-9\le x\le-7$. Целые решения $-9,\,-8,\,-7$; их сумма равна $-24.$`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 3,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $\cos5x\cos2x+\sin5x\sin2x=0$ на промежутке $(0^\circ;135^\circ)$.`,
|
||||
answer: '120',
|
||||
sol: R`По формуле косинуса разности левая часть равна $\cos(5x-2x)=\cos3x$. Уравнение $\cos3x=0$ даёт $3x=90^\circ+180^\circ n$, $x=30^\circ+60^\circ n$. Промежутку $(0^\circ;135^\circ)$ принадлежат корни $30^\circ$ (при $n=0$) и $90^\circ$ (при $n=1$). Их сумма равна $120^\circ.$`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 10' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 4,
|
||||
text: R`Основание $AC$ равнобедренного треугольника $ABC$, у которого $\angle ABC=120^\circ$, лежит в плоскости $\alpha$, образующей с плоскостью треугольника угол $30^\circ$. Найдите квадрат расстояния от вершины $B$ треугольника $ABC$ до плоскости $\alpha$, если площадь треугольника $ABC$ равна $160\sqrt3$.`,
|
||||
answer: '40',
|
||||
sol: R`Площадь $S=\dfrac12\cdot AB\cdot BC\cdot\sin120^\circ$, где $AB=BC$. Тогда $160\sqrt3=\dfrac12 AB^{2}\cdot\dfrac{\sqrt3}{2}$, откуда $AB^{2}=640$, $AB=8\sqrt{10}$. Пусть $BK$ — высота треугольника к основанию $AC$; угол $BAK=30^\circ$, поэтому $BK=AB\sin30^\circ=4\sqrt{10}$. Пусть $BM$ — перпендикуляр к плоскости $\alpha$; по теореме о трёх перпендикулярах $\angle BKM=30^\circ$ — линейный угол двугранного. Тогда $BM=BK\sin30^\circ=2\sqrt{10}$ и $BM^{2}=40.$`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 10' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\log_{\frac16}\dfrac{x-7}{x-11}\ge0$ на промежутке $(-11;12)$.`,
|
||||
answer: '-34',
|
||||
sol: R`Так как $0=\log_{\frac16}1$, а основание $\dfrac16<1$ (функция убывает), неравенство равносильно системе $\begin{cases}\dfrac{x-7}{x-11}\le1,\\[4pt]\dfrac{x-7}{x-11}>0.\end{cases}$ Первое неравенство сводится к $\dfrac{4}{x-11}\le0$, то есть $x<11$; второе даёт $x<7$ или $x>11$. Решение системы — луч $(-\infty;7)$. Пересечение с $(-11;12)$ — интервал $(-11;7)$; сумма целых чисел из него ($-10,\ldots,6$) равна $-34.$`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите сумму квадратов корней уравнения $x^{2}+3x-\sqrt{x^{2}+3x+9}=3$.`,
|
||||
answer: '23',
|
||||
sol: R`Преобразуем к виду $x^{2}+3x+9-\sqrt{x^{2}+3x+9}-12=0$. Пусть $t=\sqrt{x^{2}+3x+9}\ge0$, тогда $t^{2}-t-12=0$, $t=4$ (корень $t=-3$ отброшен). Значит, $x^{2}+3x+9=16$, $x^{2}+3x-7=0$. По теореме Виета $x_1+x_2=-3$, $x_1x_2=-7$, поэтому $x_1^{2}+x_2^{2}=(x_1+x_2)^{2}-2x_1x_2=9+14=23.$`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 4,
|
||||
text: R`Найдите сумму всех целых чисел из промежутков возрастания функции $f(x)=\dfrac{33+2x^{2}}{2-x}$.`,
|
||||
answer: '16',
|
||||
sol: R`$D(f)=(-\infty;2)\cup(2;+\infty)$. Производная $f'(x)=\dfrac{-2x^{2}+8x+33}{(2-x)^{2}}$. Решая $f'(x)>0$, то есть $-2x^{2}+8x+33>0$, получаем $x\in\left(\dfrac{4-\sqrt{82}}{2};\dfrac{4+\sqrt{82}}{2}\right)$. С учётом $x\ne2$ функция возрастает на $\left[\dfrac{4-\sqrt{82}}{2};2\right)$ и $\left(2;\dfrac{4+\sqrt{82}}{2}\right]$. Целые из этих промежутков: $-2,\,-1,\,0,\,1,\,3,\,4,\,5,\,6$; их сумма равна $16.$`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
|
||||
text: R`Дана правильная шестиугольная пирамида $SABCDEF$, у которой угол между боковым ребром и плоскостью основания равен $\arccos\dfrac49$. Объём пирамиды $SABCDEF$ равен $18\sqrt{65}$. Найдите значение выражения $\dfrac{V}{\sqrt3\cdot\pi}$, где $V$ — объём шара, радиус которого равен длине бокового ребра пирамиды $SABCDEF$.`,
|
||||
answer: '729',
|
||||
sol: R`Пусть $O$ — центр основания, $SA=R$ — боковое ребро. Из прямоугольного треугольника $SOA$: $\cos\angle SAO=\dfrac{OA}{SA}=\dfrac49$, поэтому $SA=\dfrac94 OA$. Высота пирамиды $SO=\sqrt{SA^{2}-OA^{2}}=\dfrac{OA\sqrt{65}}{4}$. Для правильного шестиугольника $OA=AB$, а площадь основания $S_0=\dfrac{3\sqrt3}{2}AB^{2}$. Из формулы объёма $18\sqrt{65}=\dfrac13\cdot\dfrac{3\sqrt3}{2}AB^{2}\cdot\dfrac{AB\sqrt{65}}{4}$ получаем $AB^{3}=48\sqrt3$. Радиус шара $R=SA=\dfrac94\sqrt[3]{48\sqrt3}$, его объём $V=\dfrac43\pi R^{3}=\dfrac43\pi\cdot\dfrac{9^{3}}{4^{3}}\cdot48\sqrt3=729\sqrt3\,\pi$. Тогда $\dfrac{V}{\sqrt3\cdot\pi}=729.$`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3; разд. 3, § 6' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_e2v1 (${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_e2v1.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 · этап II».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,388 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2324_e3v1.js
|
||||
Чистый вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2023/2024, Этап III, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 + В1–В20.
|
||||
Перенабрано вручную в KaTeX по PDF (визуальное чтение, НЕ OCR):
|
||||
F:\!Рабочие\ЦТ\Математика\Математика\РТ\2023-2024\МАТ РТ-3 23_24 В1.pdf
|
||||
|
||||
variant=106 — Этап III РТ-2023/24 (этап I — 104, этап II — 105).
|
||||
Геометрия закодирована текстом (стандартная разметка фигур / углы словами).
|
||||
Исключение — В1 (чтение графика): кусочно-линейная нечётная функция
|
||||
воспроизведена inline-SVG в figure_html (как у math9-заданий); все 6
|
||||
утверждений и ответ (145) согласованы с реконструкцией.
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx).
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2324_e3v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2324_e3v1.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 = 106;
|
||||
const PROV = 'РТ–2023/2024, Этап III, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
/* opts: метки кириллица а–д (как в существующих строках ctmath; checkAnswerServer
|
||||
имеет ветку /^[а-д]$/). РТ-варианты 1..5 → а..д. */
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
/* ── SVG-график для В1: нечётная кусочно-линейная функция на [-6;6] через
|
||||
точки (-6,-3),(-4,1),(4,-1),(6,3). f(0)=0; возрастает на [-6;-4] и [4;6],
|
||||
убывает на [-4;4]. Цвета — только в SVG-стоки (как у math9-фигур). */
|
||||
const FIG_B1 = `<svg class="task-fig" viewBox="0 0 360 230" width="360" height="230" xmlns="http://www.w3.org/2000/svg" style="max-width:360px;width:100%;height:auto;display:block;margin:10px auto">
|
||||
<g stroke="#e2e8f0" stroke-width="1">
|
||||
<line x1="24" y1="31" x2="24" y2="199"/><line x1="50" y1="31" x2="50" y2="199"/><line x1="76" y1="31" x2="76" y2="199"/><line x1="102" y1="31" x2="102" y2="199"/><line x1="128" y1="31" x2="128" y2="199"/><line x1="154" y1="31" x2="154" y2="199"/><line x1="180" y1="31" x2="180" y2="199"/><line x1="206" y1="31" x2="206" y2="199"/><line x1="232" y1="31" x2="232" y2="199"/><line x1="258" y1="31" x2="258" y2="199"/><line x1="284" y1="31" x2="284" y2="199"/><line x1="310" y1="31" x2="310" y2="199"/><line x1="336" y1="31" x2="336" y2="199"/>
|
||||
<line x1="24" y1="31" x2="336" y2="31"/><line x1="24" y1="59" x2="336" y2="59"/><line x1="24" y1="87" x2="336" y2="87"/><line x1="24" y1="115" x2="336" y2="115"/><line x1="24" y1="143" x2="336" y2="143"/><line x1="24" y1="171" x2="336" y2="171"/><line x1="24" y1="199" x2="336" y2="199"/>
|
||||
</g>
|
||||
<line x1="16" y1="115" x2="346" y2="115" stroke="#334155" stroke-width="1.5"/>
|
||||
<line x1="180" y1="214" x2="180" y2="14" stroke="#334155" stroke-width="1.5"/>
|
||||
<polygon points="346,115 338,111 338,119" fill="#334155"/>
|
||||
<polygon points="180,14 176,22 184,22" fill="#334155"/>
|
||||
<polyline points="24,199 76,87 284,143 336,31" fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round"/>
|
||||
<text x="348" y="111" font-size="13" font-style="italic" fill="#334155">x</text>
|
||||
<text x="186" y="24" font-size="13" font-style="italic" fill="#334155">y</text>
|
||||
<text x="167" y="131" font-size="12" fill="#334155">O</text>
|
||||
<text x="203" y="131" font-size="12" fill="#334155">1</text>
|
||||
<text x="162" y="91" font-size="12" fill="#334155">1</text>
|
||||
<text x="290" y="28" font-size="13" font-style="italic" fill="#2563eb">y=f(x)</text>
|
||||
</svg>`;
|
||||
|
||||
/* ── 30 заданий ─────────────────────────────────────────────────────────── */
|
||||
const TASKS = [
|
||||
// ── Часть A: А1–А10 ──────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`На координатной плоскости отмечены точки $A(2;-1)$, $B(1;2)$, $C(-2;-2)$, $D(-1;1)$, $E(0;-2)$. Выберите ту из них, сумма координат которой равна $-4$.`,
|
||||
opts: mc('$A$', '$B$', '$C$', '$D$', '$E$'),
|
||||
answer: 'в',
|
||||
sol: R`Сумма координат: для $A$ это $2+(-1)=1$, для $B$ это $1+2=3$, для $C$ это $-2+(-2)=-4$, для $D$ это $-1+1=0$, для $E$ это $0+(-2)=-2$. Сумме $-4$ соответствует точка $C$.`,
|
||||
ref: 'Математика, 6 класс, гл. 5, § 1' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 3,
|
||||
text: R`Дан куб $ABCDA_1B_1C_1D_1$. Точка $K$ — середина диагонали $A_1D$. Среди отрезков $A_1B_1$, $B_1D_1$, $C_1K$, $BB_1$, $A_1C_1$ укажите отрезок, по которому плоскость, заданная прямой $DC_1$ и точкой $K$, пересекает плоскость грани $A_1B_1C_1D_1$.`,
|
||||
opts: mc('$A_1B_1$', '$B_1D_1$', '$C_1K$', '$BB_1$', '$A_1C_1$'),
|
||||
answer: 'д',
|
||||
sol: R`Секущая плоскость, заданная прямой $DC_1$ и точкой $K$, содержит точку $C_1$ (она на $DC_1$) и точку $A_1$ (так как $K$ — середина $A_1D$, прямая $DC_1$ и точка $K$ задают плоскость диагонального сечения, проходящую через $A_1$). Точки $A_1$ и $C_1$ принадлежат и грани $A_1B_1C_1D_1$, поэтому пересечение плоскостей — отрезок $A_1C_1$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 2–3' },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Расположите числа $\log_3 27$, $\ 3^{-1}$, $\ \sqrt{64}$ в порядке возрастания.`,
|
||||
opts: mc('$\sqrt{64};\ \log_3 27;\ 3^{-1}$', '$3^{-1};\ \sqrt{64};\ \log_3 27$', '$\sqrt{64};\ 3^{-1};\ \log_3 27$', '$3^{-1};\ \log_3 27;\ \sqrt{64}$', '$\log_3 27;\ \sqrt{64};\ 3^{-1}$'),
|
||||
answer: 'г',
|
||||
sol: R`$\log_3 27=\log_3 3^{3}=3$; $\ 3^{-1}=\dfrac13$; $\ \sqrt{64}=8$. Так как $\dfrac13<3<8$, числа в порядке возрастания: $3^{-1};\ \log_3 27;\ \sqrt{64}$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 1–4; гл. 11 кл., § 3' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
|
||||
text: R`Укажите номер выражения, тождественно равного выражению $a^{3}$.`,
|
||||
opts: mc('$a:a^{3}$', '$a\cdot a^{2}$', '$\left(a^{2}\right)^{2}$', '$3a$', '$a^{-3}$'),
|
||||
answer: 'б',
|
||||
sol: R`По свойству степеней $a\cdot a^{2}=a^{1+2}=a^{3}$. (Остальные: $a:a^{3}=a^{-2}$; $\left(a^{2}\right)^{2}=a^{4}$; $3a$ и $a^{-3}$ не равны $a^{3}$.)`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
|
||||
text: R`Результат разложения многочлена $4b^{2}+4bc-4b$ на множители имеет вид:`,
|
||||
opts: mc('$4b(b+c)$', '$4b(bc-1)$', '$4b(1+c)$', '$(4b-1)(b+c)$', '$4b(b+c-1)$'),
|
||||
answer: 'д',
|
||||
sol: R`Общий множитель членов многочлена $4b^{2}+4bc-4b$ — одночлен $4b$. Тогда $4b^{2}+4bc-4b=4b(b+c-1)$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Среди чисел $10$, $99$, $0$, $-10$, $100$ укажите номера тех, которые не входят в область определения выражения $\dfrac{1}{10-\sqrt{x}}$.<br>1) $10$; 2) $99$; 3) $0$; 4) $-10$; 5) $100$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '45', ansShow: '4, 5',
|
||||
sol: R`Выражение $\dfrac{1}{10-\sqrt{x}}$ имеет смысл при $x\ge0$ и $10-\sqrt{x}\ne0$, то есть $x\ge0$, $x\ne100$. Область определения $[0;100)\cup(100;+\infty)$. Из данных чисел ей не принадлежат $-10$ (так как $-10<0$) и $100$ (исключено). Это числа под номерами 4 и 5.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 1' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`За три четверти учебного года Петя использовал $\dfrac25$ купленных в начале учебного года тетрадей, после чего у него осталось $48$ тетрадей. Сколько тетрадей купил Петя в начале учебного года?`,
|
||||
opts: mc('$80$', '$96$', '$120$', '$74$', '$116$'),
|
||||
answer: 'а',
|
||||
sol: R`Числу $48$ соответствует дробь $1-\dfrac25=\dfrac35$ всех тетрадей. Тогда куплено $48:\dfrac35=\dfrac{48\cdot5}{3}=80$ тетрадей.`,
|
||||
ref: 'Математика, 5 класс, ч. 2, гл. 3, § 10' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\operatorname{tg}(-120^\circ)+\left|-\sqrt3\right|$.`,
|
||||
opts: mc('$0$', '$\dfrac{4\sqrt3}{3}$', '$1-\sqrt3$', '$2\sqrt3$', '$-\dfrac{2\sqrt3}{3}$'),
|
||||
answer: 'г',
|
||||
sol: R`$\operatorname{tg}(-120^\circ)=-\operatorname{tg}120^\circ=-\operatorname{tg}(180^\circ-60^\circ)=\operatorname{tg}60^\circ=\sqrt3$. Тогда $\operatorname{tg}(-120^\circ)+\left|-\sqrt3\right|=\sqrt3+\sqrt3=2\sqrt3.$`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 3; § 9' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-rotation', diff: 2,
|
||||
text: R`Сечение сферы плоскостью, отстоящей от её центра на расстоянии $3$, имеет радиус $3\sqrt2$. Найдите радиус сферы.`,
|
||||
opts: mc('$9\sqrt2$', '$3\sqrt3$', '$6$', '$12$', '$4\sqrt3$'),
|
||||
answer: 'б',
|
||||
sol: R`Радиус сферы $R$ — гипотенуза прямоугольного треугольника с катетами $3$ (расстояние до плоскости) и $3\sqrt2$ (радиус сечения). По теореме Пифагора $R^{2}=3^{2}+\left(3\sqrt2\right)^{2}=9+18=27$, поэтому $R=3\sqrt3$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3, § 5' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Укажите номера функций, которые принимают только положительные значения на промежутке $(4;+\infty)$.<br>1) $f(x)=-4x$;<br>2) $f(x)=\sqrt{x-4}$;<br>3) $f(x)=x^{3}-4$;<br>4) $f(x)=\log_{\frac14}x$;<br>5) $f(x)=-x^{2}-4$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '23', ansShow: '2, 3',
|
||||
sol: R`$1)$ $-4x$ при $x>4$ отрицательна. $\ 2)$ $\sqrt{x-4}$ при $x>4$ положительна. $\ 3)$ $x^{3}-4$ положительна при $x>\sqrt[3]{4}$, а $(4;+\infty)\subset(\sqrt[3]{4};+\infty)$ — положительна. $\ 4)$ $\log_{\frac14}x$ положительна только на $(0;1)$. $\ 5)$ $-x^{2}-4$ отрицательна при всех $x$. Подходят функции 2 и 3.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 13–14' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
fig: FIG_B1,
|
||||
text: R`На рисунке изображён график функции $y=f(x)$, определённой на промежутке $[-6;6]$. Выберите верные утверждения.<br>1) функция является нечётной;<br>2) $f(3)>0$;<br>3) график функции симметричен относительно оси ординат;<br>4) $f(-5)>f(-6)$;<br>5) функция убывает на промежутке $[-4;4]$;<br>6) график функции $y=f(x)+3$ проходит через точку $(0;2)$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '145', ansShow: '1, 4, 5',
|
||||
sol: R`$1)$ верно: график симметричен относительно начала координат, поэтому функция нечётная. $\ 2)$ неверно: по графику $f(3)<0$. $\ 3)$ неверно: график нечётной функции симметричен относительно начала координат, а не оси ординат. $\ 4)$ верно: на промежутке $[-6;-4]$ функция возрастает, поэтому $f(-5)>f(-6)$. $\ 5)$ верно: на отрезке $[-4;4]$ при увеличении $x$ значения функции уменьшаются. $\ 6)$ неверно: $f(0)=0$, поэтому график $y=f(x)+3$ проходит через точку $(0;3)$, а не $(0;2)$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 6–9' },
|
||||
|
||||
{ idx: 12, type: 'long', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — куб. Точки $M$ и $K$ — середины рёбер $A_1D_1$ и $AA_1$ соответственно. Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Величина угла между прямыми $A_1B_1$ и $KM$ равна …<br>Б) Величина угла между прямыми $B_1C_1$ и $KM$ равна …<br>В) Величина угла между прямыми $BD$ и $KM$ равна …<br><b>Окончание:</b><br>1) $30^\circ$; 2) $0^\circ$; 3) $60^\circ$; 4) $90^\circ$; 5) $120^\circ$; 6) $45^\circ$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А4Б6В3', ansShow: 'А4Б6В3',
|
||||
sol: R`А) Прямая $A_1B_1$ перпендикулярна плоскости грани $AA_1D_1D$, а $KM$ лежит в этой плоскости, поэтому $A_1B_1\perp KM$ — угол $90^\circ$ (окончание 4). Б) $B_1C_1\parallel A_1D_1$, поэтому угол между $B_1C_1$ и $KM$ равен углу $A_1MK$; в равнобедренном прямоугольном треугольнике $KA_1M$ ($A_1K=A_1M$) он равен $45^\circ$ (окончание 6). В) Через середину $P$ ребра проведём $MP\parallel B_1D_1$; треугольник $PMK$ равносторонний, поэтому угол между $BD$ и $KM$ равен $60^\circ$ (окончание 3).`,
|
||||
ref: 'Геометрия, 10 класс, разд. 2, § 4' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Найдите сумму всех натуральных делителей числа $95$.`,
|
||||
answer: '120',
|
||||
sol: R`Число $95=5\cdot19$ имеет четыре натуральных делителя: $1$, $5$, $19$ и $95$. Их сумма равна $1+5+19+95=120$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 12' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Найдите произведение наименьшего и наибольшего целых решений двойного неравенства $-51\dfrac13<-3x<4\sqrt2$.`,
|
||||
answer: '-17',
|
||||
sol: R`Разделим все части на $-3$ (знаки неравенства меняются на противоположные): $-\dfrac{4\sqrt2}{3}<x<17\dfrac19$. Так как $-\dfrac{4\sqrt2}{3}\approx-1{,}9$, наименьшее целое решение равно $-1$, наибольшее — $17$. Их произведение $-1\cdot17=-17$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`На отрезке $AB$, равном $13$ см $5$ мм, взята точка $C$ так, что $AC:AB=7:15$. Найдите (в миллиметрах) длину отрезка $CB$.`,
|
||||
answer: '72',
|
||||
sol: R`$AB=13$ см $5$ мм $=135$ мм. Так как отрезок $AB$ разделён точкой $C$ в отношении $AC:AB=7:15$, то $CB$ составляет $\dfrac{15-7}{15}=\dfrac{8}{15}$ длины $AB$. Тогда $CB=\dfrac{8}{15}\cdot135=72$ (мм).`,
|
||||
ref: 'Геометрия, 7 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Туристическая фирма организовала отдых на туристической базе для $4350$ взрослых и детей. Сколько детей отдыхало на туристической базе, если количество детей составило $16\%$ от количества взрослых?`,
|
||||
answer: '600',
|
||||
sol: R`Пусть число взрослых равно $x$, тогда детей $0{,}16x$. Составим уравнение $x+0{,}16x=4350$, $1{,}16x=4350$, $x=3750$. Значит, детей $4350-3750=600$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 16' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 2,
|
||||
text: R`Геометрическая прогрессия $(b_n)$ задана формулой $n$-го члена $b_n=2\cdot3^{\,n+1}$. Найдите сумму пяти первых членов этой прогрессии.`,
|
||||
answer: '2178',
|
||||
sol: R`Первый член $b_1=2\cdot3^{2}=18$, второй $b_2=2\cdot3^{3}=54$, знаменатель $q=\dfrac{b_2}{b_1}=3$. По формуле суммы $S_5=\dfrac{b_1\left(q^{5}-1\right)}{q-1}=\dfrac{18\left(3^{5}-1\right)}{3-1}=\dfrac{18\cdot242}{2}=9\cdot242=2178$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 18' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Пусть $(x_1;y_1)$ и $(x_2;y_2)$ — решения системы уравнений $\begin{cases}x^{2}-y=7,\\y-x=5.\end{cases}$ Найдите значение выражения $x_1\cdot y_2+x_2\cdot y_1$.`,
|
||||
answer: '-19',
|
||||
sol: R`Из второго уравнения $y=5+x$. Подставив в первое: $x^{2}-(5+x)=7$, $x^{2}-x-12=0$, $x=-3$ или $x=4$. Решения системы: $(-3;2)$ и $(4;9)$. Тогда $x_1\cdot y_2+x_2\cdot y_1=(-3)\cdot9+4\cdot2=-27+8=-19$. (Ответ не зависит от порядка решений.)`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 11' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Решите уравнение $\log_4\dfrac{x^{2}+3x-40}{x-5}=1$. В ответ запишите сумму его корней (корень, если он единственный).`,
|
||||
answer: '-4',
|
||||
sol: R`Уравнение равносильно $\dfrac{x^{2}+3x-40}{x-5}=4$ при $x\ne5$. Отсюда $x^{2}+3x-40=4(x-5)$, $x^{2}-x-20=0$, $x=-4$ или $x=5$. Корень $x=5$ не входит в область определения, поэтому единственный корень $x=-4$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 9' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Найдите площадь ромба (в квадратных сантиметрах), если его периметр равен $4$ дм, а одна из диагоналей равна $12$ см.`,
|
||||
answer: '96',
|
||||
sol: R`Периметр $4$ дм $=40$ см, поэтому сторона ромба равна $10$ см. Половина данной диагонали $6$ см; по теореме Пифагора половина второй диагонали $\sqrt{10^{2}-6^{2}}=8$ см, вся вторая диагональ $16$ см. Площадь $S=\dfrac{d_1 d_2}{2}=\dfrac{12\cdot16}{2}=96$ (см$^2$).`,
|
||||
ref: 'Геометрия, 8 класс, гл. 1, § 5; гл. 2, § 15' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Найдите значение выражения $75\cos\alpha$, если $\operatorname{ctg}\alpha=\dfrac{\sqrt6}{12}$ и $\pi<\alpha<\dfrac{3\pi}{2}$.`,
|
||||
answer: '-15',
|
||||
sol: R`$\operatorname{tg}\alpha=\dfrac{1}{\operatorname{ctg}\alpha}=\dfrac{12}{\sqrt6}=2\sqrt6$. Из тождества $1+\operatorname{tg}^{2}\alpha=\dfrac{1}{\cos^{2}\alpha}$ получаем $1+24=\dfrac{1}{\cos^{2}\alpha}$, $\cos^{2}\alpha=\dfrac{1}{25}$. В третьей четверти $\cos\alpha<0$, поэтому $\cos\alpha=-\dfrac15$ и $75\cos\alpha=-15$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 4' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений неравенства $5^{\,x+1}+(0{,}2)^{-x}<750$ на промежутке $(-7;7)$.`,
|
||||
answer: '-18',
|
||||
sol: R`Так как $(0{,}2)^{-x}=5^{x}$, неравенство примет вид $5\cdot5^{x}+5^{x}<750$, $6\cdot5^{x}<750$, $5^{x}<125=5^{3}$, откуда $x<3$. Пересечение $(-\infty;3)$ с $(-7;7)$ — интервал $(-7;3)$. Целые решения $-6,\ldots,2$; их сумма равна $-18$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`В правильной шестиугольной пирамиде высота равна $2\sqrt3$, а радиус окружности, описанной около основания, равен $2\sqrt5$. Найдите объём пирамиды.`,
|
||||
answer: '60',
|
||||
sol: R`Для правильного шестиугольника сторона равна радиусу описанной окружности: $AB=2\sqrt5$. Площадь основания $S_0=\dfrac{3\sqrt3}{2}AB^{2}=\dfrac{3\sqrt3}{2}\cdot20=30\sqrt3$. Объём $V=\dfrac13 S_0 H=\dfrac13\cdot30\sqrt3\cdot2\sqrt3=\dfrac13\cdot30\cdot2\cdot3=60$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 4,
|
||||
text: R`Найдите увеличенное в пять раз произведение точек экстремума функции $f(x)=x^{3}(x-7)^{2}$.`,
|
||||
answer: '147',
|
||||
sol: R`Производная $f'(x)=3x^{2}(x-7)^{2}+x^{3}\cdot2(x-7)=x^{2}(x-7)(5x-21)$. Нули: $x=0$, $x=7$, $x=4{,}2$. Знаки $f'$ показывают, что $x=4{,}2$ — точка максимума, $x=7$ — точка минимума ($x=0$ экстремумом не является). Произведение точек экстремума $4{,}2\cdot7=29{,}4$, увеличенное в пять раз: $5\cdot29{,}4=147$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Даны два натуральных числа, одно из которых на $4$ больше другого. Произведение этих двух чисел больше их суммы на $139$. Найдите наименьшее общее кратное этих чисел.`,
|
||||
answer: '165',
|
||||
sol: R`Пусть меньшее число $x$, тогда большее $x+4$. По условию $x(x+4)=x+(x+4)+139$, $x^{2}+2x-143=0$, $x=11$ (корень $x=-13$ не подходит). Числа $11$ и $15$; так как они взаимно простые, их наименьшее общее кратное равно $11\cdot15=165$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 2, § 11' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите произведение корней уравнения $\sqrt{x-16}-\sqrt{(x-16)(3x+8)}=0$ (корень, если он единственный). В ответ запишите полученный результат, увеличенный в $3$ раза.`,
|
||||
answer: '48',
|
||||
sol: R`Уравнение равносильно $\sqrt{x-16}=\sqrt{(x-16)(3x+8)}$. Возведя в квадрат: $x-16=(x-16)(3x+8)$, $(x-16)(3x+7)=0$, откуда $x=16$ или $x=-\dfrac73$. Проверка: при $x=-\dfrac73$ выражение $\sqrt{x-16}$ не имеет смысла, поэтому единственный корень $x=16$. Увеличенное в $3$ раза: $3\cdot16=48$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 4,
|
||||
text: R`Через вершину конуса и хорду основания, стягивающую дугу в $120^\circ$, проведено сечение. Найдите значение выражения $S^{2}$, где $S$ — площадь этого сечения, если осевым сечением конуса является равносторонний треугольник, площадь которого равна $20\sqrt3$.`,
|
||||
answer: '975',
|
||||
sol: R`Площадь равностороннего осевого сечения $\dfrac{a^{2}\sqrt3}{4}=20\sqrt3$, откуда $a^{2}=80$, $a=4\sqrt5$ — образующая конуса; радиус основания $2\sqrt5$. Хорда $AB$ стягивает дугу $120^\circ$: по теореме косинусов $AB^{2}=\left(2\sqrt5\right)^{2}+\left(2\sqrt5\right)^{2}-2\cdot2\sqrt5\cdot2\sqrt5\cos120^\circ=60$, $AB=2\sqrt{15}$. Сечение — равнобедренный треугольник с боковыми сторонами $4\sqrt5$ и основанием $2\sqrt{15}$; высота к основанию $CK=\sqrt{\left(4\sqrt5\right)^{2}-\left(\sqrt{15}\right)^{2}}=\sqrt{65}$. Площадь $S=\dfrac12\cdot2\sqrt{15}\cdot\sqrt{65}=5\sqrt{39}$, поэтому $S^{2}=25\cdot39=975$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 4' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $\cos x-\sqrt5\sin3x=\cos5x$ на промежутке $[-270^\circ;-135^\circ]$.`,
|
||||
answer: '-420',
|
||||
sol: R`Перенесём: $\cos x-\cos5x-\sqrt5\sin3x=0$. По формуле разности косинусов $\cos x-\cos5x=2\sin3x\sin2x$, поэтому $\sin3x(2\sin2x-\sqrt5)=0$. Уравнение $\sin2x=\dfrac{\sqrt5}{2}>1$ решений не имеет. Из $\sin3x=0$: $3x=180^\circ n$, $x=60^\circ n$. Промежутку $[-270^\circ;-135^\circ]$ принадлежат $-180^\circ$ ($n=-3$) и $-240^\circ$ ($n=-4$); их сумма равна $-420^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 12' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\log_{0{,}7}\left(x^{2}-7x+18\right)-\log_{0{,}7}(x-1)<\log_{0{,}7}2$ на промежутке $(-10;10)$.`,
|
||||
answer: '35',
|
||||
sol: R`Неравенство приводится к виду $\log_{0{,}7}\left(x^{2}-7x+18\right)<\log_{0{,}7}\bigl(2(x-1)\bigr)$. Основание $0{,}7<1$ (функция убывает), поэтому равносильна система $\begin{cases}x^{2}-7x+18>2x-2,\\2x-2>0.\end{cases}$ Первое: $x^{2}-9x+20>0\Rightarrow x<4$ или $x>5$; второе: $x>1$. Решение $(1;4)\cup(5;+\infty)$. Пересечение с $(-10;10)$: $(1;4)\cup(5;10)$; целые $2,3,6,7,8,9$, их сумма $35$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`Сторона $AB$ треугольника $ABC$, у которого $AB=BC=12$, $AC=8$, лежит в плоскости $\alpha$, а длины проекций двух других сторон треугольника $ABC$ на эту плоскость относятся как $1:3$. Найдите значение выражения $\dfrac{13}{\cos^{2}\beta}$, где $\beta$ — угол между плоскостью треугольника $ABC$ и плоскостью $\alpha$.`,
|
||||
answer: '256',
|
||||
sol: R`Пусть $CO$ — перпендикуляр к $\alpha$, $AO:BO=1:3$, $AO=x$, $BO=3x$. Из $CO^{2}=BC^{2}-BO^{2}=AC^{2}-AO^{2}$: $144-9x^{2}=64-x^{2}$, $8x^{2}=80$, $x=\sqrt{10}$, $CO=3\sqrt6$. Высота $CK$ треугольника $ABC$ к $AB$: по формуле Герона $S=32\sqrt2$, откуда $CK=\dfrac{2S}{AB}=\dfrac{64\sqrt2}{12}=\dfrac{16\sqrt2}{3}$. Тогда $OK=\sqrt{CK^{2}-CO^{2}}=\sqrt{\dfrac{512}{9}-54}=\dfrac{\sqrt{26}}{3}$, а $\cos\beta=\dfrac{OK}{CK}=\dfrac{\sqrt{13}}{16}$. Значит, $\dfrac{13}{\cos^{2}\beta}=\dfrac{13\cdot256}{13}=256$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 10' },
|
||||
];
|
||||
|
||||
/* ── Сборка 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}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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_e3v1 (${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), ' | с фигурой:', TASKS.filter(t => t.fig).length, '\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_e3v1.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 · этап III».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,384 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2425_e1v1.js
|
||||
Эталонный «чистый» вариант-пробник для трека exam-prep `ctmath`.
|
||||
|
||||
Источник: РТ–2024/2025, Этап I, Вариант 1 (РИКЗ, «Тематическое
|
||||
консультирование по математике»). 30 заданий: А1–А10 (часть A) +
|
||||
В1–В20 (часть B). Перенабрано вручную в KaTeX по PDF
|
||||
(F:\!Рабочие\ЦТ\Математика\Математика\РТ\2024-2025\МАТ РТ-1 24_25 В1.pdf).
|
||||
|
||||
Чем отличается от уже залитых 723 задач: те сгруппированы по `variant`=ГОД
|
||||
(2024 → 114 задач в одной «пачке») и НЕ образуют чистого 30-задачного
|
||||
варианта, поэтому таймер-пробник на них собирается криво. Здесь
|
||||
variant=101 — ровно 30 заданий (task_idx 1..30) → корректный пробник
|
||||
на 180 мин (mock source='variant').
|
||||
|
||||
Идемпотентность: upsert по UNIQUE(exam_key, variant, task_idx). Повторный
|
||||
запуск обновляет строки, не плодит дубли.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/seed_ctmath_rt2425_e1v1.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/seed_ctmath_rt2425_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 = 101; // чистый 30-задачный вариант (не год)
|
||||
const PROV = 'РТ–2024/2025, Этап I, Вариант 1';
|
||||
const R = String.raw;
|
||||
|
||||
// figure helper — белый фон у самого PNG, поэтому смотрится на любой теме
|
||||
const FIG = (name, alt) =>
|
||||
`<img src="/img/ct/math/rt2425_e1v1/${name}" alt="${alt}" ` +
|
||||
`style="max-width:300px;width:100%;height:auto;display:block;margin:10px auto;` +
|
||||
`background:#fff;border-radius:8px;padding:6px;">`;
|
||||
|
||||
/* opts: метки кириллица а–д (как в существующих 723 строках 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`Результатом округления числа $1{,}3678$ до тысячных является число:`,
|
||||
opts: mc('$0{,}368$', '$1{,}367$', '$1{,}368$', '$1{,}370$', '$1{,}363$'),
|
||||
answer: 'в',
|
||||
sol: R`Округляем до третьего знака после запятой: $1{,}3678\approx 1{,}368$.`,
|
||||
ref: 'Математика, 6 класс, гл. 1, § 2' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 1,
|
||||
text: R`Среди отрезков $FP$, $OA$, $NK$, $MA$, $TE$ укажите отрезок, который является хордой окружности, изображённой на рисунке. (Точка $O$ — центр окружности.)`,
|
||||
opts: mc('$FP$', '$OA$', '$NK$', '$MA$', '$TE$'),
|
||||
answer: 'г',
|
||||
sol: R`Хорда — это отрезок, соединяющий две точки окружности. Обе точки $M$ и $A$ лежат на окружности, поэтому хордой является отрезок $MA$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 1, § 4',
|
||||
fig: FIG('a2.png', 'Окружность с центром O и точками E, K, P, F, N, A, T, M') },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Укажите номер функции, график которой параллелен графику функции $y=4x+1$.`,
|
||||
opts: mc('$y=4x-1$', '$y=-4x+1$', '$y=-4x-1$', '$y=x+4$', '$y=-x-4$'),
|
||||
answer: 'а',
|
||||
sol: R`Графики линейных функций параллельны, если их угловые коэффициенты равны, а свободные члены различны. У $y=4x+1$ коэффициент $k=4$; из предложенных только $y=4x-1$ имеет $k=4$ и $b=-1\ne 1$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Найдите значения данных выражений при $x=-1{,}1$. Укажите номер того выражения, значение которого является наибольшим.`,
|
||||
opts: mc('$2x$', '$1-x$', '$|x|$', '$x+1$', '$x^2$'),
|
||||
answer: 'б',
|
||||
sol: R`Подставим $x=-1{,}1$: $\ 1)\ 2x=-2{,}2;\quad 2)\ 1-x=2{,}1;\quad 3)\ |x|=1{,}1;\quad 4)\ x+1=-0{,}1;\quad 5)\ x^2=1{,}21.$ Наибольшее значение $2{,}1$ — у выражения $1-x$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 4' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 1,
|
||||
text: R`Определите, при каком из значений $x$, равных $6;\ 0;\ 1{,}8;\ 4{,}2;\ -1$, верно двойное неравенство $3\le x+1<7$.`,
|
||||
opts: mc('$6$', '$0$', '$1{,}8$', '$4{,}2$', '$-1$'),
|
||||
answer: 'г',
|
||||
sol: R`Неравенство $3\le x+1<7$ равносильно $2\le x<6$. Из данных чисел этому промежутку принадлежит только $x=4{,}2$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 18' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Укажите номера верных равенств.<br>1) $2^{5/7}:2^{4/7}=2^{1/7}$;<br>2) $2^{1/3}=\dfrac{1}{8}$;<br>3) $2^{1/3}\cdot 2^{2}=2^{2/3}$;<br>4) $\left(2^{1/3}\right)^{2}=2^{1/9}$;<br>5) $2^{1/3}\cdot 5^{1/3}=10^{1/3}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '15', ansShow: '1, 5',
|
||||
sol: R`$1)\ 2^{5/7}:2^{4/7}=2^{5/7-4/7}=2^{1/7}$ — верно. $\ 2)$ неверно. $\ 3)\ 2^{1/3}\cdot 2^{2}=2^{1/3+2}=2^{7/3}\ne 2^{2/3}$ — неверно. $\ 4)\ \left(2^{1/3}\right)^{2}=2^{2/3}\ne 2^{1/9}$ — неверно. $\ 5)\ 2^{1/3}\cdot 5^{1/3}=(2\cdot5)^{1/3}=10^{1/3}$ — верно.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 1, § 1' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`В состав чайного сбора входят мята и липа в отношении $2:3$ соответственно. Сколько граммов липы входит в $975$ г такого сбора?`,
|
||||
opts: mc('$390$ г', '$325$ г', '$875$ г', '$545$ г', '$585$ г'),
|
||||
answer: 'д',
|
||||
sol: R`Пусть на одну часть приходится $k$ г: мята — $2k$, липа — $3k$. Тогда $2k+3k=975$, $5k=975$, $k=195$. Липа: $3k=585$ г.`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Результат упрощения выражения $\sqrt{(x-3)^{2}}$ при $-1{,}6<x<-1$ имеет вид:`,
|
||||
opts: mc('$x-6$', '$-x+3$', '$x+3$', '$-x-3$', '$x-3$'),
|
||||
answer: 'б',
|
||||
sol: R`По свойству $\sqrt{a^{2}}=|a|$ имеем $\sqrt{(x-3)^{2}}=|x-3|$. При $-1{,}6<x<-1$ выражение $x-3<0$, поэтому $|x-3|=-(x-3)=-x+3$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-basics', diff: 2,
|
||||
text: R`Прямая $a$ перпендикулярна плоскости $\alpha$ и пересекает её в точке $A$. Точка $B$ находится на расстоянии $2$ от прямой $a$ и на расстоянии $\sqrt{21}$ от плоскости $\alpha$. Найдите расстояние от точки $B$ до точки $A$.`,
|
||||
opts: mc('$5$', '$\sqrt{17}$', '$2\sqrt{21}$', '$\sqrt{23}$', '$6$'),
|
||||
answer: 'а',
|
||||
sol: R`Опустим перпендикуляры: $BK=2$ — на прямую $a$, $BM=\sqrt{21}$ — на плоскость $\alpha$; тогда $AM=BK=2$. В прямоугольном треугольнике $BMA$ по теореме Пифагора $BA^{2}=BM^{2}+AM^{2}=(\sqrt{21})^{2}+2^{2}=25$, значит $BA=5$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 8' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Укажите номера выражений, которые имеют смысл.<br>1) $-\sqrt[4]{\sqrt{51}-8}$;<br>2) $\sqrt[3]{-8}$;<br>3) $\sqrt[4]{8^{-1}}$;<br>4) $\sqrt[4]{-8}$;<br>5) $-\sqrt[5]{8^{-1}}$.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '235', ansShow: '2, 3, 5',
|
||||
sol: R`$1)$ не имеет смысла: $\sqrt{51}-8<0$, а корень чётной степени из отрицательного числа не существует. $\ 2)\ \sqrt[3]{-8}$ — корень нечётной степени, смысл есть. $\ 3)\ \sqrt[4]{8^{-1}}$ — $8^{-1}=\tfrac18>0$, смысл есть. $\ 4)\ \sqrt[4]{-8}$ — смысла нет. $\ 5)\ -\sqrt[5]{8^{-1}}$ — смысл есть.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 13' },
|
||||
|
||||
// ── Часть B: В1–В20 ──────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'open', topic: 'stereometry', subtopic: 'ster-basics', diff: 3,
|
||||
text: R`Дана правильная четырёхугольная пирамида $SABCD$. Точки $M$ и $N$ — середины боковых рёбер $SA$ и $SC$ соответственно. Выберите верные утверждения.<br>1) прямая $MN$ пересекает прямую $SD$;<br>2) прямая $MN$ пересекает плоскость $SBD$;<br>3) прямая $MN$ лежит в плоскости $SDC$;<br>4) прямая $MN$ параллельна прямой $AB$;<br>5) прямая $MN$ параллельна плоскости $ADC$;<br>6) прямые $MN$ и $CD$ являются скрещивающимися.<br><i>Ответ запишите цифрами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '256', ansShow: '2, 5, 6',
|
||||
sol: R`$1)$ неверно: $MN$ и $SD$ скрещиваются. $\ 2)$ верно: $M$ и $N$ по разные стороны от плоскости $SBD$. $\ 3)$ неверно: $MN$ пересекает $SDC$ в точке $N$. $\ 4)$ неверно: $MN$ и $AB$ скрещиваются. $\ 5)$ верно: $MN$ — средняя линия треугольника $SAC$, значит $MN\parallel AC$, $AC\subset ADC$. $\ 6)$ верно: $MN$ и $CD$ скрещиваются.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1–3' },
|
||||
|
||||
{ idx: 12, type: 'long', topic: 'planimetry', subtopic: 'plan-circle', diff: 3,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–7 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Уравнение окружности с центром в начале координат и радиусом $\sqrt{11}$ имеет вид …<br>Б) Уравнение окружности с центром в начале координат, проходящей через точку $M(-2;5)$, имеет вид …<br>В) Уравнение прямой, проходящей через точки $M(-2;5)$ и $A(2;-5)$, имеет вид …<br><b>Окончание:</b><br>1) $5y-2x=0$; 2) $x^{2}-y^{2}=29$; 3) $x^{2}+y^{2}=29$; 4) $x+y=11$;<br>5) $2y+5x=0$; 6) $x^{2}+y^{2}=11$; 7) $x^{2}-y^{2}=11$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А6Б3В5', ansShow: 'А6Б3В5',
|
||||
sol: R`А) Центр $(0;0)$, радиус $\sqrt{11}$: $x^{2}+y^{2}=11$ — окончание 6. Б) Центр $(0;0)$, проходит через $M(-2;5)$: $R^{2}=(-2)^{2}+5^{2}=29$, то есть $x^{2}+y^{2}=29$ — окончание 3. В) Прямая через $M(-2;5)$ и $A(2;-5)$: подходит $2y+5x=0$ (обе точки ему удовлетворяют) — окончание 5.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 12' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
|
||||
text: R`Фломастеры, которых всего было $445$ штук, упаковывали в коробки по $16$ штук в каждую. Сколько получилось полных коробок, если $13$ фломастеров остались неупакованными?`,
|
||||
answer: '27',
|
||||
sol: R`Пусть $x$ — число полных коробок. По смыслу деления с остатком $445=16x+13$, откуда $16x=432$, $x=27$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 11' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'functions', subtopic: 'fn-graphs', diff: 2,
|
||||
text: R`Найдите значение выражения $4p$, где $p$ — произведение координат вершины параболы, заданной уравнением $y=-2x^{2}-6x+3$.`,
|
||||
answer: '-45',
|
||||
sol: R`Абсцисса вершины $x_0=-\dfrac{b}{2a}=-\dfrac{-6}{2\cdot(-2)}=-\dfrac32$. Ордината $y_0=-2\left(-\dfrac32\right)^{2}-6\left(-\dfrac32\right)+3=-\dfrac92+9+3=\dfrac{15}{2}$. Тогда $p=x_0y_0=-\dfrac32\cdot\dfrac{15}{2}=-\dfrac{45}{4}$ и $4p=-45$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`В треугольнике $ABC$ точки $M$ и $N$ — середины сторон $AB$ и $AC$ соответственно, $\angle ABC=95^\circ$, $\angle ANM=36^\circ$. Найдите градусную меру угла $BAC$.`,
|
||||
answer: '49',
|
||||
sol: R`$MN$ — средняя линия треугольника $ABC$, поэтому $MN\parallel BC$, и $\angle ACB=\angle ANM=36^\circ$ (соответственные углы). По сумме углов треугольника $\angle BAC=180^\circ-95^\circ-36^\circ=49^\circ$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 3, § 17' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Найдите значение выражения $6\sqrt3\,\sin 600^\circ-\sqrt2\,\cos 225^\circ$.`,
|
||||
answer: '-8',
|
||||
sol: R`$\sin600^\circ=\sin240^\circ=\sin(270^\circ-30^\circ)=-\cos30^\circ=-\dfrac{\sqrt3}{2}$; $\cos225^\circ=\cos(180^\circ+45^\circ)=-\cos45^\circ=-\dfrac{\sqrt2}{2}$. Тогда $6\sqrt3\cdot\left(-\dfrac{\sqrt3}{2}\right)-\sqrt2\cdot\left(-\dfrac{\sqrt2}{2}\right)=-9+1=-8$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 2; § 9' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 3,
|
||||
text: R`Найдите произведение наибольшего целого решения на количество всех целых решений неравенства $\left(x+\log_{0{,}5}64\right)^{2}(x-3)(x+13)\le 0$.`,
|
||||
answer: '108',
|
||||
sol: R`Так как $\log_{0{,}5}64=-6$, неравенство принимает вид $(x-6)^{2}(x-3)(x+13)\le 0$. Методом интервалов решение: $[-13;3]\cup\{6\}$. Наибольшее целое решение $6$; всего целых решений $18$ (17 на отрезке $[-13;3]$ и $x=6$). Произведение: $6\cdot18=108$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Найдите сумму всех целых решений системы неравенств $\begin{cases}(x-2)^{2}+23>(x+3)^{2}-2,\\[2pt] 1{,}6x\ge 0{,}9x-6{,}3.\end{cases}$`,
|
||||
answer: '-44',
|
||||
sol: R`Первое неравенство: $x^{2}-4x+4+23>x^{2}+6x+9-2$, то есть $-10x>-20$, $x<2$. Второе: $0{,}7x\ge-6{,}3$, $x\ge-9$. Решение системы — полуинтервал $[-9;2)$. Сумма всех целых из него равна $-44$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Функция $y=f(x)$ нечётна и определена на отрезке $[-8;8]$. Её график для $x\le 0$ изображён на рисунке. Найдите значение выражения $3n$, где $n$ — количество всех целых значений аргумента, при которых функция принимает неположительные значения.`,
|
||||
answer: '30',
|
||||
sol: R`График нечётной функции симметричен относительно начала координат. Функция неположительна ($f(x)\le0$) на промежутках $[-8;-6]$ и $(0;6)$, а также в точках $x=-6,\ 0,\ 6$. Целых значений с $f(x)<0$ — семь, плюс три нуля, итого $n=10$, значит $3n=30$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 8',
|
||||
fig: FIG('b9.png', 'График нечётной функции для x ≤ 0 на отрезке [-8;0]') },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Найдите площадь ромба $ABCD$, если его периметр равен $72$, а величина угла $BAD$ равна $30^\circ$.`,
|
||||
answer: '162',
|
||||
sol: R`Сторона ромба $a=\dfrac{72}{4}=18$. Площадь $S=a^{2}\sin\alpha=18^{2}\cdot\sin30^\circ=324\cdot\dfrac12=162$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 15' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 3,
|
||||
text: R`Найдите значение выражения $25m$, где $m$ — сумма корней уравнения $\left(\dfrac37\right)^{5x^{2}-5x+2}-\left(\dfrac73\right)^{1-3x}=0$.`,
|
||||
answer: '40',
|
||||
sol: R`Так как $\left(\dfrac73\right)^{1-3x}=\left(\dfrac37\right)^{3x-1}$, получаем $5x^{2}-5x+2=3x-1$, то есть $5x^{2}-8x+3=0$. Дискриминант положителен, корни существуют. По теореме Виета сумма корней $m=\dfrac{8}{5}=1{,}6$, поэтому $25m=40$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Поле разбили на два участка $A$ и $B$ одинаковой площади, как показано на рисунке (размеры указаны в метрах). Найдите (в метрах) периметр участка $B$.`,
|
||||
answer: '390',
|
||||
sol: R`Обозначим горизонтальный размер участка $B$ через $x$ м. Из равенства площадей: $140\cdot40+70\cdot(170-x)=70x$, откуда $14x=1750$, $x=125$. Значит, участок $B$ — прямоугольник $70\times125$, его периметр $2(70+125)=390$ м.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 16',
|
||||
fig: FIG('b12.png', 'L-образный участок: A и B, размеры 170, 70, 210, 40 м') },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Осевым сечением цилиндра является квадрат, длина диагонали которого равна $4\sqrt6$. Найдите значение выражения $\dfrac{\sqrt3\,V}{\pi}$, где $V$ — объём цилиндра.`,
|
||||
answer: '144',
|
||||
sol: R`Сторона квадрата $\dfrac{4\sqrt6}{\sqrt2}=4\sqrt3$, значит высота цилиндра и диаметр основания равны $4\sqrt3$, радиус $R=2\sqrt3$. Объём $V=\pi R^{2}h=\pi(2\sqrt3)^{2}\cdot4\sqrt3=48\pi\sqrt3$. Тогда $\dfrac{\sqrt3\,V}{\pi}=48\cdot3=144$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 2' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 3,
|
||||
text: R`Найдите произведение корней (корень, если он единственный) уравнения $\sqrt{x+7}-\sqrt{x^{2}-6x-91}=0$.`,
|
||||
answer: '-98',
|
||||
sol: R`Так как $x^{2}-6x-91=(x+7)(x-13)$, уравнение приводится к $\sqrt{x+7}=\sqrt{(x+7)(x-13)}$. После возведения в квадрат $(x+7)(x-14)=0$. Проверка показывает, что оба числа $-7$ и $14$ — корни. Их произведение $-7\cdot14=-98$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) сумму наименьшего положительного и наибольшего отрицательного корней уравнения $\sin 2x\cos 17x-\cos 2x\sin 17x=\sin\dfrac{3\pi}{2}$.`,
|
||||
answer: '-12',
|
||||
sol: R`Левая часть по формуле синуса разности равна $\sin(2x-17x)=\sin(-15x)$, а $\sin\dfrac{3\pi}{2}=-1$. Значит $\sin(-15x)=-1$, то есть $\sin15x=1$, $15x=90^\circ+360^\circ n$, $x=6^\circ+24^\circ n$. Наименьший положительный корень $6^\circ$, наибольший отрицательный $-18^\circ$; их сумма $-12^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 10' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-quadratic', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений совокупности неравенств $\left[\begin{array}{l}x^{2}-x-6\le0,\\ x^{2}-4x-5>0\end{array}\right.$ на промежутке $[-10;7]$.`,
|
||||
answer: '-36',
|
||||
sol: R`$1)\ x^{2}-x-6\le0$: решение $[-2;3]$. $\ 2)\ x^{2}-4x-5>0$: решение $(-\infty;-1)\cup(5;+\infty)$. Объединение решений совокупности: $(-\infty;3]\cup(5;+\infty)$. Пересечение с $[-10;7]$ даёт $[-10;3]\cup(5;7]$. Сумма целых: $-49+13=-36$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 16' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`В правильной четырёхугольной пирамиде $QABCD$ длина бокового ребра равна $17$, длина диагонали основания $ABCD$ равна $16$. Через середины рёбер $AB$ и $AD$ и точку $Q$ проведена секущая плоскость. Найдите значение выражения $S^{2}$, где $S$ — площадь сечения пирамиды этой плоскостью.`,
|
||||
answer: '3856',
|
||||
sol: R`Пусть $K$, $M$ — середины рёбер $AB$, $AD$; сечение — равнобедренный треугольник $KQM$. $KM$ — средняя линия треугольника $ABD$, $KM=\dfrac12 BD=8$. Высота пирамиды $QO=\sqrt{QA^{2}-OA^{2}}=\sqrt{17^{2}-8^{2}}=15$. Высота $QN$ треугольника $KQM$: $QN=\sqrt{QO^{2}+ON^{2}}=\sqrt{15^{2}+4^{2}}=\sqrt{241}$. Площадь $S=\dfrac12\cdot KM\cdot QN=4\sqrt{241}$, откуда $S^{2}=16\cdot241=3856$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 3' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 4,
|
||||
text: R`При делении некоторого натурального двузначного числа на произведение его цифр неполное частное равно $3$, а остаток равен $10$. Если цифры этого числа поменять местами, то полученное число будет меньше данного на $36$. Найдите исходное число.`,
|
||||
answer: '73',
|
||||
sol: R`Пусть $x$ — цифра десятков, $y$ — цифра единиц; число равно $10x+y$. Условия дают систему $\begin{cases}10x+y=3xy+10,\\ 10x+y=(10y+x)+36.\end{cases}$ Из второго уравнения $x-y=4$. Подставив $x=y+4$, получаем $3y^{2}+y-30=0$, откуда $y=3$, $x=7$. Искомое число — $73$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 11' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 4,
|
||||
text: R`Составьте уравнение касательной к графику функции $f(x)=32x^{3}-24x-5$ в точке с абсциссой $x_0=\dfrac14$. В ответ запишите произведение координат точки пересечения этой касательной с прямой $y=-16x-10$.`,
|
||||
answer: '-84',
|
||||
sol: R`$f'(x)=96x^{2}-24$, $f'\!\left(\dfrac14\right)=-18=k$; $f\!\left(\dfrac14\right)=-10{,}5$. Касательная $y=-18x-6$. Пересечение с $y=-16x-10$: $-18x-6=-16x-10$, $x=2$, $y=-42$. Произведение координат $2\cdot(-42)=-84$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`Из точки $E$ — середины стороны $BC$ равностороннего треугольника $ABC$ — проведён перпендикуляр $EP$ к плоскости треугольника, причём $EP=\dfrac12 BC$. На отрезке $PC$ взята точка $M$ так, что $PM:MC=2:3$. Найдите значение выражения $176\sin^{2}\alpha$, где $\alpha$ — угол между прямой $AM$ и плоскостью $ABC$.`,
|
||||
answer: '18',
|
||||
sol: R`Пусть сторона равна $a$, тогда $EP=\dfrac a2$. Проекция $M$ на плоскость — точка $K$ на $EC$, $\angle MAK=\alpha$. Из подобия $\triangle MKC\sim\triangle PEC$: $MK=\dfrac{3a}{10}$, $CK=\dfrac{3a}{10}$. По теореме косинусов в $\triangle AKC$: $AK^{2}=a^{2}+\left(\dfrac{3a}{10}\right)^{2}-2a\cdot\dfrac{3a}{10}\cos60^\circ=\dfrac{79a^{2}}{100}$. Тогда $AM^{2}=MK^{2}+AK^{2}=\dfrac{88a^{2}}{100}$, $\sin\alpha=\dfrac{MK}{AM}=\dfrac{3}{2\sqrt{22}}$, и $176\sin^{2}\alpha=176\cdot\dfrac{9}{88}=18$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 9' },
|
||||
];
|
||||
|
||||
/* ── Сборка solution_html ────────────────────────────────────────────────── */
|
||||
function ansShowOf(t) {
|
||||
if (t.ansShow != null) return t.ansShow;
|
||||
if (t.type === 'mc') return `${t.answer})`;
|
||||
return `$${t.answer}$`; // числовой/комбинация цифр — в KaTeX
|
||||
}
|
||||
function buildSolution(t) {
|
||||
const ans = ansShowOf(t);
|
||||
let html = `${t.sol}<div class="sol-ans">Ответ: ${ans}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
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`);
|
||||
// self-check автопроверки (long не автопроверяется)
|
||||
if (t.type !== 'long' && !checkAnswerServer(t.answer, t.answer))
|
||||
problems.push(`#${t.idx}: answer "${t.answer}" не проходит self-check (Unicode-минус? пробел?)`);
|
||||
// запрет Unicode-минуса в answer (нужен ASCII '-')
|
||||
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_rt2425_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), '| фигур:', TASKS.filter(t => t.fig).length, '\n');
|
||||
|
||||
console.log('idx | type | subtopic | d | answer | fig');
|
||||
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).padEnd(9)} | ${t.fig ? '✓' : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
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_rt2425_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++;
|
||||
}
|
||||
// variants_count = число «чистых» вариантов-пробников [101;1999]; год-пачки (годы≥2011, 0) скрыты роутом
|
||||
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 → «Варианты» → «Вариант ${VARIANT}».\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат транзакции:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,298 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2425_e2v1.js — РТ–2024/2025, Этап II, Вариант 1 → variant=102
|
||||
Чистый 30-задачный пробник (А1–А10 + В1–В20). Этап II — другой набор тем, чем
|
||||
Этап I (позже по программе: обратные тригфункции, логарифмы, производная,
|
||||
стереометрия). Перенабрано вручную в KaTeX по PDF
|
||||
(…\РТ\2024-2025\МАТ РТ-2 24_25 В1.pdf); чертежи вырезаны из PDF.
|
||||
Правило тиража: 1 вариант на Этап (В1/В2 одного этапа — дубли, берём один).
|
||||
|
||||
Запуск: node backend/scripts/seed_ctmath_rt2425_e2v1.js [--apply]
|
||||
Контракт формата/проверок — см. seed_ctmath_rt2425_e1v1.js.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 102;
|
||||
const PROV = 'РТ–2024/2025, Этап II, Вариант 1';
|
||||
const FIGDIR = 'rt2425_e2v1';
|
||||
const R = String.raw;
|
||||
|
||||
const FIG = (name, alt) =>
|
||||
`<img src="/img/ct/math/${FIGDIR}/${name}" alt="${alt}" ` +
|
||||
`style="max-width:300px;width:100%;height:auto;display:block;margin:10px auto;` +
|
||||
`background:#fff;border-radius:8px;padding:6px;">`;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
const TASKS = [
|
||||
// ── Часть A ──────────────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-divisibility', diff: 1,
|
||||
text: R`Юра и Ян собирали яблоки. Юра собрал яблок в $4$ раза больше, чем Ян. Какую часть всех собранных яблок собрал Ян?`,
|
||||
opts: mc('$\dfrac45$', '$\dfrac15$', '$\dfrac13$', '$\dfrac14$', '$\dfrac34$'),
|
||||
answer: 'б',
|
||||
sol: R`Ян собрал в $4$ раза меньше Юры, поэтому всё количество яблок делится на $4+1=5$ равных частей, и Ян собрал одну из них, то есть $\dfrac15$.`,
|
||||
ref: 'Математика, 5 класс, ч. 2, гл. 3, § 1' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-triangles', diff: 2,
|
||||
text: R`Используя данные рисунка, определите, чему должна быть равна градусная мера угла $1$, чтобы прямые $a$ и $b$ были параллельны.`,
|
||||
opts: mc('$68^\circ$', '$48^\circ$', '$46^\circ$', '$36^\circ$', '$44^\circ$'),
|
||||
answer: 'д',
|
||||
sol: R`Угол $2$, смежный с углом $136^\circ$, равен $44^\circ$. Прямые $a$ и $b$ параллельны, если соответственные углы $1$ и $2$ при секущей $c$ равны, поэтому $\angle 1=44^\circ$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 3, § 15',
|
||||
fig: FIG('a2.png', 'Прямые a и b, секущая c; угол 1 и угол 136°') },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'functions', subtopic: 'fn-graphs', diff: 1,
|
||||
text: R`Укажите номер рисунка, на котором изображён график функции $y=|x|$.`,
|
||||
opts: mc('$1$', '$2$', '$3$', '$4$', '$5$'),
|
||||
answer: 'в',
|
||||
sol: R`График функции $y=|x|$ — это «уголок» с вершиной в начале координат (ветви $y=x$ при $x\ge0$ и $y=-x$ при $x<0$). Ему соответствует рисунок $3$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 4, § 19',
|
||||
fig: FIG('a3.png', 'Пять графиков-кандидатов 1–5; график 3 — «уголок» y=|x|') },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 1,
|
||||
text: R`Среди значений переменной $x$, равных $16;\ -1;\ 1;\ -4;\ -15$, укажите то, при котором значение выражения $0{,}36-x^2$ равно $-15{,}64$.`,
|
||||
opts: mc('$16$', '$-1$', '$1$', '$-4$', '$-15$'),
|
||||
answer: 'г',
|
||||
sol: R`Проверяем: при $x=-4$ имеем $0{,}36-(-4)^2=0{,}36-16=-15{,}64$. Остальные значения дают другой результат.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 4' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Укажите номер, под которым приведено множество всех решений системы неравенств $\begin{cases}x\le 6,\\ x<-4.\end{cases}$`,
|
||||
opts: mc('$(-\infty;-4)$', '$(-\infty;6]$', '$(-4;6]$', '$(-\infty;6)$', '$(-\infty;-4)\cup(-4;6]$'),
|
||||
answer: 'а',
|
||||
sol: R`Решение первого неравенства — луч $(-\infty;6]$, второго — открытый луч $(-\infty;-4)$. Пересечением является $(-\infty;-4)$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'numbers', subtopic: 'num-real', diff: 2,
|
||||
text: R`Среди выражений $\log_{\sqrt2}4$; $\ -5^2$; $\ \cos\dfrac{5\pi}{6}$; $\ 7^{-1}$; $\ \sqrt[5]{(-2)^5}$ укажите те, значение которых является отрицательным числом.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '235', ansShow: '2, 3, 5',
|
||||
sol: R`$1)\ \log_{\sqrt2}4=4>0$. $\ 2)\ -5^2=-25<0$. $\ 3)\ \cos\dfrac{5\pi}{6}=-\dfrac{\sqrt3}{2}<0$. $\ 4)\ 7^{-1}=\dfrac17>0$. $\ 5)\ \sqrt[5]{(-2)^5}=-2<0$. Отрицательны выражения 2, 3, 5.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 1, § 3' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Результат разложения многочлена $(a-b)+2c(b-a)$ на множители имеет вид:`,
|
||||
opts: mc('$(a-b)(1+2c)$', '$(a-b)(2c-1)$', '$(a-b)(1-2c)$', '$-2c(a-b)$', '$2c(a-b)$'),
|
||||
answer: 'в',
|
||||
sol: R`$(a-b)+2c(b-a)=(a-b)-2c(a-b)=(a-b)(1-2c)$.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'equations', subtopic: 'eq-linear', diff: 2,
|
||||
text: R`Укажите номер неравенства, которое равносильно неравенству $x>5$.`,
|
||||
opts: mc('$x^2>5x$', '$\dfrac{1}{x-5}<0$', '$(x-5)^2>0$', '$-2x<-10$', '$(0{,}5)^{x-5}>0$'),
|
||||
answer: 'г',
|
||||
sol: R`Решение $x>5$ — луч $(5;+\infty)$. Неравенство $-2x<-10$ равносильно $x>5$ — то же множество решений. (Остальные дают другие множества.)`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 13' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 2,
|
||||
text: R`У правильной четырёхугольной призмы площадь основания равна $28$ см$^2$. Какой должна быть высота (в сантиметрах) этой призмы, чтобы её объём был равен $98$ см$^3$?`,
|
||||
opts: mc('$2$', '$4$', '$3{,}2$', '$4{,}5$', '$3{,}5$'),
|
||||
answer: 'д',
|
||||
sol: R`Объём призмы $V=S_{\text{осн}}\cdot h$. Тогда $98=28h$, откуда $h=3{,}5$ см.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 1' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`На рисунке изображён график функции $y=f(x)$, определённой на промежутке $[-6;6]$. Укажите номера верных утверждений.<br>1) множеством значений функции является отрезок $[-3;4]$;<br>2) функция является нечётной;<br>3) график функции $y=f(x-1)$ проходит через точку $(0;2)$;<br>4) функция убывает на промежутках $[-1;0]$ и $[1;6]$;<br>5) $f(-5)+f(2)<0$.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '14', ansShow: '1, 4',
|
||||
sol: R`$1)$ верно: $E(f)=[-3;4]$. $\ 2)$ неверно: график симметричен относительно оси ординат, функция чётная. $\ 3)$ неверно: точка $(0;2)$ не принадлежит графику $y=f(x-1)$. $\ 4)$ верно: на $[-1;0]$ и $[1;6]$ значения убывают. $\ 5)$ неверно: $-2<f(-5)<-1$ и $2<f(2)<3$, поэтому $f(-5)+f(2)>0$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 6–9',
|
||||
fig: FIG('a10.png', 'График чётной функции y=f(x) на [-6;6], с пиками y=4 и краями y=-3') },
|
||||
|
||||
// ── Часть B ──────────────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'long', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Значение выражения $\arcsin 0-|-5|$ равно …<br>Б) Значение выражения $\dfrac1\pi\arccos\left(-\dfrac{\sqrt3}{2}\right)-\dfrac13$ равно …<br>В) Значение выражения $4\sqrt6\,\sin\left(2\arccos\dfrac{\sqrt2}{2}-\dfrac\pi4\right)$ равно …<br><b>Окончание:</b><br>1) $6\sqrt2$; 2) $-5$; 3) $\dfrac13$; 4) $-4$; 5) $4\sqrt3$; 6) $\dfrac12$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А2Б6В5', ansShow: 'А2Б6В5',
|
||||
sol: R`А) $\arcsin0-|-5|=0-5=-5$ — окончание 2. Б) $\dfrac1\pi\cdot\dfrac{5\pi}{6}-\dfrac13=\dfrac56-\dfrac13=\dfrac12$ — окончание 6. В) $4\sqrt6\,\sin\left(2\cdot\dfrac\pi4-\dfrac\pi4\right)=4\sqrt6\sin\dfrac\pi4=4\sqrt6\cdot\dfrac{\sqrt2}{2}=4\sqrt3$ — окончание 5.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 7' },
|
||||
|
||||
{ idx: 12, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 3,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — куб. Длина пространственной ломаной $ABB_1C_1C$ равна $16\sqrt3$. Выберите верные утверждения.<br>1) длина диагонали грани $ABCD$ равна $4\sqrt3$;<br>2) площадь полной поверхности куба равна $192$;<br>3) длина диагонали куба равна $4\sqrt6$;<br>4) площадь треугольника $AC_1C$ равна $24\sqrt2$;<br>5) длина ребра куба равна $4\sqrt3$;<br>6) объём куба равен $192\sqrt3$.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '456', ansShow: '4, 5, 6',
|
||||
sol: R`Ломаная $ABB_1C_1C$ состоит из четырёх рёбер: $16\sqrt3:4=4\sqrt3$ — ребро. $\ 1)$ диагональ грани $=4\sqrt3\cdot\sqrt2=4\sqrt6$ — неверно. $\ 2)\ S=6a^2=6\cdot48=288$ — неверно. $\ 3)$ диагональ куба $=a\sqrt3=4\sqrt3\cdot\sqrt3=12$ — неверно. $\ 4)\ S_{AC_1C}=\tfrac12\cdot4\sqrt6\cdot4\sqrt3=24\sqrt2$ — верно. $\ 5)$ ребро $=4\sqrt3$ — верно. $\ 6)\ V=a^3=(4\sqrt3)^3=192\sqrt3$ — верно.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 1, § 1' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Найдите значение выражения $15\sqrt{10}\,\operatorname{tg}\alpha$, если $\operatorname{ctg}\alpha=-\dfrac{\sqrt{10}}{8}$.`,
|
||||
answer: '-120',
|
||||
sol: R`Из тождества $\operatorname{tg}\alpha\cdot\operatorname{ctg}\alpha=1$: $\operatorname{tg}\alpha=\dfrac1{\operatorname{ctg}\alpha}=-\dfrac{8}{\sqrt{10}}=-\dfrac{4\sqrt{10}}{5}$. Тогда $15\sqrt{10}\cdot\left(-\dfrac{4\sqrt{10}}{5}\right)=15\cdot\left(-\dfrac{4\cdot10}{5}\right)=-120$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 4' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`Диагонали ромба равны $4$ и $10$. Найдите значение выражения $\sqrt{29}\cdot P$, где $P$ — периметр ромба.`,
|
||||
answer: '116',
|
||||
sol: R`Диагонали ромба перпендикулярны и делятся точкой пересечения пополам. Сторона $a=\sqrt{2^2+5^2}=\sqrt{29}$. Периметр $P=4\sqrt{29}$, тогда $\sqrt{29}\cdot P=\sqrt{29}\cdot4\sqrt{29}=4\cdot29=116$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 1, § 5' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 3,
|
||||
text: R`Пусть $A$ — наименьшее натуральное число, большее $50$, при делении которого на $9$ и на $12$ получается остаток $1$. Найдите остаток при делении числа $A$ на $13$. В ответ запишите сумму числа $A$ и полученного остатка.`,
|
||||
answer: '81',
|
||||
sol: R`$A-1$ делится и на $9$, и на $12$, то есть кратно $\text{НОК}(9;12)=36$. Наименьшее такое $A-1>49$ равно $72$, значит $A=73$. Остаток от деления $73$ на $13$ равен $8$. Сумма $73+8=81$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 11–13' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 3,
|
||||
text: R`Найдите, при каком значении переменной $x$ значения выражений $x-18$; $\ x-3$; $\ x+17$ будут последовательными членами геометрической прогрессии.`,
|
||||
answer: '63',
|
||||
sol: R`По характеристическому свойству геометрической прогрессии $(x-3)^2=(x-18)(x+17)$. Раскрывая: $x^2-6x+9=x^2-x-306$, $-5x=-315$, $x=63$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 17' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Пусть $(x_1;y_1)$ и $(x_2;y_2)$ — решения системы уравнений $\begin{cases}x^2+3y=27,\\ x-y=-9.\end{cases}$ Найдите значение выражения $x_1x_2-y_1y_2$.`,
|
||||
answer: '-54',
|
||||
sol: R`Из второго уравнения $y=x+9$. Тогда $x^2+3(x+9)=27$, $x^2+3x=0$, $x=0$ или $x=-3$. Решения: $(-3;6)$ и $(0;9)$. Значение $x_1x_2-y_1y_2=(-3)\cdot0-6\cdot9=-54$ (не зависит от выбора нумерации пар).`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 11' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Аппликация состоит из двух подобных треугольников $\mathrm{I}$ и $\mathrm{II}$. Площадь треугольника $\mathrm{I}$ равна $75$ см$^2$, а длины сторон треугольника $\mathrm{II}$ на $20\%$ больше длин соответствующих сторон треугольника $\mathrm{I}$. Найдите (в см$^2$) площадь всей аппликации.`,
|
||||
answer: '183',
|
||||
sol: R`Коэффициент подобия (II к I) равен $1{,}2=\dfrac65$. Отношение площадей подобных треугольников равно квадрату коэффициента: $S_{\mathrm{II}}=75\cdot\left(\dfrac65\right)^2=75\cdot\dfrac{36}{25}=108$ см$^2$. Площадь всей аппликации $75+108=183$ см$^2$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 3, § 23' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 3,
|
||||
text: R`Найдите значение выражения $2\log_{25}\left(\dfrac{a}{125}\right)-\log_5\dfrac{25}{b}$, если $\log_{25}(ab)=19$.`,
|
||||
answer: '33',
|
||||
sol: R`$2\log_{25}\left(\dfrac{a}{125}\right)=\log_5\dfrac{a}{125}$. Тогда выражение равно $\log_5\dfrac{a}{125}-\log_5\dfrac{25}{b}=\log_5\dfrac{ab}{5^5}=\log_5(ab)-5=2\log_{25}(ab)-5=2\cdot19-5=33$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 7' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-triangles', diff: 4,
|
||||
text: R`В равнобедренном треугольнике $KMN$ проведена высота $MH$ к основанию $KN$. Точка $P$ — середина боковой стороны $MN$. Известно, что длина высоты $MH$ равна длине отрезка $HP$ и $KN=6\sqrt6$. Найдите значение выражения $S^2$, где $S$ — площадь треугольника $KMN$.`,
|
||||
answer: '972',
|
||||
sol: R`Высота $MH$ равнобедренного треугольника является и медианой, поэтому $HN=\tfrac12 KN=3\sqrt6$. В прямоугольном треугольнике $MHN$ отрезок $HP$ — медиана к гипотенузе $MN$, значит $HP=\tfrac12 MN$; по условию $MH=HP=\tfrac12 MN$, то есть катет $MH$ равен половине гипотенузы, и $\angle MNH=30^\circ$. Тогда $MH=HN\operatorname{tg}30^\circ=3\sqrt6\cdot\dfrac{\sqrt3}{3}=3\sqrt2$. Площадь $S=\tfrac12\cdot KN\cdot MH=\tfrac12\cdot6\sqrt6\cdot3\sqrt2=18\sqrt3$, откуда $S^2=972$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 15–16' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Найдите количество всех целых чисел из множества значений функции $y=\left(\dfrac13\right)^{-x}$ на отрезке $[3;4]$.`,
|
||||
answer: '55',
|
||||
sol: R`$y=\left(\dfrac13\right)^{-x}=3^x$ — возрастающая функция. При $3\le x\le4$ имеем $3^3\le 3^x\le 3^4$, то есть $E=[27;81]$. Целых чисел на отрезке $[27;81]$ — $81-27+1=55$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 4' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Первый турист ехал от базы со скоростью $40$ км/ч и успел на станцию за $3$ мин до отправления поезда. Второй турист, выехавший одновременно с первым от той же базы со скоростью $35$ км/ч, опоздал на этот же поезд на $3$ мин. На каком расстоянии (в километрах) от базы находится станция?`,
|
||||
answer: '28',
|
||||
sol: R`Пусть расстояние равно $x$ км. Разница во времени между туристами составляет $6$ мин $=\dfrac1{10}$ ч: $\dfrac{x}{35}-\dfrac{x}{40}=\dfrac1{10}$, $\dfrac{x}{280}=\dfrac1{10}$, $x=28$ км.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 16; гл. 4, § 25' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите произведение наименьшего целого решения на количество всех целых решений неравенства $\log_{0{,}4}\left(\dfrac{x^2}{4}-3\right)\ge 0$.`,
|
||||
answer: '-8',
|
||||
sol: R`$0=\log_{0{,}4}1$, и так как $0<0{,}4<1$, неравенство равносильно системе $\dfrac{x^2}{4}-3\le1$ и $\dfrac{x^2}{4}-3>0$, то есть $x^2\le16$ и $x^2>12$. Решение: $[-4;-2\sqrt3)\cup(2\sqrt3;4]$. Целых решений два ($-4$ и $4$), наименьшее $-4$. Произведение $-4\cdot2=-8$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 4,
|
||||
text: R`Найдите (в градусах) наименьший положительный корень уравнения $4\sin\dfrac{x}{7}\cos\dfrac{x}{7}=\sqrt3$.`,
|
||||
answer: '210',
|
||||
sol: R`По формуле синуса двойного аргумента $2\sin\dfrac{2x}{7}=\sqrt3$, $\sin\dfrac{2x}{7}=\dfrac{\sqrt3}{2}$. Тогда $\dfrac{2x}{7}=(-1)^k60^\circ+180^\circ k$, $x=(-1)^k210^\circ+630^\circ k$. Наименьший положительный корень — $210^\circ$ (при $k=0$).`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 11' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 4,
|
||||
text: R`В правильной треугольной пирамиде ребро основания равно $2\sqrt2$, а угол между боковым ребром и плоскостью основания равен $30^\circ$. Найдите значение выражения $9\sqrt6\cdot V$, где $V$ — объём этой пирамиды.`,
|
||||
answer: '24',
|
||||
sol: R`Высота $SO$, $\angle SAO=30^\circ$. $AO=\tfrac23 AM$, где медиана $AM=\sqrt6$, значит $AO=\dfrac{2\sqrt6}{3}$. Тогда $SO=AO\operatorname{tg}30^\circ=\dfrac{2\sqrt6}{3}\cdot\dfrac{\sqrt3}{3}=\dfrac{2\sqrt2}{3}$. Объём $V=\dfrac13\cdot\dfrac{(2\sqrt2)^2\sqrt3}{4}\cdot\dfrac{2\sqrt2}{3}=\dfrac{4\sqrt6}{9}$. Тогда $9\sqrt6\cdot V=9\sqrt6\cdot\dfrac{4\sqrt6}{9}=4\cdot6=24$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 4,
|
||||
text: R`Найдите произведение корней (корень, если он единственный) уравнения $\sqrt[4]{x^2+6x-27}\cdot\sqrt[3]{x^2-6x-27}=0$.`,
|
||||
answer: '-243',
|
||||
sol: R`Произведение равно нулю, когда один из множителей равен нулю, а другой имеет смысл. $x^2+6x-27=0\Rightarrow x=-9,\ 3$ (оба удовлетворяют ОДЗ). $x^2-6x-27=0\Rightarrow x=-3,\ 9$, но при $x=-3$ подкоренное выражение $\sqrt[4]{\;}$ отрицательно — не подходит, остаётся $x=9$. Корни уравнения: $-9,\ 3,\ 9$; произведение $-9\cdot3\cdot9=-243$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 5,
|
||||
text: R`$ABCA_1B_1C_1$ — прямая треугольная призма, все рёбра которой равны. Точки $K$ и $M$ — середины рёбер $A_1C_1$ и $B_1C_1$ соответственно. Точка $N$ лежит на ребре $AB$ так, что $AN:NB=1:5$. Найдите значение выражения $\dfrac{1}{\cos^2\varphi}$, где $\varphi$ — угол между прямыми $A_1N$ и $KM$.`,
|
||||
answer: '37',
|
||||
sol: R`Пусть ребро равно $a$, $AN=\dfrac a6$. Так как $KM\parallel AB$ (средняя линия), угол между $A_1N$ и $KM$ равен углу $\angle NA_1B_1$. В прямоугольном треугольнике $A_1AN$: $A_1N=\sqrt{a^2+\left(\dfrac a6\right)^2}=\dfrac{a\sqrt{37}}{6}$, $\sin\angle AA_1N=\dfrac{a/6}{A_1N}=\dfrac{\sqrt{37}}{37}$. Тогда $\cos\varphi=\sin\angle AA_1N=\dfrac{\sqrt{37}}{37}$, и $\dfrac{1}{\cos^2\varphi}=37$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 2, § 4' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите произведение наибольшего целого отрицательного и наименьшего целого положительного решений неравенства $5\cdot25^{\frac{5-x}{23}}-26\cdot25^{\frac{5-x}{46}}+5\ge 0$.`,
|
||||
answer: '-504',
|
||||
sol: R`Замена $t=25^{\frac{5-x}{46}}$ даёт $5t^2-26t+5\ge0$, откуда $t\le\dfrac15$ или $t\ge5$. Тогда $\dfrac{5-x}{23}\le-1$ или $\dfrac{5-x}{23}\ge1$, то есть $x\ge28$ или $x\le-18$. Решение: $(-\infty;-18]\cup[28;+\infty)$. Наибольшее целое отрицательное — $-18$, наименьшее целое положительное — $28$; произведение $-18\cdot28=-504$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 4,
|
||||
text: R`Найдите точку максимума и максимум функции $f(x)=x^3-75x-24\sin\dfrac{7\pi}{6}$. В ответ запишите их сумму.`,
|
||||
answer: '257',
|
||||
sol: R`$24\sin\dfrac{7\pi}{6}=24\cdot\left(-\dfrac12\right)=-12$, поэтому $f(x)=x^3-75x+12$. $f'(x)=3x^2-75=0$ при $x=\pm5$. Точка максимума $x_{\max}=-5$, $f(-5)=-125+375+12=262$. Сумма $-5+262=257$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
|
||||
text: R`Плоскость, параллельная основанию конуса, делит его высоту в отношении $2:5$, считая от вершины. Площадь сечения конуса меньше площади основания на $270\pi$. Образующая конуса составляет с плоскостью основания угол $\operatorname{arctg}\dfrac57$. Найдите значение выражения $\dfrac{\sqrt6\,V}{\pi}$, где $V$ — объём конуса.`,
|
||||
answer: '2940',
|
||||
sol: R`Сечением является круг; по свойству площади относятся как квадраты расстояний от вершины: $\dfrac{S_{\text{осн}}-270\pi}{S_{\text{осн}}}=\left(\dfrac27\right)^2=\dfrac{4}{49}$, откуда $45 S_{\text{осн}}=49\cdot270\pi$, $S_{\text{осн}}=294\pi$. Тогда $R^2=294$, $R=7\sqrt6$. Высота $SO=R\operatorname{tg}\left(\operatorname{arctg}\dfrac57\right)=7\sqrt6\cdot\dfrac57=5\sqrt6$. Объём $V=\dfrac13 S_{\text{осн}}\cdot SO=\dfrac13\cdot294\pi\cdot5\sqrt6=490\pi\sqrt6$. Тогда $\dfrac{\sqrt6\,V}{\pi}=490\cdot6=2940$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 4' },
|
||||
];
|
||||
|
||||
/* ── машинерия (как в e1v1) ────────────────────────────────────────────────── */
|
||||
function ansShowOf(t) { if (t.ansShow != null) return t.ansShow; if (t.type === 'mc') return `${t.answer})`; return `$${t.answer}$`; }
|
||||
function buildSolution(t) {
|
||||
let html = `${t.sol}<div class="sol-ans">Ответ: ${ansShowOf(t)}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
return html;
|
||||
}
|
||||
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(u, c0) {
|
||||
if (u == null || c0 == null) return false;
|
||||
const c = String(c0).trim();
|
||||
if (/^[а-д]$/.test(c)) return String(u).trim().toLowerCase() === c.toLowerCase();
|
||||
if (/^[^;]+;[^;]+$/.test(c)) return false;
|
||||
const cn = srvToNumber(c), un = srvToNumber(u);
|
||||
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(`Дубль idx=${t.idx}`); seen.add(t.idx);
|
||||
if (t.idx < 1 || t.idx > 30) problems.push(`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}: self-check "${t.answer}"`);
|
||||
if (/−/.test(String(t.answer))) problems.push(`#${t.idx}: Unicode-минус в answer`);
|
||||
}
|
||||
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);
|
||||
if (!db.prepare(`SELECT exam_key FROM exam_tracks WHERE exam_key=?`).get(EXAM)) { console.error(`✗ Трек '${EXAM}' не найден.`); process.exit(1); }
|
||||
console.log(`\n=== seed_ctmath_rt2425_e2v1 (${PROV}) variant=${VARIANT} ===`);
|
||||
console.log(`Режим: ${APPLY ? 'APPLY' : 'DRY-RUN'}\n`);
|
||||
console.log('Типы:', JSON.stringify(TASKS.reduce((a, t) => (a[t.type] = (a[t.type] || 0) + 1, a), {})), '| фигур:', TASKS.filter(t => t.fig).length, '\n');
|
||||
console.log('idx | type | subtopic | d | answer | fig');
|
||||
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).padEnd(9)} | ${t.fig ? '✓' : ''}`);
|
||||
if (problems.length) { console.error(`\n✗ ПРОБЛЕМЫ (${problems.length}):`); problems.forEach(p => console.error(' - ' + p)); db.close(); process.exit(1); }
|
||||
console.log('\n✓ Валидация и self-check ответов пройдены (30/30).');
|
||||
if (!APPLY) { console.log('\nDRY-RUN: ничего не записано. Для записи добавьте --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}). variants_count=${distinct}.`);
|
||||
console.log(`\nПробник: /exam-prep/ctmath → «Варианты» → «Вариант ${VARIANT}».\n`);
|
||||
} catch (e) { db.exec('ROLLBACK'); console.error('\n✗ Ошибка записи, откат:', e.message); process.exitCode = 1; }
|
||||
db.close();
|
||||
@@ -0,0 +1,294 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
seed_ctmath_rt2425_e3v1.js — РТ–2024/2025, Этап III, Вариант 1 → variant=103
|
||||
Чистый 30-задачный пробник (А1–А10 + В1–В20). Этап III — завершающий, полный
|
||||
охват программы (стереометрия тел вращения, сфера, производная, сечения).
|
||||
Перенабрано вручную в KaTeX по PDF (…\РТ\2024-2025\МАТ РТ-3 24_25 В1.pdf).
|
||||
Правило тиража: 1 вариант на Этап. Только А2 содержит данные на чертеже.
|
||||
|
||||
Запуск: node backend/scripts/seed_ctmath_rt2425_e3v1.js [--apply]
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const EXAM = 'ctmath';
|
||||
const VARIANT = 103;
|
||||
const PROV = 'РТ–2024/2025, Этап III, Вариант 1';
|
||||
const FIGDIR = 'rt2425_e3v1';
|
||||
const R = String.raw;
|
||||
|
||||
const FIG = (name, alt) =>
|
||||
`<img src="/img/ct/math/${FIGDIR}/${name}" alt="${alt}" ` +
|
||||
`style="max-width:300px;width:100%;height:auto;display:block;margin:10px auto;` +
|
||||
`background:#fff;border-radius:8px;padding:6px;">`;
|
||||
|
||||
const L = ['а', 'б', 'в', 'г', 'д'];
|
||||
const mc = (...html) => html.map((h, i) => [L[i], h]);
|
||||
|
||||
const TASKS = [
|
||||
// ── Часть A ──────────────────────────────────────────────────────────────
|
||||
{ idx: 1, type: 'mc', topic: 'numbers', subtopic: 'num-real', diff: 1,
|
||||
text: R`Среди чисел $-0{,}5;\ 2^{-1};\ -0{,}2;\ -\sqrt2;\ 2$ укажите число, противоположное числу $\dfrac12$.`,
|
||||
opts: mc('$-0{,}5$', '$2^{-1}$', '$-0{,}2$', '$-\sqrt2$', '$2$'),
|
||||
answer: 'а',
|
||||
sol: R`Противоположные числа имеют равные модули, но разные знаки. Числу $\dfrac12$ противоположно число $-\dfrac12=-0{,}5$.`,
|
||||
ref: 'Математика, 6 класс, гл. 4, § 2' },
|
||||
|
||||
{ idx: 2, type: 'mc', topic: 'planimetry', subtopic: 'plan-circle', diff: 2,
|
||||
text: R`На рисунке изображены три окружности с центрами $O$, $A$, $B$, радиусы которых равны $R$, $\dfrac R4$, $\dfrac R3$ соответственно. Найдите длину отрезка $AB$, если $R=12$.`,
|
||||
opts: mc('$13$', '$18$', '$15$', '$17$', '$19$'),
|
||||
answer: 'г',
|
||||
sol: R`Отрезок $AB$ лежит на диаметре большой окружности (радиус $R=12$, диаметр $24$). Меньшие окружности касаются большой изнутри, их радиусы $\dfrac R4=3$ и $\dfrac R3=4$. Тогда $AB=24-3-4=17$.`,
|
||||
ref: 'Геометрия, 7 класс, гл. 1, § 4',
|
||||
fig: FIG('a2.png', 'Большая окружность с центром O и две внутренние окружности A и B на диаметре') },
|
||||
|
||||
{ idx: 3, type: 'mc', topic: 'functions', subtopic: 'fn-properties', diff: 2,
|
||||
text: R`Укажите номер множества чисел, которое может являться областью определения нечётной функции.`,
|
||||
opts: mc('$[-7;7]$', '$(-6;0)\cup(0;6]$', '$[-5;10]$', '$[-9;2)\cup(2;9]$', '$(-11;0)\cup(0;11)$'),
|
||||
answer: 'д',
|
||||
sol: R`Область определения нечётной функции симметрична относительно нуля. Из предложенных множеств этим свойством обладает $(-11;0)\cup(0;11)$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 2, § 8' },
|
||||
|
||||
{ idx: 4, type: 'mc', topic: 'equations', subtopic: 'eq-exponential', diff: 2,
|
||||
text: R`Укажите номер показательного уравнения, корнем которого является число $-2$.`,
|
||||
opts: mc('$(0{,}3)^{x-6}=(0{,}3)^{6x+4}$', '$2^{2x}=64$', '$(0{,}5)^{x^2+4}=1$', '$16x+35=3$', '$7^x=11$'),
|
||||
answer: 'а',
|
||||
sol: R`Подставим $x=-2$: $(0{,}3)^{-2-6}=(0{,}3)^{6\cdot(-2)+4}$, то есть $(0{,}3)^{-8}=(0{,}3)^{-8}$ — верно. Остальные показательные уравнения числу $-2$ не удовлетворяют (а уравнение 4 не является показательным).`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 5' },
|
||||
|
||||
{ idx: 5, type: 'mc', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 2,
|
||||
text: R`Найдите значение выражения $\sqrt[7]{(-49)^7}-|5{,}25-6|$.`,
|
||||
opts: mc('$-48{,}25$', '$-49{,}75$', '$-49{,}25$', '$-48{,}75$', '$-50$'),
|
||||
answer: 'б',
|
||||
sol: R`$\sqrt[7]{(-49)^7}=-49$ (корень нечётной степени), $|5{,}25-6|=0{,}75$. Тогда $-49-0{,}75=-49{,}75$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 6, type: 'open', topic: 'expressions', subtopic: 'expr-polynomials', diff: 2,
|
||||
text: R`Укажите номера пар, состоящих из подобных одночленов.<br>1) $2ab^2$ и $-2a^2b$;<br>2) $\dfrac13 m$ и $-m^3$;<br>3) $5xy$ и $-0{,}2xy$;<br>4) $-16$ и $-16n$;<br>5) $-1{,}2c^8$ и $-8c^8$.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '35', ansShow: '3, 5',
|
||||
sol: R`Подобные одночлены отличаются только числовым коэффициентом (одинаковая буквенная часть). $\ 3)\ 5xy$ и $-0{,}2xy$ — подобны. $\ 5)\ -1{,}2c^8$ и $-8c^8$ — подобны. Остальные пары различаются буквенной частью.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 2, § 6–7' },
|
||||
|
||||
{ idx: 7, type: 'mc', topic: 'word-sequences', subtopic: 'word-problems', diff: 2,
|
||||
text: R`Юра, редактируя изображение шириной $27$ см и высотой $36$ см, уменьшил ширину на $6$ см так, что отношение ширины к высоте полученного изображения не изменилось. Найдите высоту полученного изображения (в см).`,
|
||||
opts: mc('$31$', '$27$', '$28$', '$30$', '$26$'),
|
||||
answer: 'в',
|
||||
sol: R`Новая ширина $27-6=21$ см. Отношение сохранилось: $\dfrac{27}{36}=\dfrac{21}{x}$, откуда $x=\dfrac{36\cdot21}{27}=28$ см.`,
|
||||
ref: 'Математика, 6 класс, гл. 2, § 3' },
|
||||
|
||||
{ idx: 8, type: 'mc', topic: 'trigonometry', subtopic: 'trig-identities', diff: 2,
|
||||
text: R`Найдите значение выражения $\operatorname{arcctg}(-\sqrt3)+\dfrac\pi2$.`,
|
||||
opts: mc('$\dfrac\pi3$', '$\dfrac{7\pi}{6}$', '$\dfrac\pi6$', '$\dfrac{4\pi}{3}$', '$\dfrac{3\pi}{2}$'),
|
||||
answer: 'г',
|
||||
sol: R`$\operatorname{arcctg}(-\sqrt3)=\dfrac{5\pi}{6}$ (так как $\dfrac{5\pi}{6}\in(0;\pi)$ и $\operatorname{ctg}\dfrac{5\pi}{6}=-\sqrt3$). Тогда $\dfrac{5\pi}{6}+\dfrac\pi2=\dfrac{5\pi}{6}+\dfrac{3\pi}{6}=\dfrac{8\pi}{6}=\dfrac{4\pi}{3}$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 7' },
|
||||
|
||||
{ idx: 9, type: 'mc', topic: 'stereometry', subtopic: 'ster-angles-distances', diff: 3,
|
||||
text: R`Из точки $A$, отстоящей на $\sqrt3$ от плоскости $\alpha$, проведена наклонная $AB$. Проекция наклонной $AB$ на плоскость $\alpha$ равна $\sqrt{13}$. Найдите косинус угла между наклонной $AB$ и плоскостью $\alpha$.`,
|
||||
opts: mc('$\dfrac{\sqrt3}{4}$', '$\dfrac{\sqrt{13}}{4}$', '$\dfrac{\sqrt{39}}{13}$', '$\dfrac14$', '$\dfrac{\sqrt3}{2}$'),
|
||||
answer: 'б',
|
||||
sol: R`Пусть $O$ — основание перпендикуляра: $AO=\sqrt3$, проекция $OB=\sqrt{13}$. По теореме Пифагора $AB=\sqrt{(\sqrt3)^2+(\sqrt{13})^2}=\sqrt{16}=4$. Искомый угол — $\angle ABO$, $\cos\angle ABO=\dfrac{OB}{AB}=\dfrac{\sqrt{13}}{4}$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 3, § 9' },
|
||||
|
||||
{ idx: 10, type: 'open', topic: 'functions', subtopic: 'fn-properties', diff: 3,
|
||||
text: R`Укажите номера верных утверждений.<br>1) функция $f(x)=(\sqrt3-1)^x$ является возрастающей на области определения;<br>2) график функции $f(x)=3^x$ пересекает прямую $y=1$;<br>3) значение функции $f(x)=\log_{0{,}5}x$ меньше нуля при $x=\dfrac23$;<br>4) функция $f(x)=\log_{2{,}02}x$ является возрастающей на области определения;<br>5) $f(3{,}5)>f(4{,}2)$, если $f(x)=\left(\dfrac13\right)^x$.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '245', ansShow: '2, 4, 5',
|
||||
sol: R`$1)$ неверно: $0<\sqrt3-1<1$, функция убывает. $\ 2)$ верно: график $y=3^x$ пересекает $y=1$ в точке $(0;1)$. $\ 3)$ неверно: $\log_{0{,}5}\dfrac23>0$. $\ 4)$ верно: $2{,}02>1$, функция возрастает. $\ 5)$ верно: при основании $\dfrac13$ функция убывает, и из $3{,}5<4{,}2$ следует $f(3{,}5)>f(4{,}2)$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 4; гл. 3, § 8' },
|
||||
|
||||
// ── Часть B ──────────────────────────────────────────────────────────────
|
||||
{ idx: 11, type: 'long', topic: 'stereometry', subtopic: 'ster-rotation', diff: 3,
|
||||
text: R`Конус получен вращением равнобедренного прямоугольного треугольника вокруг прямой, содержащей его катет, равный $\sqrt{21}$. Для начала каждого из предложений А–В подберите его окончание 1–6 так, чтобы получилось верное утверждение.<br><b>Начало:</b><br>А) Диаметр основания конуса равен …<br>Б) Площадь осевого сечения конуса равна …<br>В) Объём конуса, если в качестве числа $\pi$ взято число Архимеда $\dfrac{22}{7}$, равен …<br><b>Окончание:</b><br>1) $42$; 2) $22\sqrt{21}$; 3) $66\sqrt{21}$; 4) $21$; 5) $2\sqrt{21}$; 6) $\sqrt{21}$.<br><i>Ответ запишите сочетанием букв и цифр, например: А1Б1В4.</i>`,
|
||||
answer: 'А5Б4В2', ansShow: 'А5Б4В2',
|
||||
sol: R`Радиус и высота конуса равны катету $\sqrt{21}$. А) Диаметр $=2\sqrt{21}$ — окончание 5. Б) Осевое сечение — равнобедренный треугольник с основанием $2\sqrt{21}$ и высотой $\sqrt{21}$: $S=\tfrac12\cdot2\sqrt{21}\cdot\sqrt{21}=21$ — окончание 4. В) $V=\tfrac13\cdot\dfrac{22}{7}\cdot(\sqrt{21})^2\cdot\sqrt{21}=\tfrac13\cdot\dfrac{22}{7}\cdot21\sqrt{21}=22\sqrt{21}$ — окончание 2.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 4' },
|
||||
|
||||
{ idx: 12, type: 'open', topic: 'expressions', subtopic: 'expr-powers-roots', diff: 3,
|
||||
text: R`Выберите верные утверждения.<br>1) значение выражения $(-1)^{-5}\cdot(-2)^2$ равно $-4$;<br>2) значение выражения $8^{1/3}\cdot12^0$ равно $-2$;<br>3) значение выражения $5^{-1/7}:25^{-4/7}$ равно $0{,}2$;<br>4) значение выражения $4-64^{1/3}$ равно $8$;<br>5) значение выражения $16^{-1/4}$ равно $0{,}5$;<br>6) значение выражения $2\cdot49^{0{,}5}+\left(2^{-1{,}5}\right)^{-2}$ равно $22$.<br><i>Ответ запишите номерами в порядке возрастания, без пробелов.</i>`,
|
||||
answer: '156', ansShow: '1, 5, 6',
|
||||
sol: R`$1)\ (-1)^{-5}\cdot(-2)^2=-1\cdot4=-4$ — верно. $\ 2)\ 8^{1/3}\cdot1=2\ne-2$ — неверно. $\ 3)\ 5^{-1/7}:5^{-8/7}=5^{1}=5\ne0{,}2$ — неверно. $\ 4)\ 4-4=0\ne8$ — неверно. $\ 5)\ 16^{-1/4}=2^{-1}=0{,}5$ — верно. $\ 6)\ 2\cdot7+2^3=14+8=22$ — верно.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 1, § 1' },
|
||||
|
||||
{ idx: 13, type: 'open', topic: 'numbers', subtopic: 'num-divisibility', diff: 2,
|
||||
text: R`Первый диспетчер такси принял за день $155$ заявок. Найдите наибольшее число заявок, принятых вторым диспетчером, если число заявок, принятых двумя диспетчерами вместе, не превосходит $300$ и кратно $9$.`,
|
||||
answer: '142',
|
||||
sol: R`Наибольшее не превосходящее $300$ число, кратное $9$, равно $297$. Тогда наибольшее число заявок второго диспетчера $297-155=142$.`,
|
||||
ref: 'Математика, 5 класс, ч. 1, гл. 1, § 13' },
|
||||
|
||||
{ idx: 14, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 2,
|
||||
text: R`В параллелограмме $ABCD$ угол $BAD$ равен $45^\circ$, $BH$ — высота, проведённая к стороне $AD$, $AH=4$, $DH=8$. Найдите площадь параллелограмма $ABCD$.`,
|
||||
answer: '48',
|
||||
sol: R`Так как $\angle BAD=45^\circ$, прямоугольный треугольник $BHA$ равнобедренный, поэтому $BH=AH=4$. Сторона $AD=AH+HD=12$. Площадь $S=AD\cdot BH=12\cdot4=48$.`,
|
||||
ref: 'Геометрия, 8 класс, гл. 2, § 14' },
|
||||
|
||||
{ idx: 15, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите значение выражения $x_0-4$, где $x_0$ — корень уравнения $\log_{81}(7-x)-1\dfrac14=0$.`,
|
||||
answer: '-240',
|
||||
sol: R`$\log_{81}(7-x)=\dfrac54$, $\dfrac14\log_3(7-x)=\dfrac54$, $\log_3(7-x)=5$, $7-x=3^5=243$, $x=-236$. Тогда $x_0-4=-236-4=-240$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 9' },
|
||||
|
||||
{ idx: 16, type: 'open', topic: 'functions', subtopic: 'fn-graphs', diff: 3,
|
||||
text: R`Найдите количество всех целых значений аргумента, при которых функция $f(x)=\dfrac1{12}(x-8)^2-3$ принимает отрицательные значения.`,
|
||||
answer: '11',
|
||||
sol: R`Нули функции: $\dfrac1{12}(x-8)^2-3=0$, $(x-8)^2=36$, $x_1=2$, $x_2=14$. Ветви параболы направлены вверх, поэтому функция отрицательна на $(2;14)$. Целых значений на этом промежутке — $11$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 3, § 14' },
|
||||
|
||||
{ idx: 17, type: 'open', topic: 'trigonometry', subtopic: 'trig-identities', diff: 3,
|
||||
text: R`Найдите значение выражения $64\cos 2\alpha$, если $\sin\alpha=\dfrac18$.`,
|
||||
answer: '62',
|
||||
sol: R`$\cos2\alpha=1-2\sin^2\alpha=1-2\cdot\dfrac1{64}=\dfrac{62}{64}$. Тогда $64\cos2\alpha=64\cdot\dfrac{62}{64}=62$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 11' },
|
||||
|
||||
{ idx: 18, type: 'open', topic: 'equations', subtopic: 'eq-rational', diff: 4,
|
||||
text: R`Два дачных участка прямоугольной формы имеют одинаковую длину. Площадь первого участка равна $434$ м$^2$, площадь второго участка равна $558$ м$^2$. Найдите (в метрах) периметр второго участка, если известно, что сумма ширин двух участков составляет $320$ дм.`,
|
||||
answer: '98',
|
||||
sol: R`$320$ дм $=32$ м. Пусть ширина второго участка $x$ м, первого $(32-x)$ м, общая длина $y$ м. Тогда $\begin{cases}(32-x)y=434,\\ xy=558.\end{cases}$ Подставив $xy=558$: $32y-558=434$, $32y=992$, $y=31$, $x=18$. Второй участок $31\times18$, периметр $2(31+18)=98$ м.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 3, § 11' },
|
||||
|
||||
{ idx: 19, type: 'open', topic: 'word-sequences', subtopic: 'seq-progressions', diff: 4,
|
||||
text: R`В арифметической прогрессии $(a_n)$ четвёртый, пятый и шестой члены имеют вид $a_4=-2x$; $\ a_5=15-3x$; $\ a_6=55-5x$. Найдите сумму тридцати первых членов этой прогрессии.`,
|
||||
answer: '-4950',
|
||||
sol: R`По свойству $a_5=\dfrac{a_4+a_6}{2}$: $15-3x=\dfrac{-2x+55-5x}{2}$, $30-6x=-7x+55$, $x=25$. Тогда $a_4=-50$, $a_5=-60$, $a_6=-70$, разность $d=-10$, $a_1=a_4-3d=-20$. $S_{30}=\dfrac{2a_1+d(30-1)}{2}\cdot30=\dfrac{-40-290}{2}\cdot30=-4950$.`,
|
||||
ref: 'Алгебра, 9 класс, гл. 4, § 15–16' },
|
||||
|
||||
{ idx: 20, type: 'open', topic: 'planimetry', subtopic: 'plan-quadrilaterals', diff: 4,
|
||||
text: R`Радиус окружности, вписанной в равнобедренную трапецию, равен $3\sqrt2$. Тупой угол равнобедренной трапеции равен $120^\circ$. Найдите значение выражения $P^2$, где $P$ — периметр равнобедренной трапеции.`,
|
||||
answer: '1536',
|
||||
sol: R`Высота трапеции равна диаметру вписанной окружности: $BK=6\sqrt2$. В прямоугольном треугольнике с острым углом $30^\circ$ боковая сторона $AB=\dfrac{BK}{\sin60^\circ}=\dfrac{6\sqrt2}{\sqrt3/?}$… Для описанной окружностью трапеции $AB+CD=BC+AD$, $AB=CD=4\sqrt6$, поэтому сумма оснований $BC+AD=8\sqrt6$. Периметр $P=2\cdot8\sqrt6=16\sqrt6$, тогда $P^2=256\cdot6=1536$.`,
|
||||
ref: 'Геометрия, 9 класс, гл. 2, § 10' },
|
||||
|
||||
{ idx: 21, type: 'open', topic: 'equations', subtopic: 'eq-linear', diff: 3,
|
||||
text: R`Найдите сумму всех целых решений совокупности неравенств $\left[\begin{array}{l}\dfrac{x-2}{7}<\dfrac{x+2}{2}-\dfrac1{14},\\[4pt] (x-3)^2+5<(x+2)^2-20\end{array}\right.$ на промежутке $[-7;7]$.`,
|
||||
answer: '22',
|
||||
sol: R`Первое неравенство: $2x-4<7x+14-1$, $-5x<17$, $x>-3{,}4$. Второе: $x^2-6x+9+5<x^2+4x+4-20$, $-10x<-30$, $x>3$. Объединение совокупности — луч $(-3{,}4;+\infty)$. Пересечение с $[-7;7]$ даёт $(-3{,}4;7]$. Сумма целых от $-3$ до $7$ равна $22$.`,
|
||||
ref: 'Алгебра, 8 класс, гл. 1, § 6' },
|
||||
|
||||
{ idx: 22, type: 'open', topic: 'word-sequences', subtopic: 'word-problems', diff: 3,
|
||||
text: R`Имеется $28$ кг сплава меди с цинком, содержащего $34{,}5\%$ меди. Сколько меди (в граммах) необходимо добавить к этому сплаву, чтобы получить сплав, содержащий $60\%$ меди?`,
|
||||
answer: '17850',
|
||||
sol: R`Масса меди в исходном сплаве $28\cdot0{,}345=9{,}66$ кг. Пусть добавили $x$ кг меди: $(9{,}66+x)=(28+x)\cdot0{,}6$, $9{,}66+x=16{,}8+0{,}6x$, $0{,}4x=7{,}14$, $x=17{,}85$ кг $=17850$ г.`,
|
||||
ref: 'Алгебра, 7 класс, гл. 3, § 16' },
|
||||
|
||||
{ idx: 23, type: 'open', topic: 'equations', subtopic: 'eq-irrational', diff: 4,
|
||||
text: R`Найдите произведение корней (корень, если он единственный) уравнения $\sqrt{2x^2+11x-14}=-x-2$.`,
|
||||
answer: '-9',
|
||||
sol: R`Возведём в квадрат: $2x^2+11x-14=x^2+4x+4$, $x^2+7x-18=0$, корни $-9$ и $2$. Проверка: при $x=-9$ $\sqrt{49}=7=-(-9)-2$ — верно; при $x=2$ $\sqrt{16}=4\ne-4$ — посторонний. Единственный корень $-9$; произведение равно $-9$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 2, § 17' },
|
||||
|
||||
{ idx: 24, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`$ABCDA_1B_1C_1D_1$ — куб, у которого длина ребра равна $6\sqrt3$. Точки $M$ и $N$ — середины рёбер $AB$ и $AD$. Через точки $M$, $N$ и $C_1$ проведена секущая плоскость. Найдите значение выражения $n\cdot a^2$, где $n$ — количество вершин многоугольника сечения, $a$ — длина отрезка, по которому секущая плоскость пересекает грань $AA_1D_1D$.`,
|
||||
answer: '195',
|
||||
sol: R`Сечение — пятиугольник $C_1LNMK$, поэтому $n=5$. Сторона $NL$ (по грани $AA_1D_1D$): из построения и подобия $DL=2\sqrt3$, $DN=3\sqrt3$, тогда $NL=\sqrt{DL^2+DN^2}=\sqrt{(2\sqrt3)^2+(3\sqrt3)^2}=\sqrt{39}$, то есть $a=\sqrt{39}$. Значение $n\cdot a^2=5\cdot39=195$.`,
|
||||
ref: 'Геометрия, 10 класс, разд. 1, § 3' },
|
||||
|
||||
{ idx: 25, type: 'open', topic: 'trigonometry', subtopic: 'trig-equations', diff: 5,
|
||||
text: R`Найдите (в градусах) сумму различных корней уравнения $\sin 3x\cos 3x\cos 6x=-\dfrac{\sqrt3}{8}$ на промежутке $[-60^\circ;0^\circ]$.`,
|
||||
answer: '-90',
|
||||
sol: R`$\tfrac12\sin6x\cos6x=-\dfrac{\sqrt3}{8}$, $\tfrac14\sin12x=-\dfrac{\sqrt3}{8}$, $\sin12x=-\dfrac{\sqrt3}{2}$. Тогда $12x=(-1)^{k+1}60^\circ+180^\circ k$, $x=(-1)^{k+1}5^\circ+15^\circ k$. На $[-60^\circ;0^\circ]$ корни: $-5^\circ,\ -10^\circ,\ -35^\circ,\ -40^\circ$. Их сумма $-90^\circ$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 1, § 8; § 11' },
|
||||
|
||||
{ idx: 26, type: 'open', topic: 'stereometry', subtopic: 'ster-rotation', diff: 5,
|
||||
text: R`Сфера касается всех сторон равнобедренного треугольника $ABC$, у которого длина основания $AC$ равна $10$ и длина боковой стороны $AB$ равна $11$. Расстояние от центра сферы до плоскости треугольника $ABC$ равно $\dfrac{5\sqrt{42}}{4}$. Найдите значение выражения $\dfrac{S}{\pi}$, где $S$ — площадь сферы.`,
|
||||
answer: '300',
|
||||
sol: R`Точки касания равноудалены от проекции $O_1$ центра сферы, значит $O_1$ — центр вписанной в $ABC$ окружности. Площадь по Герону: $p=16$, $S_{ABC}=\sqrt{16\cdot5\cdot5\cdot6}=20\sqrt6$, радиус вписанной $r=\dfrac{S}{p}=\dfrac{20\sqrt6}{16}=\dfrac{5\sqrt6}{4}$. Радиус сферы $OK=\sqrt{OO_1^2+r^2}=\sqrt{\dfrac{25\cdot42}{16}+\dfrac{25\cdot6}{16}}=\sqrt{75}=5\sqrt3$. Площадь сферы $S=4\pi R^2=4\pi\cdot75=300\pi$, поэтому $\dfrac{S}{\pi}=300$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 3, § 5' },
|
||||
|
||||
{ idx: 27, type: 'open', topic: 'equations', subtopic: 'eq-logarithmic', diff: 4,
|
||||
text: R`Найдите сумму всех целых решений неравенства $\log_{\lg 8}(8-x)-\log_{\lg 8}(x-4)\ge 0$.`,
|
||||
answer: '13',
|
||||
sol: R`Так как $0<\lg8<1$, функция $\log_{\lg8}t$ убывает, поэтому неравенство $\log_{\lg8}(8-x)\ge\log_{\lg8}(x-4)$ равносильно системе $8-x\le x-4$ и $8-x>0$, то есть $x\ge6$ и $x<8$. Решение $[6;8)$, целые $6$ и $7$, сумма $13$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 3, § 10' },
|
||||
|
||||
{ idx: 28, type: 'open', topic: 'equations', subtopic: 'eq-exponential', diff: 4,
|
||||
text: R`Найдите произведение наименьшего целого решения на количество всех целых решений неравенства $\left(\sqrt2-1\right)^{\frac{(x+9)^2(3-x)}{x-6}}\le 1$.`,
|
||||
answer: '-36',
|
||||
sol: R`Так как $\sqrt2-1\in(0;1)$, неравенство равносильно $\dfrac{(x+9)^2(3-x)}{x-6}\ge0$, или $\dfrac{(x+9)^2(x-3)}{x-6}\le0$. Методом интервалов (нули $-9,3$; разрыв $6$): решение $\{-9\}\cup[3;6)$. Целых решений $4$ ($-9,3,4,5$), наименьшее $-9$. Произведение $-9\cdot4=-36$.`,
|
||||
ref: 'Алгебра, 11 класс, гл. 2, § 6' },
|
||||
|
||||
{ idx: 29, type: 'open', topic: 'functions', subtopic: 'fn-derivative', diff: 5,
|
||||
text: R`Дана функция $f(x)=\dfrac{2x^2-x}{x+5}$. Найдите значение выражения $a\cdot n$, где $a$ — наименьшее целое число из промежутков убывания данной функции, $n$ — количество всех целых чисел из промежутков убывания данной функции.`,
|
||||
answer: '-100',
|
||||
sol: R`$f'(x)=\dfrac{2x^2+20x-5}{(x+5)^2}$. Убывание: $f'(x)<0$ при $\dfrac{-10-\sqrt{110}}{2}<x<-5$ и $-5<x<\dfrac{-10+\sqrt{110}}{2}$ (то есть примерно на $(-10{,}2;-5)$ и $(-5;0{,}2)$). Наименьшее целое из этих промежутков $a=-10$, количество целых $n=10$. Значение $a\cdot n=-100$.`,
|
||||
ref: 'Алгебра, 10 класс, гл. 3, § 20' },
|
||||
|
||||
{ idx: 30, type: 'open', topic: 'stereometry', subtopic: 'ster-polyhedra', diff: 5,
|
||||
text: R`В основании пирамиды лежит прямоугольный треугольник, у которого гипотенуза равна $8$ и один из острых углов равен $60^\circ$. Каждая боковая грань пирамиды наклонена к плоскости основания под углом, равным $\operatorname{arcctg}\dfrac{\sqrt5}{2}$. Найдите значение выражения $\left(3\sqrt5+\sqrt{15}\right)\cdot V$, где $V$ — объём данной пирамиды.`,
|
||||
answer: '64',
|
||||
sol: R`Высота пирамиды опущена в центр вписанной в основание окружности (двугранные углы при основании равны). В прямоугольном треугольнике $BC=4$, $AB=4\sqrt3$, $S_{ABC}=\tfrac12\cdot4\sqrt3\cdot4=8\sqrt3$, $r=\dfrac{AB+BC-AC}{2}=2\sqrt3-2$. Высота $SO=\dfrac{r}{\operatorname{ctg}\angle SMO}=\dfrac{2\sqrt3-2}{\sqrt5/2}=\dfrac{4\sqrt{15}-4\sqrt5}{5}$. Объём $V=\tfrac13\cdot8\sqrt3\cdot SO=\dfrac{32(3\sqrt5-\sqrt{15})}{15}$. Тогда $\left(3\sqrt5+\sqrt{15}\right)\cdot V=64$.`,
|
||||
ref: 'Геометрия, 11 класс, разд. 2, § 3' },
|
||||
];
|
||||
|
||||
/* ── машинерия ─────────────────────────────────────────────────────────────── */
|
||||
function ansShowOf(t) { if (t.ansShow != null) return t.ansShow; if (t.type === 'mc') return `${t.answer})`; return `$${t.answer}$`; }
|
||||
function buildSolution(t) {
|
||||
let html = `${t.sol}<div class="sol-ans">Ответ: ${ansShowOf(t)}</div>`;
|
||||
if (t.ref) html += `<div class="sol-ref" style="margin-top:6px;font-size:.85em;opacity:.7">Учебник: ${t.ref}</div>`;
|
||||
return html;
|
||||
}
|
||||
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(u, c0) {
|
||||
if (u == null || c0 == null) return false;
|
||||
const c = String(c0).trim();
|
||||
if (/^[а-д]$/.test(c)) return String(u).trim().toLowerCase() === c.toLowerCase();
|
||||
if (/^[^;]+;[^;]+$/.test(c)) return false;
|
||||
const cn = srvToNumber(c), un = srvToNumber(u);
|
||||
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(`Дубль idx=${t.idx}`); seen.add(t.idx);
|
||||
if (t.idx < 1 || t.idx > 30) problems.push(`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}: self-check "${t.answer}"`);
|
||||
if (/−/.test(String(t.answer))) problems.push(`#${t.idx}: Unicode-минус в answer`);
|
||||
}
|
||||
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);
|
||||
if (!db.prepare(`SELECT exam_key FROM exam_tracks WHERE exam_key=?`).get(EXAM)) { console.error(`✗ Трек '${EXAM}' не найден.`); process.exit(1); }
|
||||
console.log(`\n=== seed_ctmath_rt2425_e3v1 (${PROV}) variant=${VARIANT} ===`);
|
||||
console.log(`Режим: ${APPLY ? 'APPLY' : 'DRY-RUN'}\n`);
|
||||
console.log('Типы:', JSON.stringify(TASKS.reduce((a, t) => (a[t.type] = (a[t.type] || 0) + 1, a), {})), '| фигур:', TASKS.filter(t => t.fig).length, '\n');
|
||||
console.log('idx | type | subtopic | d | answer | fig');
|
||||
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).padEnd(9)} | ${t.fig ? '✓' : ''}`);
|
||||
if (problems.length) { console.error(`\n✗ ПРОБЛЕМЫ (${problems.length}):`); problems.forEach(p => console.error(' - ' + p)); db.close(); process.exit(1); }
|
||||
console.log('\n✓ Валидация и self-check ответов пройдены (30/30).');
|
||||
if (!APPLY) { console.log('\nDRY-RUN: ничего не записано. Для записи добавьте --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}). variants_count=${distinct}.`);
|
||||
console.log(`\nПробник: /exam-prep/ctmath → «Варианты» → «Вариант ${VARIANT}».\n`);
|
||||
} catch (e) { db.exec('ROLLBACK'); console.error('\n✗ Ошибка записи, откат:', e.message); process.exitCode = 1; }
|
||||
db.close();
|
||||
@@ -0,0 +1,70 @@
|
||||
'use strict';
|
||||
/* ───────────────────────────────────────────────────────────────────────────
|
||||
update_textbook_authors.js
|
||||
Приводит метаданные учебников к политике «все учебники наши»:
|
||||
• колонка textbooks.author → 'LearnSpace' (у всех учебников и их глав);
|
||||
• в textbooks.description убирается оборот «по учебнику <автор>:» → «:».
|
||||
|
||||
Миграции 004/008/017–027/031/038/049/050 уже применены к БД с фамилиями сторонних
|
||||
авторов — их исходники почищены, но ЖИВУЮ БД правит этот идемпотентный скрипт.
|
||||
|
||||
Запуск:
|
||||
node backend/scripts/update_textbook_authors.js # DRY-RUN (по умолчанию)
|
||||
node backend/scripts/update_textbook_authors.js --apply # запись в БД
|
||||
|
||||
⚠️ Массовую запись в БД запускает ПОЛЬЗОВАТЕЛЬ вручную. Без --apply ничего не пишется.
|
||||
─────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
const { DatabaseSync } = require('node:sqlite');
|
||||
const path = require('path');
|
||||
|
||||
const APPLY = process.argv.includes('--apply');
|
||||
const AUTHOR = 'LearnSpace';
|
||||
const DB = path.join(__dirname, '..', 'data', 'learnspace.db');
|
||||
|
||||
const stripDesc = d => (d ? d.replace(/ по учебнику [^:]*:/g, ':') : d);
|
||||
|
||||
const db = new DatabaseSync(DB);
|
||||
|
||||
// есть ли таблица/колонки
|
||||
const cols = db.prepare(`PRAGMA table_info(textbooks)`).all().map(c => c.name);
|
||||
if (!cols.includes('author')) { console.error('✗ В таблице textbooks нет колонки author. Прерывание.'); db.close(); process.exit(1); }
|
||||
|
||||
const rows = db.prepare(`SELECT id, slug, author, description FROM textbooks`).all();
|
||||
const changes = [];
|
||||
for (const r of rows) {
|
||||
const newDesc = stripDesc(r.description);
|
||||
const authorChange = (r.author || '') !== AUTHOR;
|
||||
const descChange = newDesc !== r.description;
|
||||
if (authorChange || descChange) changes.push({ id: r.id, slug: r.slug, oldAuthor: r.author, authorChange, descChange, newDesc });
|
||||
}
|
||||
|
||||
console.log(`\n=== update_textbook_authors (учебников: ${rows.length}) ===`);
|
||||
console.log(`Режим: ${APPLY ? 'APPLY (запись)' : 'DRY-RUN (только показ)'}\n`);
|
||||
console.log(`Под изменение: ${changes.length}`);
|
||||
for (const c of changes) {
|
||||
const tags = [c.authorChange ? `author: ${c.oldAuthor || '∅'} → ${AUTHOR}` : null, c.descChange ? 'описание: убран «по учебнику …»' : null].filter(Boolean).join('; ');
|
||||
console.log(` ${String(c.slug).padEnd(20)} ${tags}`);
|
||||
}
|
||||
|
||||
if (!changes.length) { console.log('\nНечего менять — БД уже чистая.'); db.close(); process.exit(0); }
|
||||
|
||||
if (!APPLY) {
|
||||
console.log('\nDRY-RUN: ничего не записано. Для записи: node backend/scripts/update_textbook_authors.js --apply\n');
|
||||
db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const upd = db.prepare(`UPDATE textbooks SET author = ?, description = ? WHERE id = ?`);
|
||||
let n = 0;
|
||||
db.exec('BEGIN');
|
||||
try {
|
||||
for (const c of changes) { upd.run(AUTHOR, c.newDesc, c.id); n++; }
|
||||
db.exec('COMMIT');
|
||||
console.log(`\n✓ Обновлено строк: ${n}. Все учебники → author='${AUTHOR}', обороты «по учебнику …» убраны.\n`);
|
||||
} catch (e) {
|
||||
db.exec('ROLLBACK');
|
||||
console.error('\n✗ Ошибка записи, откат:', e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
db.close();
|
||||
@@ -0,0 +1,58 @@
|
||||
'use strict';
|
||||
/* Авто-здоровье LLM-провайдеров Квантика: периодический пинг каждого провайдера
|
||||
* (lightweight pingLLM) + авто-понижение активного, если он стабильно не отвечает,
|
||||
* а есть здоровый запасной. Результат — в app_settings.assistant_health (JSON-карта
|
||||
* { id: { ok, at, error, ms, fails } }). Авто-переключение пишет тот же
|
||||
* assistant_failover, что показывает баннер в админке. Период — 15 мин (вкл. по
|
||||
* умолчанию; app_settings.assistant_health_enabled='0' выключает). */
|
||||
const db = require('./db/db');
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
function _get(k) { const r = db.prepare('SELECT value FROM app_settings WHERE key=?').get(k); return r && r.value != null ? r.value : null; }
|
||||
function _set(k, v) { db.prepare('INSERT OR REPLACE INTO app_settings (key,value) VALUES (?,?)').run(k, v); }
|
||||
function _providers() { try { return JSON.parse(_get('assistant_providers') || '[]') || []; } catch (e) { return []; } }
|
||||
function _enabled() { return _get('assistant_health_enabled') !== '0'; }
|
||||
function _noKey(u) { return /\/\/(localhost|127\.0\.0\.1)/.test(u || '') || /\/\/[^/]*\bpollinations\.ai\b/i.test(u || ''); }
|
||||
|
||||
async function runHealthCheck() {
|
||||
if (!_enabled()) return { skipped: true };
|
||||
const provs = _providers();
|
||||
if (!provs.length) return { providers: 0 };
|
||||
const a = require('./controllers/assistantController');
|
||||
let prev = {}; try { prev = JSON.parse(_get('assistant_health') || '{}') || {}; } catch (e) {}
|
||||
const health = {};
|
||||
for (const p of provs) {
|
||||
// нет ключа и не keyless-шлюз — не пингуем (в FAQ-режиме), помечаем как «нет ключа»
|
||||
if (!p.key && !_noKey(p.url)) { health[p.id] = { ok: false, at: new Date().toISOString(), error: 'нет ключа', ms: 0, fails: (prev[p.id] && prev[p.id].fails || 0) }; continue; }
|
||||
const t0 = Date.now();
|
||||
let r; try { r = await a.pingLLM({ url: p.url, model: p.model, key: p.key }); } catch (e) { r = { ok: false, error: 'сбой' }; }
|
||||
const ok = !!(r && r.ok);
|
||||
health[p.id] = {
|
||||
ok, at: new Date().toISOString(), ms: Date.now() - t0,
|
||||
error: ok ? null : String((r && (r.error || ('HTTP ' + r.status))) || 'ошибка').slice(0, 140),
|
||||
fails: ok ? 0 : ((prev[p.id] && prev[p.id].fails || 0) + 1),
|
||||
};
|
||||
}
|
||||
_set('assistant_health', JSON.stringify(health));
|
||||
|
||||
// авто-понижение активного: 2+ подряд неудач И есть здоровый рабочий запасной
|
||||
const activeId = _get('assistant_active');
|
||||
const active = provs.find(p => p.id === activeId);
|
||||
if (active && health[activeId] && !health[activeId].ok && health[activeId].fails >= 2) {
|
||||
const healthy = provs.find(p => p.id !== activeId && health[p.id] && health[p.id].ok && (p.key || _noKey(p.url)));
|
||||
if (healthy) {
|
||||
_set('assistant_active', healthy.id);
|
||||
_set('assistant_failover', JSON.stringify({ at: new Date().toISOString(), failedId: activeId, failedName: active.name, servedId: healthy.id, servedName: healthy.name, reason: 'health', auto: true }));
|
||||
logger.info('assistant-health auto-demote', { from: active.name, to: healthy.name, fails: health[activeId].fails });
|
||||
}
|
||||
}
|
||||
return { providers: provs.length, health };
|
||||
}
|
||||
|
||||
function schedule() {
|
||||
const run = () => { runHealthCheck().catch(() => {}); };
|
||||
setTimeout(run, 90_000).unref(); // первый прогон через 1.5 мин после старта
|
||||
setInterval(run, 15 * 60 * 1000).unref(); // далее каждые 15 минут
|
||||
}
|
||||
|
||||
module.exports = { runHealthCheck, schedule };
|
||||
@@ -1,7 +1,10 @@
|
||||
const db = require('../db/db');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const { audit } = require('../utils/audit');
|
||||
const { purgeAccessFor } = require('../services/contentAccess');
|
||||
const sysReset = require('../services/systemReset');
|
||||
|
||||
/* ── Prepared statements ──────────────────────────────────────────────── */
|
||||
const stmts = {
|
||||
@@ -292,13 +295,18 @@ function getUserSessions(req, res) {
|
||||
|
||||
/* ── GET /api/admin/sessions ─────────────────────────────────────────── */
|
||||
function getAllSessions(req, res) {
|
||||
const { subject, user_id } = req.query;
|
||||
const { subject, user_id, status } = req.query;
|
||||
const limit = Math.min(500, Math.max(1, Number(req.query.limit) || 200));
|
||||
const offset = Math.max(0, Number(req.query.offset) || 0);
|
||||
|
||||
const where = ['ts.status = \'completed\''];
|
||||
// По умолчанию показываем и завершённые, и НЕзавершённые (in_progress) — иначе зависшие
|
||||
// сессии не находились в списке (см. алерт «Зависла»). Опционально сужаем по ?status=.
|
||||
const where = [];
|
||||
const params = [];
|
||||
|
||||
if (status && ['completed', 'in_progress', 'abandoned'].includes(status)) {
|
||||
where.push('ts.status = ?'); params.push(status);
|
||||
}
|
||||
if (subject) { where.push('s.slug = ?'); params.push(subject); }
|
||||
if (user_id) { where.push('ts.user_id = ?'); params.push(Number(user_id)); }
|
||||
|
||||
@@ -314,7 +322,7 @@ function getAllSessions(req, res) {
|
||||
FROM test_sessions ts
|
||||
LEFT JOIN subjects s ON s.id = ts.subject_id
|
||||
JOIN users u ON u.id = ts.user_id
|
||||
WHERE ${where.join(' AND ')}
|
||||
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
||||
ORDER BY ts.started_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`).all(...params);
|
||||
@@ -525,7 +533,7 @@ function getFeatures(_req, res) {
|
||||
function updateFeatures(req, res) {
|
||||
const allowed = ['crossword', 'hangman', 'pet', 'red_book', 'collection',
|
||||
'flashcards', 'knowledge_map', 'board', 'biochem', 'live_quiz', 'classroom',
|
||||
'gamification', 'assistant', 'sim_builder'];
|
||||
'gamification', 'assistant', 'sim_builder', 'quantik', 'theory', 'lab', 'sitemap', 'wishes'];
|
||||
const updates = req.body;
|
||||
const stmt = db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES (?, ?)");
|
||||
const getOld = db.prepare("SELECT value FROM app_settings WHERE key = ?");
|
||||
@@ -586,6 +594,56 @@ function updateFreeStudentFeatures(req, res) {
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/reset-system/plan ──────────────────────────────────
|
||||
План «чистого запуска»: что переназначится / сотрётся / неизвестно. Без изменений. */
|
||||
function getResetPlan(req, res) {
|
||||
try {
|
||||
const plan = sysReset.classify(db);
|
||||
// Текущий админ остаётся залогиненным — сохраняем именно его, не min-id.
|
||||
res.json({ ...plan, keptAdmin: { id: req.user.id, email: req.user.email, name: req.user.name } });
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: 'Не удалось построить план: ' + e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/* ── POST /api/admin/reset-system ──────────────────────────────────────
|
||||
⚠️ ДЕСТРУКТИВНО. Только admin. Требует body.confirm === 'СБРОС' (или 'RESET').
|
||||
Делает бэкап БД, сохраняет ТЕКУЩЕГО админа (оператор остаётся в системе),
|
||||
стирает остальных пользователей + активность, переназначает контент. */
|
||||
function resetSystem(req, res) {
|
||||
const confirm = (req.body && req.body.confirm) || '';
|
||||
if (confirm !== 'СБРОС' && confirm !== 'RESET') {
|
||||
return res.status(400).json({ error: 'Подтверждение не совпало. Введите СБРОС.' });
|
||||
}
|
||||
const keptId = req.user.id;
|
||||
// 1) Бэкап ДО любых изменений (checkpoint WAL → копия основного файла).
|
||||
let backupName = null;
|
||||
try {
|
||||
const dbPath = db._path;
|
||||
if (!dbPath) throw new Error('путь к БД неизвестен');
|
||||
const backupsDir = path.join(path.dirname(dbPath), 'backups');
|
||||
if (!fs.existsSync(backupsDir)) fs.mkdirSync(backupsDir, { recursive: true });
|
||||
try { db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch { /* не WAL — ок */ }
|
||||
const d = new Date();
|
||||
const p2 = n => String(n).padStart(2, '0');
|
||||
const ts = `${d.getFullYear()}${p2(d.getMonth() + 1)}${p2(d.getDate())}-${p2(d.getHours())}${p2(d.getMinutes())}${p2(d.getSeconds())}`;
|
||||
backupName = `learnspace-prereset-${ts}.db`;
|
||||
fs.copyFileSync(dbPath, path.join(backupsDir, backupName));
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'Бэкап не удался — сброс отменён: ' + e.message });
|
||||
}
|
||||
// 2) Сброс (бросает при ошибке → откат внутри сервиса, данные целы).
|
||||
let summary;
|
||||
try {
|
||||
summary = sysReset.runReset(db, keptId);
|
||||
} catch (e) {
|
||||
return res.status(500).json({ error: 'Сброс не выполнен (откат): ' + e.message, backup: backupName });
|
||||
}
|
||||
// 3) Аудит ПОСЛЕ сброса (admin_audit_log очищается сбросом — пишем первой записью).
|
||||
try { audit(req, 'system.reset', 'system', `keptAdmin=${keptId} backup=${backupName} deleted=${summary.deletedUsers}`); } catch {}
|
||||
res.json({ ok: true, backup: backupName, ...summary });
|
||||
}
|
||||
|
||||
/* ── GET /api/admin/audit-log ───────────────────────────────────────── */
|
||||
function getAuditLog(req, res) {
|
||||
const limit = Math.min(500, Math.max(1, Number(req.query.limit) || 100));
|
||||
@@ -659,8 +717,6 @@ function clearSecurityLog(req, res) {
|
||||
|
||||
/* ── GET /api/admin/health ─────────────────────────────────────────── */
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { execSync } = require('child_process');
|
||||
const { monitorEventLoopDelay } = require('perf_hooks');
|
||||
const sse = require('../sse');
|
||||
@@ -879,29 +935,41 @@ function broadcast(req, res) {
|
||||
|
||||
/* ── Ассистент «Квантик»: конфиг LLM из админки ──────────────────────── */
|
||||
const ASSISTANT_PRESETS = [
|
||||
{ name: 'Kilo Code (бесплатно)', url: 'https://kilocode.ai/api/openrouter/chat/completions', model: 'nvidia/nemotron-3-ultra-550b-a55b:free' },
|
||||
{ name: 'Kilo Code (бесплатно)', url: 'https://kilocode.ai/api/openrouter/chat/completions', model: 'nvidia/nemotron-3-super-120b-a12b:free' },
|
||||
{ name: 'Google Gemini', url: 'https://generativelanguage.googleapis.com/v1beta/openai/chat/completions', model: 'gemini-2.5-flash' },
|
||||
{ name: 'Groq', url: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile' },
|
||||
{ name: 'OpenRouter', url: 'https://openrouter.ai/api/v1/chat/completions', model: 'meta-llama/llama-3.3-70b-instruct:free' },
|
||||
{ name: 'HuggingFace Router', url: 'https://router.huggingface.co/v1/chat/completions', model: 'Qwen/Qwen2.5-72B-Instruct' },
|
||||
{ name: 'Pollinations (без ключа)', url: 'https://text.pollinations.ai/openai', model: 'openai' },
|
||||
{ name: 'Ollama (локально)', url: 'http://localhost:11434/v1/chat/completions', model: 'qwen2.5:3b' },
|
||||
];
|
||||
// Проверенные бесплатные модели Kilo (чистый русский) — для выпадающего списка
|
||||
// Проверенные бесплатные модели шлюза Kilo (отдают чистый русский). Порядок — от мощных к лёгким.
|
||||
// ctx — окно контекста, out — макс. токенов в ответе (данные из /api/openrouter/models). Все бесплатные ($0).
|
||||
// Сверено с live-списком шлюза и протестировано на русский 2026-06-24 (% — доля кириллицы в тест-ответе):
|
||||
// owl-alpha 95%, nemotron-super 91%, nano-omni 99%, laguna-xs.2 92%, openrouter/free 100% — чисто;
|
||||
// nemotron-ultra/laguna-m.1 — существуют, но на free-тарифе бывает таймаут; nex-n2-pro удалён со шлюза.
|
||||
const KILO_MODELS = [
|
||||
{ id: 'nvidia/nemotron-3-ultra-550b-a55b:free', label: 'Nemotron 550B — флагман (1M)', ctx: 1000000, out: 65536 },
|
||||
{ id: 'nvidia/nemotron-3-super-120b-a12b:free', label: 'Nemotron 120B — баланс (1M)', ctx: 1000000, out: 262144 },
|
||||
{ id: 'nex-agi/nex-n2-pro:free', label: 'Nex N2 Pro — чистый русский (262K)', ctx: 262144, out: 65536 },
|
||||
{ id: 'openrouter/owl-alpha', label: 'Owl Alpha — чистый русский (1M)', ctx: 1048756, out: 262144 },
|
||||
{ id: 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', label: 'Nemotron Nano 30B — быстрая (256K)', ctx: 256000, out: 65536 },
|
||||
{ id: 'poolside/laguna-m.1:free', label: 'Laguna M.1 — быстрая (262K)', ctx: 262144, out: 32768 },
|
||||
{ id: 'poolside/laguna-xs.2:free', label: 'Laguna XS — лёгкая (262K)', ctx: 262144, out: 32768 },
|
||||
{ id: 'nvidia/nemotron-3-super-120b-a12b:free', label: 'Nemotron 120B — баланс, быстрый (262K)', ctx: 262144, out: 262144 },
|
||||
{ id: 'openrouter/owl-alpha', label: 'Owl Alpha — чистый русский (1M)', ctx: 1048576, out: 262144 },
|
||||
{ id: 'nvidia/nemotron-3-ultra-550b-a55b:free', label: 'Nemotron 550B — флагман, медленный (1M)', ctx: 1000000, out: 65536 },
|
||||
{ id: 'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free', label: 'Nemotron Nano 30B — быстрый, мультимодальный (256K)', ctx: 256000, out: 65536 },
|
||||
{ id: 'poolside/laguna-m.1:free', label: 'Laguna M.1 (262K)', ctx: 262144, out: 32768 },
|
||||
{ id: 'poolside/laguna-xs.2:free', label: 'Laguna XS.2 — лёгкая, быстрая (262K)', ctx: 262144, out: 32768 },
|
||||
{ id: 'openrouter/free', label: 'Авто-роутер (бесплатные модели)', ctx: 200000, out: 32768 },
|
||||
];
|
||||
function _aset(k) { const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get(k); return r && r.value != null ? r.value : null; }
|
||||
|
||||
// Рабочий список бесплатных моделей: обновлённый сканом (app_settings) либо хардкод KILO_MODELS как сид.
|
||||
function _kiloModels() {
|
||||
try { const r = _aset('assistant_kilo_models'); if (r) { const a = JSON.parse(r); if (Array.isArray(a) && a.length) return a; } } catch (e) {}
|
||||
return KILO_MODELS;
|
||||
}
|
||||
|
||||
function _aProviders() { try { return JSON.parse(_aset('assistant_providers') || '[]') || []; } catch (e) { return []; } }
|
||||
function _aSetProviders(arr) { db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_providers', ?)").run(JSON.stringify(arr)); }
|
||||
function _aIsLocal(u) { return /\/\/(localhost|127\.0\.0\.1)/.test(u || ''); }
|
||||
// Шлюзы с бесплатным инференсом без ключа (как localhost): ключ не обязателен.
|
||||
function _aNoKey(u) { return _aIsLocal(u) || /\/\/[^/]*\bpollinations\.ai\b/i.test(u || ''); }
|
||||
|
||||
function getAssistant(_req, res) {
|
||||
// Миграция legacy-настроек в список провайдеров (один раз)
|
||||
@@ -915,10 +983,10 @@ function getAssistant(_req, res) {
|
||||
}
|
||||
}
|
||||
const list = _aProviders();
|
||||
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) }));
|
||||
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, noKey: _aNoKey(p.url), ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) }));
|
||||
const activeId = _aset('assistant_active') || (providers[0] && providers[0].id) || null;
|
||||
const ap = list.find(p => p.id === activeId);
|
||||
const active = !!(ap && (ap.key || _aIsLocal(ap.url)));
|
||||
const active = !!(ap && (ap.key || _aNoKey(ap.url)));
|
||||
|
||||
let chunks = 0, usage = { model_calls: 0, cache_hits: 0, faq: 0 }, usage30 = { model_calls: 0, cache_hits: 0, faq: 0 };
|
||||
try { chunks = db.prepare('SELECT COUNT(*) n FROM textbook_chunks').get().n; } catch (e) {}
|
||||
@@ -939,8 +1007,11 @@ function getAssistant(_req, res) {
|
||||
res.json({
|
||||
providers, activeId, active,
|
||||
rag: _aset('assistant_rag') !== '0', examButtons: _aset('assistant_exam_buttons') === '1',
|
||||
memory: _aset('assistant_memory') !== '0',
|
||||
chunks, usage, usage30, feedback, failover, presets: ASSISTANT_PRESETS, kiloModels: KILO_MODELS,
|
||||
memory: _aset('assistant_memory') !== '0', socratic: _aset('assistant_socratic') === '1',
|
||||
healthEnabled: _aset('assistant_health_enabled') !== '0',
|
||||
health: (() => { try { return JSON.parse(_aset('assistant_health') || '{}') || {}; } catch (e) { return {}; } })(),
|
||||
chunks, usage, usage30, feedback, failover, presets: ASSISTANT_PRESETS,
|
||||
kiloModels: _kiloModels(), kiloModelsCustom: !!_aset('assistant_kilo_models'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -951,6 +1022,8 @@ function saveAssistant(req, res) {
|
||||
if (typeof b.rag === 'boolean') set('assistant_rag', b.rag ? '1' : '0');
|
||||
if (typeof b.examButtons === 'boolean') set('assistant_exam_buttons', b.examButtons ? '1' : '0');
|
||||
if (typeof b.memory === 'boolean') set('assistant_memory', b.memory ? '1' : '0');
|
||||
if (typeof b.socratic === 'boolean') set('assistant_socratic', b.socratic ? '1' : '0');
|
||||
if (typeof b.healthEnabled === 'boolean') set('assistant_health_enabled', b.healthEnabled ? '1' : '0');
|
||||
if (b.dismissFailover) { try { db.prepare("DELETE FROM app_settings WHERE key = 'assistant_failover'").run(); } catch (e) {} }
|
||||
audit(req, 'assistant.config', 'assistant', 'настройки');
|
||||
res.json({ ok: true });
|
||||
@@ -1050,6 +1123,105 @@ async function getProviderModels(req, res) {
|
||||
res.json({ models: r.models, current });
|
||||
}
|
||||
|
||||
/* ── Сканер бесплатных моделей шлюза (наполняет список KILO_MODELS) ───────── */
|
||||
// Заведомо не-чат модели (музыка/картинки/эмбеддинги/модерация) — не тестируем.
|
||||
const _NONCHAT_RE = /(lyria|whisper|tts|embed|rerank|moderation|content-safety|guard|dall-?e|imagen|sora|veo|\bmusic\b)/i;
|
||||
|
||||
// Kilo-провайдер (со шлюзом kilocode.ai): по id, иначе активный, иначе первый с ключом.
|
||||
function _pickKiloProvider(id) {
|
||||
const arr = _aProviders();
|
||||
if (id) return arr.find(p => p.id === id) || null;
|
||||
const active = arr.find(p => p.id === _aset('assistant_active'));
|
||||
if (active && /kilocode\.ai/.test(active.url || '') && active.key) return active;
|
||||
return arr.find(p => /kilocode\.ai/.test(p.url || '') && p.key)
|
||||
|| arr.find(p => /kilocode\.ai/.test(p.url || '')) || null;
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/scan { id? } — найти бесплатные модели на шлюзе провайдера.
|
||||
* Без инференса: список + сверка с текущим рабочим списком (что новое / что исчезло). */
|
||||
async function scanModels(req, res) {
|
||||
const prov = _pickKiloProvider(req.body && req.body.id);
|
||||
if (!prov) return res.json({ error: 'Нет Kilo-провайдера. Добавьте провайдера со шлюзом kilocode.ai с ключом.' });
|
||||
const r = await _fetchModels(prov.url, prov.key);
|
||||
if (r.error) return res.json({ error: r.error, status: r.status });
|
||||
const cur = _kiloModels();
|
||||
const curIds = new Set(cur.map(m => m.id));
|
||||
const liveIds = new Set(r.models.map(m => m.id));
|
||||
const free = r.models
|
||||
.filter(m => (m.free === true || /:free$/.test(m.id)) && !_NONCHAT_RE.test(m.id))
|
||||
.map(m => ({ id: m.id, ctx: m.ctx, out: m.out, status: curIds.has(m.id) ? 'current' : 'new' }));
|
||||
free.sort((a, b) => (a.status === b.status ? (b.ctx || 0) - (a.ctx || 0) : a.status === 'current' ? -1 : 1));
|
||||
const gone = cur.filter(m => !liveIds.has(m.id)).map(m => ({ id: m.id, label: m.label }));
|
||||
res.json({ providerId: prov.id, providerName: prov.name, total: r.models.length, models: free, gone, current: cur });
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/probe { id?, model } — один тест-запрос на русском. */
|
||||
async function probeModel(req, res) {
|
||||
const b = req.body || {};
|
||||
const prov = _pickKiloProvider(b.id);
|
||||
if (!prov) return res.json({ ok: false, error: 'нет провайдера' });
|
||||
const model = String(b.model || '').trim().slice(0, 120);
|
||||
if (!model) return res.json({ ok: false, error: 'нет модели' });
|
||||
if (typeof fetch !== 'function') return res.json({ ok: false, error: 'fetch недоступен' });
|
||||
const PROMPT = 'Ученик 9 класса спрашивает: что такое синус острого угла в прямоугольном треугольнике? Объясни кратко и понятно. Отвечай только на русском языке.';
|
||||
const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 22000);
|
||||
const t0 = Date.now();
|
||||
try {
|
||||
const r = await fetch(prov.url, {
|
||||
method: 'POST', signal: ctrl.signal,
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, prov.key ? { Authorization: 'Bearer ' + prov.key } : {}),
|
||||
body: JSON.stringify({ model, max_tokens: 160, temperature: 0.3, messages: [{ role: 'user', content: PROMPT }] }),
|
||||
});
|
||||
const ms = Date.now() - t0;
|
||||
const txt = await r.text();
|
||||
if (!r.ok) {
|
||||
let msg = txt.slice(0, 200);
|
||||
try { const j = JSON.parse(txt); if (j && j.error) msg = String(j.error.message || JSON.stringify(j.error)).slice(0, 200); } catch (e) {}
|
||||
return res.json({ ok: false, status: r.status, ms, error: msg });
|
||||
}
|
||||
let sample = '';
|
||||
try { const j = JSON.parse(txt); const m = j.choices && j.choices[0] && j.choices[0].message; sample = String((m && (m.content || m.reasoning)) || '').trim(); } catch (e) {}
|
||||
const letters = (sample.match(/[A-Za-zА-Яа-яЁё一-鿿]/g) || []).length;
|
||||
const cyr = (sample.match(/[А-Яа-яЁё]/g) || []).length;
|
||||
const cjk = (sample.match(/[一-鿿]/g) || []).length;
|
||||
const ratio = letters ? cyr / letters : 0;
|
||||
const verdict = !sample ? 'пусто' : cjk > 0 ? 'иероглифы' : ratio > 0.55 ? 'чистый русский' : ratio > 0.2 ? 'смешанный' : 'не русский';
|
||||
res.json({ ok: true, ms, ratio: Math.round(ratio * 100), cjk, verdict, sample: sample.replace(/\s+/g, ' ').slice(0, 180) });
|
||||
} catch (e) { res.json({ ok: false, ms: Date.now() - t0, error: e.name === 'AbortError' ? 'таймаут' : 'сеть' }); }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/models/apply { models:[{id,label,ctx,out}] | reset:true } */
|
||||
function applyModels(req, res) {
|
||||
const b = req.body || {};
|
||||
if (b.reset) {
|
||||
try { db.prepare("DELETE FROM app_settings WHERE key = 'assistant_kilo_models'").run(); } catch (e) {}
|
||||
audit(req, 'assistant.models', 'kilo', 'сброс к встроенному');
|
||||
return res.json({ ok: true, reset: true });
|
||||
}
|
||||
const arr = Array.isArray(b.models) ? b.models : null;
|
||||
if (!arr) return res.status(400).json({ error: 'models[] обязателен' });
|
||||
const clean = [];
|
||||
for (const m of arr.slice(0, 40)) {
|
||||
const id = String((m && m.id) || '').trim().slice(0, 120);
|
||||
if (!id) continue;
|
||||
clean.push({ id, label: String((m && m.label) || id).trim().slice(0, 80), ctx: Number(m && m.ctx) || null, out: Number(m && m.out) || null });
|
||||
}
|
||||
if (!clean.length) return res.status(400).json({ error: 'пустой список' });
|
||||
db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_kilo_models', ?)").run(JSON.stringify(clean));
|
||||
audit(req, 'assistant.models', 'kilo', clean.length + ' моделей');
|
||||
res.json({ ok: true, count: clean.length });
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/health — прогнать проверку здоровья провайдеров сейчас */
|
||||
async function runHealth(req, res) {
|
||||
try {
|
||||
const r = await require('../assistant-health').runHealthCheck();
|
||||
audit(req, 'assistant.health', 'assistant', 'ручная проверка');
|
||||
res.json({ ok: true, result: r });
|
||||
} catch (e) { res.status(500).json({ ok: false, error: e.message || 'ошибка' }); }
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/active { id } — выбрать активного провайдера */
|
||||
function setActiveProvider(req, res) {
|
||||
const id = String((req.body && req.body.id) || '');
|
||||
@@ -1087,7 +1259,7 @@ async function testAssistant(req, res) {
|
||||
};
|
||||
}
|
||||
override.local = _aIsLocal(override.url);
|
||||
override.on = !!(override.key || override.local);
|
||||
override.on = !!(override.key || _aNoKey(override.url));
|
||||
const r = await a.pingLLM(override);
|
||||
// Успешный тест активного провайдера снимает устаревший флаг failover
|
||||
try { const activeId = _aset('assistant_active'); if (r && r.ok && (!b.id || b.id === activeId)) a.clearFailover(); } catch (e) {}
|
||||
@@ -1157,9 +1329,11 @@ module.exports = {
|
||||
getUsers, updateRole, getUserSessions, getAllSessions, getSessionDetail,
|
||||
clearUserSessions, deleteSession, updateUser, banUser, deleteUser,
|
||||
getFeatures, updateFeatures, getFreeStudentFeatures, updateFreeStudentFeatures,
|
||||
getResetPlan, resetSystem,
|
||||
getAuditLog, clearAuditLog, getErrorLog, clearErrorLog, getHealth, getMetrics,
|
||||
getSecurityLog, clearSecurityLog,
|
||||
getTopics, createTopic, updateTopic, deleteTopic,
|
||||
broadcast,
|
||||
getAssistant, saveAssistant, testAssistant, reindexTextbooks, saveProvider, deleteProvider, setActiveProvider, getProviderModels,
|
||||
scanModels, probeModel, applyModels, runHealth,
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ const db = require('../db/db');
|
||||
const { pushNotif } = require('../utils/notifications');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const { SESSION_MODES } = require('../constants');
|
||||
const AssignmentUtils = require('../../../frontend/js/assignment-utils.js'); // единый источник: тип/«сдано»
|
||||
|
||||
const VALID_ASSIGN_MODES = SESSION_MODES;
|
||||
|
||||
@@ -256,9 +257,9 @@ function teacherAssignments(req, res) {
|
||||
res.json(rows);
|
||||
}
|
||||
|
||||
/* ── GET /api/assignments/my ── student: all pending/done assignments ──── */
|
||||
function myAssignments(req, res) {
|
||||
const uid = req.user.id;
|
||||
/* Собрать все задания пользователя (классовые + личные) с вычисленным статусом.
|
||||
Переиспользуется в /assignments/my и в обзоре задолженностей класса. */
|
||||
function assignmentRowsForUser(uid) {
|
||||
const rows = db.prepare(`
|
||||
SELECT * FROM (
|
||||
SELECT a.id, a.title, a.subject_slug, a.mode, a.count, a.deadline, a.created_at,
|
||||
@@ -267,6 +268,7 @@ function myAssignments(req, res) {
|
||||
tb.slug AS textbook_slug, tb.title AS textbook_title, tb.color AS textbook_color, tb.para_count AS textbook_para_count,
|
||||
tp.paragraphs_read AS textbook_read,
|
||||
c.name AS class_name, c.id AS class_id, u.name AS teacher_name,
|
||||
a.created_by AS created_by,
|
||||
latest.session_id,
|
||||
ts.score, ts.total, ts.status AS session_status,
|
||||
ROUND(CAST(ts.score AS REAL) / ts.total * 100) AS percent,
|
||||
@@ -295,6 +297,7 @@ function myAssignments(req, res) {
|
||||
tb.slug AS textbook_slug, tb.title AS textbook_title, tb.color AS textbook_color, tb.para_count AS textbook_para_count,
|
||||
tp.paragraphs_read AS textbook_read,
|
||||
'Личное задание' AS class_name, 0 AS class_id, u.name AS teacher_name,
|
||||
a.created_by AS created_by,
|
||||
latest.session_id,
|
||||
ts.score, ts.total, ts.status AS session_status,
|
||||
ROUND(CAST(ts.score AS REAL) / ts.total * 100) AS percent,
|
||||
@@ -334,8 +337,78 @@ function myAssignments(req, res) {
|
||||
// Strip raw paragraphs_read JSON from response (not needed by client)
|
||||
delete r.textbook_read;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
res.json(rows);
|
||||
/* ── GET /api/assignments/my ── student: all pending/done assignments ──── */
|
||||
function myAssignments(req, res) {
|
||||
res.json(assignmentRowsForUser(req.user.id));
|
||||
}
|
||||
|
||||
/* ── GET /api/classes/:id/outstanding ── что «висит» у каждого ученика класса ──
|
||||
Учитель/админ видят по каждому ученику его НЕзакрытые задания (классовые + личные
|
||||
от этого учителя) со статусом: не начато / в процессе / на доработке / просрочено. */
|
||||
function classOutstanding(req, res) {
|
||||
const cid = req.params.id;
|
||||
const cls = db.prepare('SELECT id, name, teacher_id FROM classes WHERE id = ?').get(cid);
|
||||
if (!cls) return res.status(404).json({ error: 'Not found' });
|
||||
if (req.user.role !== 'admin' && cls.teacher_id !== req.user.id)
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
|
||||
const members = db.prepare(`
|
||||
SELECT u.id, u.name, u.email FROM class_members cm
|
||||
JOIN users u ON u.id = cm.user_id WHERE cm.class_id = ? ORDER BY u.name
|
||||
`).all(cid);
|
||||
|
||||
// Последняя сдача по (задание, ученик) в этом классе — для upload/file done-статуса.
|
||||
const subRows = db.prepare(`
|
||||
SELECT s.assignment_id, s.student_id, s.status
|
||||
FROM submissions s
|
||||
JOIN (SELECT assignment_id, student_id, MAX(id) AS mid FROM submissions
|
||||
WHERE class_id = ? GROUP BY assignment_id, student_id) last ON last.mid = s.id
|
||||
`).all(cid);
|
||||
const subMap = new Map();
|
||||
for (const s of subRows) subMap.set(s.assignment_id + '_' + s.student_id, s.status);
|
||||
|
||||
const now = Date.now();
|
||||
const cidNum = Number(cid);
|
||||
const RANK = { overdue: 0, revision: 1, in_progress: 2, not_started: 3 };
|
||||
|
||||
const students = members.map(m => {
|
||||
// Только задания ЭТОГО класса + личные, созданные учителем этого класса.
|
||||
const rows = assignmentRowsForUser(m.id).filter(r =>
|
||||
r.class_id === cidNum || (r.class_id === 0 && r.created_by === cls.teacher_id)
|
||||
);
|
||||
const pending = [];
|
||||
for (const r of rows) {
|
||||
const t = AssignmentUtils.type(r);
|
||||
const st = (t === 'upload' || t === 'file') ? subMap.get(r.id + '_' + m.id) : null;
|
||||
// Учительская семантика: любая сдача не на доработке = не долг (default opts).
|
||||
if (AssignmentUtils.isDone(r, st ? { status: st } : null)) continue;
|
||||
const overdue = r.deadline && new Date(r.deadline).getTime() < now;
|
||||
let status = overdue ? 'overdue' : 'not_started';
|
||||
if (st === 'revision') status = 'revision'; // вернули на доработку
|
||||
else if (t === 'test' && r.session_status === 'in_progress') status = 'in_progress';
|
||||
pending.push({
|
||||
assignment_id: r.id, title: r.title, type: t, deadline: r.deadline,
|
||||
status, is_homework: r.is_homework ? 1 : 0,
|
||||
scope: r.class_id === cidNum ? 'class' : 'direct',
|
||||
});
|
||||
}
|
||||
pending.sort((a, b) => (RANK[a.status] - RANK[b.status]) ||
|
||||
((a.deadline ? new Date(a.deadline).getTime() : Infinity) -
|
||||
(b.deadline ? new Date(b.deadline).getTime() : Infinity)));
|
||||
const counts = { total: pending.length, overdue: 0, in_progress: 0, not_started: 0, revision: 0 };
|
||||
pending.forEach(p => { counts[p.status]++; });
|
||||
return { id: m.id, name: m.name, email: m.email, pending, counts };
|
||||
});
|
||||
|
||||
const summary = {
|
||||
students_total: members.length,
|
||||
debtors: students.filter(s => s.counts.total > 0).length,
|
||||
overdue: students.reduce((a, s) => a + s.counts.overdue, 0),
|
||||
};
|
||||
res.json({ className: cls.name, summary, students });
|
||||
}
|
||||
|
||||
/* Parse "1-5", "1,3,7", "1-3,5,7-9" → [1,2,3,4,5] or [1,3,7] etc.
|
||||
@@ -732,6 +805,7 @@ module.exports = {
|
||||
deleteAssignment,
|
||||
teacherAssignments,
|
||||
myAssignments,
|
||||
classOutstanding,
|
||||
startAssignment,
|
||||
assignmentResults,
|
||||
assignmentQuestionStats,
|
||||
|
||||
@@ -339,6 +339,8 @@ function searchFaq(q, n) {
|
||||
* на ENV и дефолты. Если ключа нет и URL не локальный — работаем как FAQ. */
|
||||
function _setting(k) { try { const r = db.prepare('SELECT value FROM app_settings WHERE key = ?').get(k); return r && r.value != null ? r.value : null; } catch (e) { return null; } }
|
||||
function _isLocal(url) { return /\/\/(localhost|127\.0\.0\.1)/.test(url || ''); }
|
||||
// Шлюзы с бесплатным инференсом БЕЗ ключа (наряду с localhost): ключ не обязателен.
|
||||
function _noKeyNeeded(url) { return _isLocal(url) || /\/\/[^/]*\bpollinations\.ai\b/i.test(url || ''); }
|
||||
|
||||
/* Список провайдеров (несколько ключей/моделей). Хранится JSON в app_settings.
|
||||
* Если списка нет — синтезируем из legacy-настроек/ENV, чтобы ничего не сломать. */
|
||||
@@ -357,7 +359,7 @@ function _providers() {
|
||||
/* Конфиги в порядке использования: активный первым, затем остальные с ключом
|
||||
* (для авто-перехвата при лимите/ошибке). */
|
||||
function providersOrdered() {
|
||||
const arr = _providers().filter(p => p && (p.key || _isLocal(p.url)));
|
||||
const arr = _providers().filter(p => p && (p.key || _noKeyNeeded(p.url)));
|
||||
const activeId = _setting('assistant_active');
|
||||
const active = arr.filter(p => p.id === activeId);
|
||||
const rest = arr.filter(p => p.id !== activeId);
|
||||
@@ -451,11 +453,69 @@ async function callLLMFailover(messages, maxTokens) {
|
||||
return last;
|
||||
}
|
||||
|
||||
/* Потоковый вызов OpenAI-совместимого chat/completions (stream:true).
|
||||
* onDelta(piece) — на каждый кусок текста. Возвращает { text, any, error }. */
|
||||
async function callLLMStream(messages, maxTokens, cfg, onDelta) {
|
||||
if (typeof fetch !== 'function' || !cfg.on) return { text: null, any: false, error: 'off' };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 60000); // стриминг длиннее обычного
|
||||
try {
|
||||
const r = await fetch(cfg.url, {
|
||||
method: 'POST',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json' }, cfg.key ? { Authorization: `Bearer ${cfg.key}` } : {}),
|
||||
body: JSON.stringify({ model: cfg.model, temperature: 0.3, max_tokens: maxTokens || 1200, messages, stream: true }),
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
if (!r.ok) return { text: null, any: false, error: r.status === 429 ? 'rate_limit' : 'http', status: r.status };
|
||||
if (!r.body) return { text: null, any: false, error: 'empty' };
|
||||
const dec = new TextDecoder();
|
||||
let buf = '', full = '', any = false;
|
||||
for await (const chunk of r.body) {
|
||||
buf += dec.decode(chunk, { stream: true });
|
||||
let nl;
|
||||
while ((nl = buf.indexOf('\n')) >= 0) {
|
||||
const line = buf.slice(0, nl).trim(); buf = buf.slice(nl + 1);
|
||||
if (!line.startsWith('data:')) continue;
|
||||
const data = line.slice(5).trim();
|
||||
if (data === '[DONE]') return { text: full || null, any, error: full ? null : 'empty' };
|
||||
try {
|
||||
const j = JSON.parse(data);
|
||||
const d = j.choices && j.choices[0] && j.choices[0].delta;
|
||||
const piece = d && d.content;
|
||||
if (piece) { full += piece; any = true; onDelta(piece); }
|
||||
} catch (e) { /* частичный/служебный кусок — пропускаем */ }
|
||||
}
|
||||
}
|
||||
return { text: full || null, any, error: full ? null : 'empty' };
|
||||
} catch (e) { return { text: null, any: false, error: e.name === 'AbortError' ? 'timeout' : 'network' }; }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
/* Стриминг с перебором провайдеров. Failover возможен ТОЛЬКО до первого куска;
|
||||
* как только клиенту ушёл текст (any) — остаёмся на этом провайдере. */
|
||||
async function callLLMStreamFailover(messages, maxTokens, onDelta) {
|
||||
const cfgs = providersOrdered();
|
||||
if (!cfgs.length) return { text: null, error: 'off' };
|
||||
let firstErr = null;
|
||||
for (let i = 0; i < cfgs.length; i++) {
|
||||
const res = await callLLMStream(messages, maxTokens, cfgs[i], onDelta);
|
||||
if (i === 0) firstErr = res.error;
|
||||
if (res.text) {
|
||||
if (i === 0) _clearFailover(); else _recordFailover(cfgs[0], cfgs[i], firstErr);
|
||||
return res;
|
||||
}
|
||||
if (res.any) return res; // часть уже улетела клиенту — переключиться нельзя
|
||||
if (!_RETRYABLE[res.error]) break;
|
||||
}
|
||||
if (_RETRYABLE[firstErr]) _recordFailover(cfgs[0], null, firstErr);
|
||||
return { text: null, error: firstErr || 'error' };
|
||||
}
|
||||
|
||||
/* Тест-пинг для админки: подробный статус (status/ошибка/пример ответа). */
|
||||
async function pingLLM(override) {
|
||||
const cfg = override || llmConfig();
|
||||
if (!cfg.url) return { ok: false, error: 'URL не задан' };
|
||||
if (!cfg.key && !/\/\/(localhost|127\.0\.0\.1)/.test(cfg.url)) return { ok: false, error: 'Ключ не задан' };
|
||||
if (!cfg.key && !_noKeyNeeded(cfg.url)) return { ok: false, error: 'Ключ не задан' };
|
||||
if (typeof fetch !== 'function') return { ok: false, error: 'fetch недоступен' };
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 15000);
|
||||
@@ -496,7 +556,12 @@ const META_RE = new RegExp('(' + _SELF + '[\\sа-яёa-z0-9,?!.-]{0,25}' + _TERM
|
||||
'|на\\s+ч[её]м\\s+ты\\s+(?:работа|сдела|постро|основ)|кто\\s+тебя\\s+(?:сделал|создал|обуч|разработ|написал)|систем[а-яё]*\\s+промпт|what\\s+model\\s+are\\s+you|which\\s+(?:ai\\s+)?model|your\\s+system\\s+prompt)', 'i');
|
||||
const META_ANSWER = 'Я — Квантик, помощник LearnSpace. Помогаю с учёбой и навигацией по платформе. Давай вернёмся к делу — что объяснить или подсказать?';
|
||||
|
||||
async function askModel(q, hits, context, history, role, mode, mem) {
|
||||
// Анти-чит: явная просьба «сделай за меня» (а не «помоги разобраться»).
|
||||
const _CHEAT_RE = /за\s+меня|вместо\s+меня|do\s+my\s+homework|(сделай|реши|выполни|напиши)\s+([а-яёА-ЯЁ]+\s+)?(дз|домашк|контрольн)/i;
|
||||
function _socraticOn() { return _setting('assistant_socratic') === '1'; }
|
||||
|
||||
// Сборка messages+cap для модели — общая для обычного и стримингового ответа.
|
||||
function buildAskMessages(q, hits, context, history, role, mode, mem, socratic) {
|
||||
const ref = hits.map((h, i) => `${i + 1}. ${h.q}\n${h.a}${h.url ? ` (раздел: ${h.url})` : ''}`).join('\n') || '(пусто)';
|
||||
const user = (context ? `Контекст (опирайся на него, если относится к вопросу):\n${context}\n\n` : '') +
|
||||
`Справка по платформе:\n${ref}\n\nВопрос: ${q}`;
|
||||
@@ -510,15 +575,33 @@ async function askModel(q, hits, context, history, role, mode, mem) {
|
||||
sys += ' РЕЖИМ ПОДСКАЗКИ: дай ТОЛЬКО наводящую подсказку или следующий шаг к решению. Не давай готовый ответ — пусть ученик додумает сам.';
|
||||
} else if (mode === 'check') {
|
||||
sys += ' РЕЖИМ ПРОВЕРКИ: ученик прислал своё решение. Скажи, верно оно или нет, и укажи КОНКРЕТНО, где ошибка (если есть). Не выдавай сразу полный правильный ответ — дай шанс исправить.';
|
||||
} else if (socratic) {
|
||||
// Сократический режим (для учеников): теория — полно, но задачи не решаем «под ключ».
|
||||
sys += ' СОКРАТИЧЕСКИЙ РЕЖИМ: понятия, определения и теорию объясняй полно и по существу. ' +
|
||||
'Но если просят РЕШИТЬ конкретную задачу/пример/уравнение или «сделать» задание — НЕ выдавай готовое решение и итоговый ответ. ' +
|
||||
'Вместо этого назови нужный метод/формулу, разбери первый шаг и задай наводящий вопрос, предложи ученику продолжить самому. ' +
|
||||
'Если ученик пришлёт свой шаг или ответ — проверь и мягко направь дальше. Будь доброжелателен, подбадривай.';
|
||||
}
|
||||
const msgs = [{ role: 'system', content: sys }];
|
||||
(history || []).forEach(m => { if (m && (m.role === 'user' || m.role === 'assistant') && m.content) msgs.push({ role: m.role, content: String(m.content).slice(0, 1500) }); });
|
||||
msgs.push({ role: 'user', content: user });
|
||||
// подсказка короткая; ответ/проверка — длиннее, чтобы пошаговое решение с формулами не обрезалось на середине
|
||||
const cap = mode === 'hint' ? 320 : (mode === 'check' ? 900 : 1200);
|
||||
return { msgs, cap };
|
||||
}
|
||||
|
||||
async function askModel(q, hits, context, history, role, mode, mem, socratic) {
|
||||
const { msgs, cap } = buildAskMessages(q, hits, context, history, role, mode, mem, socratic);
|
||||
return callLLMFailover(msgs, cap);
|
||||
}
|
||||
|
||||
// Сократический режим включается для УЧЕНИКА: если включён тумблер ИЛИ явная просьба «сделай за меня».
|
||||
function _socraticFor(role, mode, q) {
|
||||
if (role && role !== 'student') return false; // учителям/админам не ограничиваем
|
||||
if (mode !== 'answer') return false; // hint/check уже наводящие
|
||||
return _socraticOn() || _CHEAT_RE.test(q || '');
|
||||
}
|
||||
|
||||
/* ── POST /api/assistant/ask { q, context?, history? } ── «Спроси Квантика» ─
|
||||
* Грунтуем ответ топ-FAQ (+ опц. контекст страницы + история диалога). Если
|
||||
* LLM настроена — её ответ (source:'model'), иначе FAQ (source:'faq'). */
|
||||
@@ -551,8 +634,9 @@ async function ask(req, res) {
|
||||
let context = pageCtx;
|
||||
if (rag.text) context = (context ? context + '\n\n' : '') + 'Из учебников:\n' + rag.text;
|
||||
|
||||
const socratic = _socraticFor(req.user && req.user.role, mode, q);
|
||||
let r = { text: null, error: 'network' };
|
||||
try { r = await askModel(q, hits, context, history, req.user && req.user.role, mode, mem); } catch (e) { r = { text: null, error: 'network' }; }
|
||||
try { r = await askModel(q, hits, context, history, req.user && req.user.role, mode, mem, socratic); } catch (e) { r = { text: null, error: 'network' }; }
|
||||
const answer = r && r.text;
|
||||
|
||||
if (answer) {
|
||||
@@ -572,6 +656,66 @@ async function ask(req, res) {
|
||||
res.json({ source: 'faq', answer: null, answers: faqJson, sources: [] });
|
||||
}
|
||||
|
||||
/* ── POST /api/assistant/ask/stream ── то же, что ask, но ответ модели стримится
|
||||
* по SSE (event: meta|delta|done). Быстрые пути (FAQ/кэш/мета) отдаются одним done. */
|
||||
async function askStream(req, res) {
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no'); // не буферизовать за прокси
|
||||
if (res.flushHeaders) res.flushHeaders();
|
||||
const sse = (event, data) => { try { res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); } catch (e) {} };
|
||||
|
||||
const q = String((req.body && req.body.q) || '').trim().slice(0, 500);
|
||||
if (!q || q.length < 2) { sse('done', { source: 'faq', answer: null, answers: [] }); return res.end(); }
|
||||
if (META_RE.test(q)) { sse('delta', { t: META_ANSWER }); sse('done', { source: 'model', answers: [], sources: [] }); return res.end(); }
|
||||
const pageCtx = String((req.body && req.body.context) || '').slice(0, 4000);
|
||||
const mode = ['hint', 'check'].includes(req.body && req.body.mode) ? req.body.mode : 'answer';
|
||||
let history = (req.body && req.body.history);
|
||||
history = Array.isArray(history) ? history.slice(-6) : [];
|
||||
const hits = searchFaq(q, 3);
|
||||
const faqJson = hits.map(h => ({ id: h.id, q: h.q, a: h.a, url: h.url || null }));
|
||||
sse('meta', { answers: faqJson });
|
||||
|
||||
if (!providersOrdered().length) { bumpUsage('faq'); sse('done', { source: 'faq', answer: null, answers: faqJson, sources: [] }); return res.end(); }
|
||||
|
||||
const rag = ragContext(q);
|
||||
const mem = _memoryBlock(req.user.id);
|
||||
const cacheable = mode === 'answer' && !pageCtx && !history.length && !mem;
|
||||
const qhash = q.toLowerCase().replace(/\s+/g, ' ').trim();
|
||||
if (cacheable) {
|
||||
try {
|
||||
const c = db.prepare("SELECT answer FROM assistant_cache WHERE qhash = ? AND created_at > datetime('now','-7 days')").get(qhash);
|
||||
if (c) { bumpUsage('cache_hits'); sse('delta', { t: c.answer }); sse('done', { source: 'model', answers: faqJson, sources: rag.sources, cached: true }); return res.end(); }
|
||||
} catch (e) {}
|
||||
}
|
||||
if (rag.sources && rag.sources.length) sse('meta', { sources: rag.sources });
|
||||
|
||||
let context = pageCtx;
|
||||
if (rag.text) context = (context ? context + '\n\n' : '') + 'Из учебников:\n' + rag.text;
|
||||
const socratic = _socraticFor(req.user && req.user.role, mode, q);
|
||||
const { msgs, cap } = buildAskMessages(q, hits, context, history, req.user && req.user.role, mode, mem, socratic);
|
||||
|
||||
let full = '';
|
||||
let r = { text: null, error: 'network' };
|
||||
try { r = await callLLMStreamFailover(msgs, cap, (piece) => { full += piece; sse('delta', { t: piece }); }); }
|
||||
catch (e) { r = { text: null, error: 'network' }; }
|
||||
|
||||
const answer = (r && r.text) || full;
|
||||
if (answer) {
|
||||
bumpUsage('model_calls');
|
||||
if (cacheable) { try { db.prepare("INSERT OR REPLACE INTO assistant_cache (qhash, answer, created_at) VALUES (?, ?, datetime('now'))").run(qhash, answer); } catch (e) {} }
|
||||
if (_setting('assistant_memory') !== '0' && (mode === 'check' || history.length >= 4)) _extractMemory(req.user.id, q, answer);
|
||||
sse('done', { source: 'model', answers: faqJson, sources: rag.sources });
|
||||
return res.end();
|
||||
}
|
||||
bumpUsage('faq');
|
||||
if (r && r.error === 'rate_limit') sse('done', { source: 'limit', answer: 'Сейчас слишком много запросов к ИИ за короткое время — подожди минутку и спроси снова. Память диалога не потеряется.', answers: faqJson, sources: [] });
|
||||
else if (r && (r.error === 'timeout' || r.error === 'network' || r.error === 'http')) sse('done', { source: 'error', answer: 'Не получилось обратиться к ИИ. Попробуй ещё раз чуть позже.', answers: faqJson, sources: [] });
|
||||
else sse('done', { source: 'faq', answer: null, answers: faqJson, sources: [] });
|
||||
res.end();
|
||||
}
|
||||
|
||||
/* ── POST /api/assistant/feedback { rating, q? } ── лайк/дизлайк ответа ── */
|
||||
function feedback(req, res) {
|
||||
const rating = (req.body && req.body.rating) === 1 ? 1 : ((req.body && req.body.rating) === -1 ? -1 : 0);
|
||||
@@ -621,4 +765,50 @@ async function flashcardsFromText(req, res) {
|
||||
res.json({ title, cards });
|
||||
}
|
||||
|
||||
module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, getMemory, clearMemory, getStudentProfile, llmConfig, pingLLM, clearFailover: _clearFailover, callLLMFailover };
|
||||
/* ── POST /api/assistant/questions { text, count? } ── учитель: сгенерировать
|
||||
* тестовые вопросы (single-choice) из темы/текста для банка вопросов. */
|
||||
async function questionsFromText(req, res) {
|
||||
if (!providersOrdered().length) return res.status(503).json({ error: 'LLM не настроена' });
|
||||
const text = String((req.body && req.body.text) || '').trim().slice(0, 6000);
|
||||
let count = Number(req.body && req.body.count);
|
||||
count = Number.isFinite(count) ? Math.max(3, Math.min(10, Math.round(count))) : 5;
|
||||
if (text.length < 3) return res.status(400).json({ error: 'Введите тему или текст' });
|
||||
const sys = 'Ты составляешь тестовые вопросы с выбором одного верного ответа для школьников. ' +
|
||||
'Если дан учебный текст/параграф — делай вопросы СТРОГО по нему; если дана короткая тема — раскрой её по школьной программе. ' +
|
||||
'Верни СТРОГО JSON-массив из ' + count + ' объектов вида ' +
|
||||
'{"q":"текст вопроса","options":["вариант1","вариант2","вариант3","вариант4"],"correct":0,"explanation":"кратко, почему верен"}. ' +
|
||||
'РОВНО 4 варианта; correct — индекс правильного (0..3); ровно один правильный. ' +
|
||||
'По-русски, формулы в LaTeX между $...$. Никакого текста вне JSON, без markdown.';
|
||||
let rr;
|
||||
try { rr = await callLLMFailover([{ role: 'system', content: sys }, { role: 'user', content: text }], 2200); }
|
||||
catch (e) { return res.status(502).json({ error: 'Не удалось обратиться к ИИ' }); }
|
||||
const raw = rr && rr.text;
|
||||
let questions = [];
|
||||
if (raw) {
|
||||
let s = raw.replace(/```(?:json)?/gi, '').trim();
|
||||
const a = s.indexOf('[');
|
||||
if (a >= 0) {
|
||||
const b = s.lastIndexOf(']');
|
||||
if (b > a) s = s.slice(a, b + 1);
|
||||
else { const last = s.lastIndexOf('}'); s = last > a ? s.slice(a, last + 1) + ']' : ''; }
|
||||
}
|
||||
try {
|
||||
const arr = JSON.parse(s);
|
||||
if (Array.isArray(arr)) {
|
||||
questions = arr
|
||||
.filter(x => x && x.q && Array.isArray(x.options) && x.options.length >= 2)
|
||||
.slice(0, count + 2)
|
||||
.map(x => {
|
||||
const opts = x.options.slice(0, 6).map(o => String(o).slice(0, 300)).filter(Boolean);
|
||||
let correct = Number(x.correct); if (!Number.isInteger(correct) || correct < 0 || correct >= opts.length) correct = 0;
|
||||
return { q: String(x.q).slice(0, 1000), options: opts, correct, explanation: String(x.explanation || '').slice(0, 600) };
|
||||
})
|
||||
.filter(x => x.options.length >= 2);
|
||||
}
|
||||
} catch (e) { /* не-JSON */ }
|
||||
}
|
||||
if (!questions.length) return res.status(502).json({ error: 'Не удалось сгенерировать вопросы' });
|
||||
res.json({ questions });
|
||||
}
|
||||
|
||||
module.exports = { getContext, markSeen, dismiss, setSettings, ask, askStream, flashcardsFromText, questionsFromText, feedback, getMemory, clearMemory, getStudentProfile, llmConfig, pingLLM, clearFailover: _clearFailover, callLLMFailover };
|
||||
|
||||
@@ -41,6 +41,15 @@ function createSession(req, res) {
|
||||
|
||||
const session = db.prepare('SELECT * FROM classroom_sessions WHERE id=?').get(sessionId);
|
||||
emitToSession(sessionId, { type: 'classroom_started', sessionId, title, classId: class_id || null, teacherName: teacher.name });
|
||||
// Баннер «идёт онлайн-урок» на дашбордах — через SSE-канал (доска работает по WS,
|
||||
// дашборд по SSE, поэтому нужен отдельный сигнал ученикам класса / приглашённым / учителю).
|
||||
try {
|
||||
const sse = require('../../sse');
|
||||
const payload = { type: 'classroom_live', state: 'started', sessionId, title, classId: class_id || null };
|
||||
if (class_id) sse.emitToClass(class_id, payload);
|
||||
else if (user_ids) for (const uid of user_ids) sse.emit(uid, payload);
|
||||
sse.emit(teacher.id, payload);
|
||||
} catch { /* SSE недоступен — не критично */ }
|
||||
res.json(session);
|
||||
}
|
||||
|
||||
@@ -74,6 +83,17 @@ function endSession(req, res) {
|
||||
db.prepare('DELETE FROM classroom_draw_permissions WHERE session_id=?').run(sessionId);
|
||||
db.prepare('DELETE FROM classroom_muted WHERE session_id=?').run(sessionId);
|
||||
emitToSession(sessionId, { type: 'classroom_ended', sessionId });
|
||||
// Снять баннер «идёт онлайн-урок» с дашбордов (SSE-канал).
|
||||
try {
|
||||
const sse = require('../../sse');
|
||||
const payload = { type: 'classroom_live', state: 'ended', sessionId };
|
||||
if (session.class_id) sse.emitToClass(session.class_id, payload);
|
||||
else {
|
||||
const invited = db.prepare('SELECT user_id FROM classroom_invites WHERE session_id=?').all(sessionId);
|
||||
for (const r of invited) sse.emit(r.user_id, payload);
|
||||
}
|
||||
sse.emit(session.teacher_id, payload);
|
||||
} catch { /* SSE недоступен — не критично */ }
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
/* clientErrorController — приём ошибок из браузера пользователя.
|
||||
Пишем в общий error_log с level='client', чтобы они появились в админ-вкладке «Ошибки».
|
||||
Запись не должна ронять запрос — любые сбои глушим. */
|
||||
const db = require('../db/db');
|
||||
|
||||
const MAX_MSG = 1000, MAX_STACK = 4000, MAX_ROUTE = 400;
|
||||
const clamp = (v, n) => (v == null ? null : String(v).slice(0, n));
|
||||
|
||||
function report(req, res) {
|
||||
const b = req.body || {};
|
||||
const message = (clamp(b.message, MAX_MSG) || '').trim();
|
||||
if (!message) return res.status(400).json({ error: 'message required' });
|
||||
|
||||
const kind = b.kind === 'unhandledrejection' ? 'rejection' : 'error';
|
||||
const route = clamp(b.url || b.route, MAX_ROUTE);
|
||||
let stack = clamp(b.stack, MAX_STACK);
|
||||
// если стека нет — собираем источник:строка:колонка
|
||||
if (!stack && (b.source || b.line)) stack = `${b.source || ''}:${b.line || ''}:${b.col || ''}`;
|
||||
|
||||
try {
|
||||
db.prepare(
|
||||
'INSERT INTO error_log (level, message, stack, route, method, user_id) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run('client', message, stack, route, kind, req.user.id);
|
||||
} catch { /* лог не должен ломать ответ */ }
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = { report };
|
||||
@@ -1,5 +1,6 @@
|
||||
const db = require('../db/db');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const prepTracks = require('../services/prepTracks');
|
||||
|
||||
/* ── валидация URL картинки ────────────────────────────────────────────────
|
||||
Принимаем ТОЛЬКО свои загруженные файлы (/uploads/flashcards/<file>) —
|
||||
@@ -110,7 +111,10 @@ function deckAccess(deckId, user) {
|
||||
(a.type = 'class' AND a.target_id IN (SELECT class_id FROM class_members WHERE user_id = ?))
|
||||
) LIMIT 1
|
||||
`).get(deckId, user.id, user.id);
|
||||
return { deck, owner: false, canRead: !!shared, canEdit: false };
|
||||
// Колода открыта мастер-флагом направления: collection ∈ коллекций треков ученика.
|
||||
const byTrack = !shared && deck.collection &&
|
||||
prepTracks.studentCollections(user.id).has(deck.collection);
|
||||
return { deck, owner: false, canRead: !!shared || !!byTrack, canEdit: false };
|
||||
}
|
||||
|
||||
/* due_count карты колоды для пользователя: learning/review к повтору (due_at<=now)
|
||||
@@ -140,6 +144,10 @@ function deckDueCount(deckId, uid) {
|
||||
для UI: общие открываются только на чтение и изучение. */
|
||||
function listDecks(req, res) {
|
||||
const uid = req.user.id;
|
||||
// Коллекции, открытые мастер-флагом направления (динамически, без правил доступа).
|
||||
const cols = [...prepTracks.studentCollections(uid)];
|
||||
const colClause = cols.length
|
||||
? `OR d.collection IN (${cols.map(() => '?').join(',')})` : '';
|
||||
const decks = db.prepare(`
|
||||
SELECT d.*, u.name AS owner_name,
|
||||
CASE WHEN d.user_id = ? THEN 1 ELSE 0 END AS can_edit,
|
||||
@@ -152,8 +160,9 @@ function listDecks(req, res) {
|
||||
OR EXISTS (SELECT 1 FROM flashcard_deck_access a
|
||||
JOIN class_members cm ON cm.class_id = a.target_id AND cm.user_id = ?
|
||||
WHERE a.deck_id = d.id AND a.type = 'class')
|
||||
${colClause}
|
||||
ORDER BY shared ASC, d.created_at DESC
|
||||
`).all(uid, uid, uid, uid, uid);
|
||||
`).all(uid, uid, uid, uid, uid, ...cols);
|
||||
|
||||
const cardStmt = db.prepare(`SELECT COUNT(*) AS n FROM flashcard_cards WHERE deck_id = ?`);
|
||||
for (const d of decks) {
|
||||
|
||||
@@ -542,6 +542,7 @@ function onClassJoined(userId) {
|
||||
}
|
||||
|
||||
function onLabExperiment(userId, reactionsDiscovered) {
|
||||
if (!isGamificationEnabled()) return; // master kill-switch
|
||||
stmts.incrLabExp.run(userId);
|
||||
if (reactionsDiscovered > 0) stmts.incrLabReact.run(reactionsDiscovered, userId);
|
||||
awardXP(userId, 15, 'lab_experiment');
|
||||
@@ -650,6 +651,7 @@ function ensureChallenges(userId) {
|
||||
}
|
||||
|
||||
function updateChallenges(userId, score, total, subjectSlug, topicId) {
|
||||
if (!isGamificationEnabled()) return; // master kill-switch
|
||||
const week = _currentWeek();
|
||||
const pct = total > 0 ? Math.round(score / total * 100) : 0;
|
||||
const challenges = stmts.getOpenChallenges.all(userId, week);
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
'use strict';
|
||||
/* prepController — управление флагом «подготовки к направлению» (мастер-флаг).
|
||||
*
|
||||
* Флаг student_prep(user_id, track) открывает ученику весь контент трека
|
||||
* (карточки + курс + пробники) — резолв в services/prepTracks.js + contentAccess.js.
|
||||
*
|
||||
* Управление: учитель — СВОИМ ученикам (из своих классов или персональным),
|
||||
* админ — любым. Read own-статус доступен самому ученику (GET /me).
|
||||
*/
|
||||
const db = require('../db/db');
|
||||
const prepTracks = require('../services/prepTracks');
|
||||
|
||||
/* Ученик «свой» для учителя: член одного из его классов ИЛИ персональный ученик. */
|
||||
function canManageStudent(user, studentId) {
|
||||
if (user.role === 'admin') return true;
|
||||
if (user.role !== 'teacher') return false;
|
||||
return !!db.prepare(`
|
||||
SELECT 1 FROM class_members cm JOIN classes c ON c.id = cm.class_id
|
||||
WHERE cm.user_id = ? AND c.teacher_id = ?
|
||||
UNION
|
||||
SELECT 1 FROM teacher_students WHERE student_id = ? AND teacher_id = ?
|
||||
LIMIT 1
|
||||
`).get(studentId, user.id, studentId, user.id);
|
||||
}
|
||||
|
||||
/* Класс «свой» для учителя (admin — любой). */
|
||||
function canManageClass(user, classId) {
|
||||
if (user.role === 'admin') return true;
|
||||
if (user.role !== 'teacher') return false;
|
||||
return !!db.prepare(`SELECT 1 FROM classes WHERE id = ? AND teacher_id = ?`).get(classId, user.id);
|
||||
}
|
||||
|
||||
/* ── GET /api/prep/tracks — список доступных направлений (для UI-меток) ── */
|
||||
function listTracks(req, res) {
|
||||
res.json({ tracks: prepTracks.listTracks() });
|
||||
}
|
||||
|
||||
/* ── GET /api/prep/me — мои треки (ученик видит свой статус) ── */
|
||||
function myTracks(req, res) {
|
||||
const rows = db.prepare(`SELECT track FROM student_prep WHERE user_id = ?`).all(req.user.id);
|
||||
res.json({ tracks: rows.map(r => r.track).filter(prepTracks.isTrack) });
|
||||
}
|
||||
|
||||
/* ── GET /api/prep/student/:id — треки ученика (учитель своего / админ) ── */
|
||||
function studentTracks(req, res) {
|
||||
const studentId = Number(req.params.id) || 0;
|
||||
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
const rows = db.prepare(`SELECT track FROM student_prep WHERE user_id = ?`).all(studentId);
|
||||
res.json({ tracks: rows.map(r => r.track).filter(prepTracks.isTrack) });
|
||||
}
|
||||
|
||||
/* ── POST /api/prep/student/:id { track } — включить флаг ── */
|
||||
function setStudent(req, res) {
|
||||
const studentId = Number(req.params.id) || 0;
|
||||
const track = String(req.body.track || '');
|
||||
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
|
||||
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
// Ставим флаг только реально существующему ученику.
|
||||
const stu = db.prepare(`SELECT id, role FROM users WHERE id = ?`).get(studentId);
|
||||
if (!stu) return res.status(404).json({ error: 'Ученик не найден' });
|
||||
db.prepare(`INSERT OR IGNORE INTO student_prep (user_id, track, created_by) VALUES (?,?,?)`)
|
||||
.run(studentId, track, req.user.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── DELETE /api/prep/student/:id?track= — снять флаг ── */
|
||||
function unsetStudent(req, res) {
|
||||
const studentId = Number(req.params.id) || 0;
|
||||
const track = String(req.query.track || '');
|
||||
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
|
||||
if (!canManageStudent(req.user, studentId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
db.prepare(`DELETE FROM student_prep WHERE user_id = ? AND track = ?`).run(studentId, track);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* ── GET /api/prep/class/:id?track= — статус флага у членов класса ── */
|
||||
function classStatus(req, res) {
|
||||
const classId = Number(req.params.id) || 0;
|
||||
const track = String(req.query.track || '');
|
||||
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
|
||||
if (!canManageClass(req.user, classId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
const rows = db.prepare(`
|
||||
SELECT u.id, u.name,
|
||||
CASE WHEN sp.id IS NULL THEN 0 ELSE 1 END AS prep
|
||||
FROM class_members cm
|
||||
JOIN users u ON u.id = cm.user_id
|
||||
LEFT JOIN student_prep sp ON sp.user_id = u.id AND sp.track = ?
|
||||
WHERE cm.class_id = ?
|
||||
ORDER BY u.name
|
||||
`).all(track, classId);
|
||||
res.json({ students: rows });
|
||||
}
|
||||
|
||||
/* ── POST /api/prep/class/:id { track, on } — массово по классу ── */
|
||||
function setClass(req, res) {
|
||||
const classId = Number(req.params.id) || 0;
|
||||
const track = String(req.body.track || '');
|
||||
const on = !!req.body.on;
|
||||
if (!prepTracks.isTrack(track)) return res.status(400).json({ error: 'Неизвестный трек' });
|
||||
if (!canManageClass(req.user, classId)) return res.status(403).json({ error: 'Forbidden' });
|
||||
const members = db.prepare(`SELECT user_id FROM class_members WHERE class_id = ?`).all(classId);
|
||||
const ins = db.prepare(`INSERT OR IGNORE INTO student_prep (user_id, track, created_by) VALUES (?,?,?)`);
|
||||
const del = db.prepare(`DELETE FROM student_prep WHERE user_id = ? AND track = ?`);
|
||||
const run = db.transaction(() => {
|
||||
for (const m of members) on ? ins.run(m.user_id, track, req.user.id) : del.run(m.user_id, track);
|
||||
});
|
||||
run();
|
||||
res.json({ ok: true, count: members.length });
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
listTracks, myTracks, studentTracks, setStudent, unsetStudent, classStatus, setClass,
|
||||
};
|
||||
@@ -1,5 +1,17 @@
|
||||
const db = require('../db/db');
|
||||
|
||||
/* Вопросы теста без зафиксированного правильного ответа (нет верного варианта И
|
||||
* нет correct_text). matching исключаем (там ответ — пары match_pair).
|
||||
* Такой вопрос нельзя оценить → не пускаем тест к ученикам. */
|
||||
function unanswerableInTest(testId) {
|
||||
return db.prepare(`
|
||||
SELECT tq.question_id AS id FROM test_questions tq JOIN questions q ON q.id = tq.question_id
|
||||
WHERE tq.test_id = ? AND q.type <> 'matching'
|
||||
AND (q.correct_text IS NULL OR TRIM(q.correct_text) = '')
|
||||
AND NOT EXISTS (SELECT 1 FROM options o WHERE o.question_id = q.id AND o.is_correct = 1)
|
||||
`).all(testId).map(r => r.id);
|
||||
}
|
||||
|
||||
/* ── GET /api/tests ─────────────────────────────────────────────────────── */
|
||||
function list(req, res) {
|
||||
const { subject } = req.query;
|
||||
@@ -7,13 +19,16 @@ function list(req, res) {
|
||||
const args = [];
|
||||
let where = '1=1';
|
||||
if (subject) { where += ' AND t.subject_slug = ?'; args.push(subject); }
|
||||
if (role !== 'admin') { where += ' AND t.created_by = ?'; args.push(uid); }
|
||||
const isStudent = role === 'student' || role === 'free_student';
|
||||
// Ученик видит каталог тестов, помеченных доступными; учитель — только свои; админ — все.
|
||||
if (isStudent) { where += ' AND t.available_to_students = 1'; }
|
||||
else if (role !== 'admin') { where += ' AND t.created_by = ?'; args.push(uid); }
|
||||
// Экзаменационные варианты — это служебные строки в tests (см. import-exam9.js),
|
||||
// не показываем их во вкладке «Тесты (шаблоны)» админки.
|
||||
where += ' AND t.id NOT IN (SELECT test_id FROM exam9_variant_tests)';
|
||||
|
||||
const rows = db.prepare(`
|
||||
SELECT t.id, t.title, t.subject_slug, t.description, t.created_at,
|
||||
let rows = db.prepare(`
|
||||
SELECT t.id, t.title, t.subject_slug, t.description, t.created_at, t.available_to_students,
|
||||
u.name AS creator_name,
|
||||
COUNT(tq.question_id) AS question_count
|
||||
FROM tests t
|
||||
@@ -22,18 +37,19 @@ function list(req, res) {
|
||||
WHERE ${where}
|
||||
GROUP BY t.id ORDER BY t.created_at DESC
|
||||
`).all(...args);
|
||||
if (isStudent) rows = rows.filter(r => r.question_count > 0); // пустые тесты ученику не показываем
|
||||
res.json(rows);
|
||||
}
|
||||
|
||||
/* ── POST /api/tests ─────────────────────────────────────────────────────── */
|
||||
function create(req, res) {
|
||||
const { title, subject_slug, description, show_answers = 1, time_limit } = req.body;
|
||||
const { title, subject_slug, description, show_answers = 1, time_limit, available_to_students = 0 } = req.body;
|
||||
if (!title?.trim()) return res.status(400).json({ error: 'title required' });
|
||||
if (!subject_slug) return res.status(400).json({ error: 'subject_slug required' });
|
||||
const tl = time_limit ? Math.max(1, Math.min(600, Number(time_limit))) : null;
|
||||
const r = db.prepare(
|
||||
'INSERT INTO tests (title, subject_slug, description, show_answers, time_limit, created_by) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(title.trim(), subject_slug, description?.trim() || null, show_answers ? 1 : 0, tl, req.user.id);
|
||||
'INSERT INTO tests (title, subject_slug, description, show_answers, time_limit, available_to_students, created_by) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(title.trim(), subject_slug, description?.trim() || null, show_answers ? 1 : 0, tl, available_to_students ? 1 : 0, req.user.id);
|
||||
res.status(201).json({ id: r.lastInsertRowid });
|
||||
}
|
||||
|
||||
@@ -76,13 +92,23 @@ function getOne(req, res) {
|
||||
|
||||
/* ── PUT /api/tests/:id ──────────────────────────────────────────────────── */
|
||||
function update(req, res) {
|
||||
const { title, subject_slug, description, show_answers, time_limit } = req.body;
|
||||
const t = req.resource; // ownership verified by requireOwnership middleware
|
||||
const tl = time_limit !== undefined ? (time_limit ? Math.max(1, Math.min(600, Number(time_limit))) : null) : undefined;
|
||||
db.prepare('UPDATE tests SET title = ?, subject_slug = ?, description = ?, show_answers = ?, time_limit = ? WHERE id = ?')
|
||||
.run(title?.trim(), subject_slug, description?.trim() || null, show_answers === undefined ? 1 : (show_answers ? 1 : 0),
|
||||
tl !== undefined ? tl : t.time_limit,
|
||||
t.id);
|
||||
const b = req.body;
|
||||
const t = req.resource; // ownership verified by requireOwnership middleware
|
||||
// Частичный апдейт: НЕ переданные поля сохраняем из текущей строки (иначе toggleTstAvail,
|
||||
// присылающий только available_to_students, обнулил бы title/subject и т.п.).
|
||||
const title = b.title !== undefined ? (b.title?.trim() || t.title) : t.title;
|
||||
const subject_slug = b.subject_slug !== undefined ? b.subject_slug : t.subject_slug;
|
||||
const description = b.description !== undefined ? (b.description?.trim() || null) : t.description;
|
||||
const show_answers = b.show_answers !== undefined ? (b.show_answers ? 1 : 0) : t.show_answers;
|
||||
const time_limit = b.time_limit !== undefined ? (b.time_limit ? Math.max(1, Math.min(600, Number(b.time_limit))) : null) : t.time_limit;
|
||||
const available = b.available_to_students !== undefined ? (b.available_to_students ? 1 : 0) : t.available_to_students;
|
||||
// Гард целостности: нельзя публиковать тест ученикам с вопросами без правильного ответа.
|
||||
if (available === 1) {
|
||||
const broken = unanswerableInTest(t.id);
|
||||
if (broken.length) return res.status(400).json({ error: `Нельзя опубликовать: ${broken.length} вопрос(ов) без правильного ответа. Исправьте их в банке.`, brokenQuestions: broken });
|
||||
}
|
||||
db.prepare('UPDATE tests SET title = ?, subject_slug = ?, description = ?, show_answers = ?, time_limit = ?, available_to_students = ? WHERE id = ?')
|
||||
.run(title, subject_slug, description, show_answers, time_limit, available, t.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
const db = require('../db/db');
|
||||
const { stripTags } = require('../utils/sanitize');
|
||||
const { pushNotif } = require('../utils/notifications');
|
||||
|
||||
const CATEGORIES = ['ui', 'content', 'feature', 'bug', 'other'];
|
||||
const STATUSES = ['new', 'planned', 'in_progress', 'done', 'declined'];
|
||||
const STATUS_LABEL = {
|
||||
new: 'Новое', planned: 'Запланировано', in_progress: 'В работе',
|
||||
done: 'Готово', declined: 'Отклонено',
|
||||
};
|
||||
|
||||
function clampStr(v, max) {
|
||||
return stripTags(String(v == null ? '' : v)).slice(0, max).trim();
|
||||
}
|
||||
|
||||
/* ── GET /api/wishes ── список: админ видит все (с фильтрами), остальные — свои ── */
|
||||
function list(req, res) {
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const { status, category } = req.query;
|
||||
const where = [];
|
||||
const args = [];
|
||||
if (!isAdmin) { where.push('w.user_id = ?'); args.push(req.user.id); }
|
||||
if (status && STATUSES.includes(status)) { where.push('w.status = ?'); args.push(status); }
|
||||
if (category && CATEGORIES.includes(category)) { where.push('w.category = ?'); args.push(category); }
|
||||
const sql = `
|
||||
SELECT w.id, w.user_id, w.title, w.body, w.category, w.status, w.admin_note,
|
||||
w.created_at, w.updated_at,
|
||||
${isAdmin ? 'u.name AS author_name, u.email AS author_email,' : ''}
|
||||
0 AS _pad
|
||||
FROM wishes w
|
||||
${isAdmin ? 'JOIN users u ON u.id = w.user_id' : ''}
|
||||
${where.length ? 'WHERE ' + where.join(' AND ') : ''}
|
||||
ORDER BY CASE w.status WHEN 'new' THEN 0 WHEN 'planned' THEN 1 WHEN 'in_progress' THEN 2 ELSE 3 END,
|
||||
w.updated_at DESC`;
|
||||
const rows = db.prepare(sql).all(...args);
|
||||
|
||||
let counts = null;
|
||||
if (isAdmin) {
|
||||
counts = {};
|
||||
for (const r of db.prepare('SELECT status, COUNT(*) c FROM wishes GROUP BY status').all()) counts[r.status] = r.c;
|
||||
}
|
||||
res.json({ wishes: rows, counts, isAdmin });
|
||||
}
|
||||
|
||||
/* ── POST /api/wishes ── создать (любой авторизованный) ── */
|
||||
function create(req, res) {
|
||||
const title = clampStr(req.body?.title, 200);
|
||||
if (!title) return res.status(400).json({ error: 'Заголовок обязателен' });
|
||||
const body = clampStr(req.body?.body, 4000);
|
||||
let category = String(req.body?.category || 'other');
|
||||
if (!CATEGORIES.includes(category)) category = 'other';
|
||||
|
||||
const info = db.prepare(
|
||||
`INSERT INTO wishes (user_id, title, body, category) VALUES (?,?,?,?)`
|
||||
).run(req.user.id, title, body || null, category);
|
||||
const row = db.prepare('SELECT * FROM wishes WHERE id = ?').get(Number(info.lastInsertRowid));
|
||||
res.status(201).json(row);
|
||||
}
|
||||
|
||||
/* ── PATCH /api/wishes/:id ── триаж: статус + ответ (только админ) ── */
|
||||
function update(req, res) {
|
||||
const wish = db.prepare('SELECT * FROM wishes WHERE id = ?').get(req.params.id);
|
||||
if (!wish) return res.status(404).json({ error: 'Не найдено' });
|
||||
|
||||
const fields = [];
|
||||
const args = [];
|
||||
let newStatus = null;
|
||||
if (req.body?.status !== undefined) {
|
||||
if (!STATUSES.includes(req.body.status)) return res.status(400).json({ error: 'Неверный статус' });
|
||||
if (req.body.status !== wish.status) newStatus = req.body.status;
|
||||
fields.push('status = ?'); args.push(req.body.status);
|
||||
}
|
||||
if (req.body?.admin_note !== undefined) {
|
||||
fields.push('admin_note = ?'); args.push(clampStr(req.body.admin_note, 2000) || null);
|
||||
}
|
||||
if (!fields.length) return res.status(400).json({ error: 'Нет изменений' });
|
||||
|
||||
fields.push("updated_at = datetime('now')");
|
||||
db.prepare(`UPDATE wishes SET ${fields.join(', ')} WHERE id = ?`).run(...args, wish.id);
|
||||
|
||||
// Уведомить автора при смене статуса (durable + SSE).
|
||||
if (newStatus && wish.user_id !== req.user.id) {
|
||||
try {
|
||||
pushNotif(wish.user_id, 'wish_update',
|
||||
`Ваше пожелание «${wish.title}»: ${STATUS_LABEL[newStatus] || newStatus}`, '/wishes');
|
||||
} catch { /* notif не критичен */ }
|
||||
}
|
||||
res.json(db.prepare('SELECT * FROM wishes WHERE id = ?').get(wish.id));
|
||||
}
|
||||
|
||||
/* ── DELETE /api/wishes/:id ── автор (пока «новое») или админ ── */
|
||||
function remove(req, res) {
|
||||
const wish = db.prepare('SELECT id, user_id, status FROM wishes WHERE id = ?').get(req.params.id);
|
||||
if (!wish) return res.status(404).json({ error: 'Не найдено' });
|
||||
const isAdmin = req.user.role === 'admin';
|
||||
const isOwner = wish.user_id === req.user.id;
|
||||
if (!isAdmin && !(isOwner && wish.status === 'new'))
|
||||
return res.status(403).json({ error: 'Удалять можно только своё необработанное пожелание' });
|
||||
db.prepare('DELETE FROM wishes WHERE id = ?').run(wish.id);
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
module.exports = { list, create, update, remove, CATEGORIES, STATUSES };
|
||||
@@ -48,4 +48,5 @@ db.transaction = function transaction(fn) {
|
||||
};
|
||||
};
|
||||
|
||||
db._path = dbPath; // абсолютный путь к файлу БД (нужен бэкапу при сбросе системы)
|
||||
module.exports = db;
|
||||
|
||||
@@ -37,6 +37,6 @@ INSERT INTO textbooks (slug, subject, grade, title, author, description, html_pa
|
||||
('chemistry-9', 'chemistry', 9, 'Химия — 9 класс', 'Шиманович Е. Я.',
|
||||
'Полный курс химии за 9 класс. §1–60: строение атома, химическая связь, классы соединений, ОВР, металлы и их соединения, электролиз.',
|
||||
'chemistry_9.html', 60, 'amber', 1),
|
||||
('physics-9', 'physics', 9, 'Физика — 9 класс', 'Исаченкова Л. А.',
|
||||
('physics-9', 'physics', 9, 'Физика — 9 класс', 'LearnSpace',
|
||||
'Полный курс физики за 9 класс: §1–38. Механика, кинематика, динамика, статика, законы сохранения, импульс, работа и энергия.',
|
||||
'physics_9.html', 38, 'blue', 2);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- Add interactive Algebra 8 textbook (Глава 1 only for now)
|
||||
-- by Арефьева И. Г., Пирютко О. Н., 2018 (Минск, «Народная асвета»)
|
||||
-- Учебная программа, 2018 (Минск, «Народная асвета»)
|
||||
INSERT OR IGNORE INTO textbooks (slug, subject, grade, title, author, description, html_path, para_count, color, sort_order) VALUES
|
||||
('algebra-8', 'math', 8, 'Алгебра — 8 класс', 'Арефьева И. Г., Пирютко О. Н.',
|
||||
('algebra-8', 'math', 8, 'Алгебра — 8 класс', 'LearnSpace',
|
||||
'Интерактивный учебник по алгебре 8 класса. Глава 1 «Квадратные корни и их свойства. Действительные числа»: §1–§6 + Финал главы. Боксёрский ринг, игра «Таблица квадратов», 5+ интерактивов в каждом параграфе.',
|
||||
'algebra_8.html', 7, 'pink', 3);
|
||||
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('geometry-8', 'math', 8, 'Геометрия — 8 класс', '',
|
||||
'Полный курс геометрии 8 класса по учебнику В. В. Казакова: многоугольники, площади, подобные треугольники, окружности. 4 главы, 56 параграфов, 200+ интерактивов, 28 боссов-проверок.',
|
||||
'Полный курс геометрии 8 класса: многоугольники, площади, подобные треугольники, окружности. 4 главы, 56 параграфов, 200+ интерактивов, 28 боссов-проверок.',
|
||||
'geometry_8_hub.html', 56, 'blue', 4, 1);
|
||||
|
||||
-- 2. Insert 4 chapter children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Algebra 7 hub migration.
|
||||
-- Adds hub row + 4 chapter children for Алгебра 7 (Арефьева/Пирютко, 2022).
|
||||
-- Adds hub row + 4 chapter children for Алгебра 7.
|
||||
-- Pattern mirrors 014_algebra_8_hub.sql and 017_geometry_8_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('algebra-7', 'math', 7, 'Алгебра — 7 класс', '',
|
||||
'Полный курс алгебры 7 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: степени, многочлены и ФСУ, линейные уравнения и функция, системы. 4 главы, 25 параграфов, ~120 интерактивов, 21 босс-проверка.',
|
||||
'Полный курс алгебры 7 класса: степени, многочлены и ФСУ, линейные уравнения и функция, системы. 4 главы, 25 параграфов, ~120 интерактивов, 21 босс-проверка.',
|
||||
'algebra_7_hub.html', 25, 'pink', 6, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Geometry 7 hub migration.
|
||||
-- Adds hub row + 5 chapter children for Геометрия 7 (Казаков, 2022).
|
||||
-- Adds hub row + 5 chapter children for Геометрия 7.
|
||||
-- Pattern mirrors 017_geometry_8_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('geometry-7', 'math', 7, 'Геометрия — 7 класс', '',
|
||||
'Полный курс геометрии 7 класса по учебнику В. В. Казакова: начальные понятия, признаки равенства треугольников, параллельность прямых, сумма углов треугольника, задачи на построение. 5 глав, 31 параграф, ~150 интерактивов, 25 боссов-проверок.',
|
||||
'Полный курс геометрии 7 класса: начальные понятия, признаки равенства треугольников, параллельность прямых, сумма углов треугольника, задачи на построение. 5 глав, 31 параграф, ~150 интерактивов, 25 боссов-проверок.',
|
||||
'geometry_7_hub.html', 31, 'blue', 7, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Algebra 9 hub migration.
|
||||
-- Adds hub row + 4 chapter children for Алгебра 9 (Арефьева/Пирютко, 2019).
|
||||
-- Adds hub row + 4 chapter children for Алгебра 9.
|
||||
-- Pattern mirrors 018_algebra_7_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('algebra-9', 'math', 9, 'Алгебра — 9 класс', '',
|
||||
'Полный курс алгебры 9 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: рациональные выражения, функции и их свойства, дробно-рациональные уравнения и неравенства, прогрессии. 4 главы, 19 параграфов.',
|
||||
'Полный курс алгебры 9 класса: рациональные выражения, функции и их свойства, дробно-рациональные уравнения и неравенства, прогрессии. 4 главы, 19 параграфов.',
|
||||
'algebra_9_hub.html', 19, 'indigo', 7, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
-- Geometry 9 hub migration.
|
||||
-- Adds hub row + 4 chapter children for Геометрия 9 (Казаков, 2019).
|
||||
-- Adds hub row + 4 chapter children for Геометрия 9.
|
||||
-- Pattern mirrors 020_algebra_9_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('geometry-9', 'math', 9, 'Геометрия — 9 класс', 'В. В. Казаков',
|
||||
'Полный курс геометрии 9 класса по учебнику В. В. Казакова: соотношения в прямоугольном треугольнике, окружности треугольника и четырёхугольника, теоремы синусов и косинусов, правильные многоугольники. 4 главы, 16 параграфов.',
|
||||
('geometry-9', 'math', 9, 'Геометрия — 9 класс', 'LearnSpace',
|
||||
'Полный курс геометрии 9 класса: соотношения в прямоугольном треугольнике, окружности треугольника и четырёхугольника, теоремы синусов и косинусов, правильные многоугольники. 4 главы, 16 параграфов.',
|
||||
'geometry_9_hub.html', 16, 'rose', 8, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
@@ -15,18 +15,18 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active, parent_slug)
|
||||
VALUES
|
||||
('geometry-9-ch1', 'math', 9, 'Геометрия 9 · Соотношения в прямоугольном треугольнике',
|
||||
'В. В. Казаков',
|
||||
'LearnSpace',
|
||||
'§1–§6: тригонометрия острого и тупого угла, решение прямоугольного треугольника, формулы площади и среднего геометрического.',
|
||||
'geometry_9_ch1.html', 6, 'amber', 1, 1, 'geometry-9'),
|
||||
('geometry-9-ch2', 'math', 9, 'Геометрия 9 · Окружности',
|
||||
'В. В. Казаков',
|
||||
'LearnSpace',
|
||||
'§7–§9: описанная и вписанная окружности треугольника, окружности прямоугольного треугольника, вписанные и описанные четырёхугольники.',
|
||||
'geometry_9_ch2.html', 3, 'emerald', 2, 1, 'geometry-9'),
|
||||
('geometry-9-ch3', 'math', 9, 'Геометрия 9 · Теоремы синусов и косинусов',
|
||||
'В. В. Казаков',
|
||||
'LearnSpace',
|
||||
'§10–§12: теоремы синусов и косинусов, формула Герона, решение произвольных треугольников.',
|
||||
'geometry_9_ch3.html', 3, 'violet', 3, 1, 'geometry-9'),
|
||||
('geometry-9-ch4', 'math', 9, 'Геометрия 9 · Правильные многоугольники',
|
||||
'В. В. Казаков',
|
||||
'LearnSpace',
|
||||
'§13–§16: внутренние углы и радиусы, треугольник/квадрат/шестиугольник, длина окружности и площадь круга.',
|
||||
'geometry_9_ch4.html', 4, 'cyan', 4, 1, 'geometry-9');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Algebra 10 hub migration.
|
||||
-- Adds hub row + 3 chapter children for Алгебра 10 (Арефьева/Пирютко, 2019).
|
||||
-- Adds hub row + 3 chapter children for Алгебра 10.
|
||||
-- Pattern mirrors 020_algebra_9_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('algebra-10', 'math', 10, 'Алгебра — 10 класс', '',
|
||||
'Полный курс алгебры 10 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: тригонометрия (единичная окружность, функции, уравнения, тождества), корень n-й степени, производная и её применение к исследованию функций. 3 главы, 22 параграфа, ~140 интерактивов, 25 боссов.',
|
||||
'Полный курс алгебры 10 класса: тригонометрия (единичная окружность, функции, уравнения, тождества), корень n-й степени, производная и её применение к исследованию функций. 3 главы, 22 параграфа, ~140 интерактивов, 25 боссов.',
|
||||
'algebra_10_hub.html', 22, 'teal', 8, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Algebra 11 hub migration.
|
||||
-- Adds hub row + 3 chapter children for Алгебра 11 (Арефьева/Пирютко, 2020).
|
||||
-- Adds hub row + 3 chapter children for Алгебра 11.
|
||||
-- Pattern mirrors 023_algebra_10_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('algebra-11', 'math', 11, 'Алгебра — 11 класс', '',
|
||||
'Полный курс алгебры 11 класса по учебнику И. Г. Арефьевой и О. Н. Пирютко: обобщение понятия степени, степенная функция, определение логарифма, показательная функция и уравнения/неравенства, свойства логарифмов, логарифмическая функция и уравнения/неравенства. 3 главы, 10 параграфов.',
|
||||
'Полный курс алгебры 11 класса: обобщение понятия степени, степенная функция, определение логарифма, показательная функция и уравнения/неравенства, свойства логарифмов, логарифмическая функция и уравнения/неравенства. 3 главы, 10 параграфов.',
|
||||
'algebra_11_hub.html', 10, 'emerald', 9, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Geometry 11 hub migration.
|
||||
-- Adds hub row + 4 chapter children for Геометрия 11 (Латотин, Чеботаревский и др., 2020).
|
||||
-- Adds hub row + 4 chapter children for Геометрия 11.
|
||||
-- Pattern mirrors 025_algebra_11_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,8 +7,8 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('geometry-11', 'math', 11, 'Геометрия — 11 класс',
|
||||
'Л. А. Латотин, Б. Д. Чеботаревский, И. В. Горбунова, О. Е. Цыбулько',
|
||||
'Полный курс стереометрии 11 класса по учебнику Латотина и Чеботаревского: призма, цилиндр, пирамида, конус, сфера, шар, правильные многогранники, повторение всей геометрии. 4 раздела, 11 параграфов.',
|
||||
'LearnSpace',
|
||||
'Полный курс стереометрии 11 класса: призма, цилиндр, пирамида, конус, сфера, шар, правильные многогранники, повторение всей геометрии. 4 раздела, 11 параграфов.',
|
||||
'geometry_11_hub.html', 11, 'cyan', 10, 1);
|
||||
|
||||
-- 2. Chapter children (разделы).
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Geometry 10 hub migration.
|
||||
-- Adds hub row + 4 section children for Геометрия 10 (Латотин/Чеботаревский/Горбунова, 2020).
|
||||
-- Adds hub row + 4 section children for Геометрия 10.
|
||||
-- Pattern mirrors 023_algebra_10_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('geometry-10', 'math', 10, 'Геометрия — 10 класс', '',
|
||||
'Полный курс стереометрии 10 класса по учебнику Л. А. Латотина и Б. Д. Чеботаревского: введение в стереометрию (аксиомы, сечения), параллельность прямых и плоскостей, перпендикулярность, координаты и векторы в пространстве. 4 раздела, 14 параграфов, ~140 интерактивов, 24 босса. Все 3D-фигуры — через библиотеку stereo3d.js.',
|
||||
'Полный курс стереометрии 10 класса: введение в стереометрию (аксиомы, сечения), параллельность прямых и плоскостей, перпендикулярность, координаты и векторы в пространстве. 4 раздела, 14 параграфов, ~140 интерактивов, 24 босса. Все 3D-фигуры — через библиотеку stereo3d.js.',
|
||||
'geometry_10_hub.html', 14, 'blue', 9, 1);
|
||||
|
||||
-- 2. Section children.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-- Physics 11 hub migration.
|
||||
-- Adds hub row + 8 chapter children for Физика 11 (Жилко/Маркович/Сокольский, 2021).
|
||||
-- Adds hub row + 8 chapter children for Физика 11.
|
||||
-- Pattern mirrors 030_physics_10_hub.sql.
|
||||
|
||||
-- 1. Hub row.
|
||||
@@ -7,7 +7,7 @@ INSERT INTO textbooks
|
||||
(slug, subject, grade, title, author, description, html_path, para_count, color, sort_order, is_active)
|
||||
VALUES
|
||||
('physics-11', 'physics', 11, 'Физика — 11 класс', '',
|
||||
'Полный курс физики 11 класса по учебнику Жилко-Маркович-Сокольского: механические и электромагнитные колебания и волны, оптика, основы СТО, фотоны, физика атома, ядерная физика и элементарные частицы, единая физическая картина мира. 8 глав, 45 параграфов, реальные симуляции через библиотеку phys-fx.js (анимации, маятники, контуры, лучевые трассировщики, спектры, ядро).',
|
||||
'Полный курс физики 11 класса: механические и электромагнитные колебания и волны, оптика, основы СТО, фотоны, физика атома, ядерная физика и элементарные частицы, единая физическая картина мира. 8 глав, 45 параграфов, реальные симуляции через библиотеку phys-fx.js (анимации, маятники, контуры, лучевые трассировщики, спектры, ядро).',
|
||||
'physics_11_hub.html', 45, 'cyan', 12, 1);
|
||||
|
||||
-- 2. Chapter children.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
-- physics-9-ch4 (Законы сохранения, §§31–36) → physics_9_ch4.html
|
||||
-- physics-9-ch5 (Лабораторный практикум, 12 ЛР) → physics_9_ch5.html
|
||||
--
|
||||
-- Source: Исаченкова Л.А., Сокольский А.А., Захаревич Е.В.,
|
||||
-- Source: учебная программа РБ,
|
||||
-- «Физика 9», Народная асвета, 2019. Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
-- math-6-ch5 (Координатная плоскость, §§1–5) → math_6_ch5.html
|
||||
-- math-6-ch6 (Наглядная геометрия, §§1–5) → math_6_ch6.html
|
||||
--
|
||||
-- Source: Герасимов В. Д., Пирютко О. Н., «Математика. 6 класс»,
|
||||
-- Source: «Математика. 6 класс»,
|
||||
-- Минск: Адукацыя і выхаванне, 2022 (2-е изд.). Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
-- math-5-ch2 (Выражения. Уравнения, §§1–9) → math_5_ch2.html
|
||||
-- math-5-ch3 (Обыкновенные дроби, §§1–18) → math_5_ch3.html
|
||||
--
|
||||
-- Source: Герасимов В. Д., Пирютко О. Н., Лобанов А. П., «Математика. 5 класс»,
|
||||
-- Source: «Математика. 5 класс»,
|
||||
-- в 2 частях, Минск: Адукацыя і выхаванне, 2020 (2-е изд.). Контент авторский (наш).
|
||||
-- Author left empty per project policy.
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 077: ЦЭ/ЦТ по математике — трек 'ctmath' + дерево тем exam_topics
|
||||
--
|
||||
-- План: plans/ct-math/ (PLAN.md, TOPICS_SEED.md).
|
||||
-- Формат экзамена: часть А (А1–А10, выбор из 5) + часть В (В1–В20,
|
||||
-- открытый ответ) = 30 заданий, ~180 мин, до 100 тестовых баллов.
|
||||
--
|
||||
-- Двухуровневая иерархия (как 024_exam_topics_seed): раздел (parent=NULL)
|
||||
-- → подтема. textbook_slug ссылается на существующие учебники платформы
|
||||
-- (algebra-7..11, geometry-8..11, math-5/6); textbook_paragraph пока NULL
|
||||
-- (точные § проставляются при маппинге контента).
|
||||
--
|
||||
-- variants_count=0 — варианты ещё не оцифрованы (см. DIGITIZATION_SPEC.md).
|
||||
-- scoring_json — иллюстративный placeholder; заменить официальной
|
||||
-- таблицей РИКЗ (первичный→тестовый) при наличии вариантов.
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
-- ── Трек ─────────────────────────────────────────────────────────
|
||||
INSERT INTO exam_tracks (
|
||||
exam_key, title, subject_slug, grade, duration_min,
|
||||
tasks_per_variant, variants_count, scoring_json, intro_html, enabled, sort_order
|
||||
) VALUES (
|
||||
'ctmath',
|
||||
'ЦЭ/ЦТ — Математика',
|
||||
'math',
|
||||
11,
|
||||
180,
|
||||
30,
|
||||
0,
|
||||
'[{"correct":30,"score":100},{"correct":28,"score":92},{"correct":26,"score":85},{"correct":24,"score":78},{"correct":22,"score":71},{"correct":20,"score":64},{"correct":18,"score":57},{"correct":16,"score":50},{"correct":14,"score":43},{"correct":12,"score":36},{"correct":10,"score":30},{"correct":8,"score":24},{"correct":6,"score":17},{"correct":4,"score":11},{"correct":2,"score":5},{"correct":0,"score":0}]',
|
||||
'<p><b>Подготовка к ЦЭ/ЦТ по математике.</b> Формат: часть А — 10 заданий с выбором ответа (А1–А10), часть В — 20 заданий с открытым ответом (В1–В20), всего 30 заданий, ~180 минут, без калькулятора.</p><p>Курс устроен по темам с входной диагностикой и тремя уровнями сложности (База / Ядро / Продвинутый): теория с выводом формул, разбор эталонных задач, тренажёр по темам, карточки формул с интервальным повторением и пробные экзамены с таймером на реальных вариантах РТ/ЦТ прошлых лет.</p>',
|
||||
1,
|
||||
20
|
||||
);
|
||||
|
||||
-- ── Разделы (parent_slug = NULL) ─────────────────────────────────
|
||||
INSERT INTO exam_topics (slug, exam_key, parent_slug, title, description, sort_order, textbook_slug) VALUES
|
||||
('numbers', 'ctmath', NULL, 'Числа и вычисления', 'Действительные числа, делимость, проценты, преобразование числовых выражений', 10, 'math-6'),
|
||||
('expressions', 'ctmath', NULL, 'Алгебраические преобразования','Многочлены, степени и корни, рациональные дроби, ОДЗ', 20, 'algebra-7'),
|
||||
('equations', 'ctmath', NULL, 'Уравнения и неравенства', 'Линейные, квадратные, рациональные, модуль, иррациональные, показательные, логарифмические; метод рационализации', 30, 'algebra-9'),
|
||||
('functions', 'ctmath', NULL, 'Функции и производная', 'Свойства функций, графики, исследование с производной', 40, 'algebra-9-ch2'),
|
||||
('trigonometry', 'ctmath', NULL, 'Тригонометрия', 'Круг, тождества, уравнения и отбор корней', 50, 'algebra-10-ch1'),
|
||||
('word-sequences', 'ctmath', NULL, 'Прогрессии и текстовые задачи','Арифметическая/геометрическая прогрессии; проценты, движение, работа, смеси', 60, 'algebra-9-ch4'),
|
||||
('planimetry', 'ctmath', NULL, 'Планиметрия', 'Треугольники, четырёхугольники, окружность; координатный метод', 70, 'geometry-8'),
|
||||
('stereometry', 'ctmath', NULL, 'Стереометрия', 'Расположение, многогранники, тела вращения, углы и расстояния', 80, 'geometry-10'),
|
||||
('advanced', 'ctmath', NULL, 'Продвинутое и комбинированное','Параметры, комбинированные задачи, функциональные методы', 90, NULL);
|
||||
|
||||
-- ── Подтемы (parent_slug = раздел) ───────────────────────────────
|
||||
INSERT INTO exam_topics (slug, exam_key, parent_slug, title, sort_order, textbook_slug) VALUES
|
||||
-- Числа и вычисления
|
||||
('num-real', 'ctmath', 'numbers', 'Действительные числа, координатная прямая', 11, 'math-6'),
|
||||
('num-divisibility', 'ctmath', 'numbers', 'Делимость, дроби, НОД/НОК', 12, 'math-5-ch1'),
|
||||
('num-expressions', 'ctmath', 'numbers', 'Преобразование числовых выражений', 13, 'algebra-7-ch2'),
|
||||
-- Алгебраические преобразования
|
||||
('expr-polynomials', 'ctmath', 'expressions', 'Многочлены, ФСУ, разложение на множители', 21, 'algebra-7-ch2'),
|
||||
('expr-powers-roots', 'ctmath', 'expressions', 'Степени и корни, ОДЗ выражений', 22, 'algebra-10-ch2'),
|
||||
('expr-fractions', 'ctmath', 'expressions', 'Рациональные (алгебраические) дроби', 23, 'algebra-9-ch1'),
|
||||
-- Уравнения и неравенства
|
||||
('eq-linear', 'ctmath', 'equations', 'Линейные уравнения/неравенства, системы', 31, 'algebra-7-ch3'),
|
||||
('eq-quadratic', 'ctmath', 'equations', 'Квадратные уравнения/неравенства, Виет', 32, 'algebra-8'),
|
||||
('eq-rational', 'ctmath', 'equations', 'Рациональные уравнения/неравенства, метод интервалов', 33, 'algebra-9-ch3'),
|
||||
('eq-modulus', 'ctmath', 'equations', 'Уравнения и неравенства с модулем', 34, 'algebra-9'),
|
||||
('eq-irrational', 'ctmath', 'equations', 'Иррациональные уравнения/неравенства', 35, 'algebra-10-ch2'),
|
||||
('eq-exponential', 'ctmath', 'equations', 'Показательные уравнения/неравенства', 36, 'algebra-11-ch2'),
|
||||
('eq-logarithmic', 'ctmath', 'equations', 'Логарифмические уравнения/неравенства', 37, 'algebra-11-ch3'),
|
||||
('eq-rationalization','ctmath', 'equations', 'Метод рационализации (замена множителей)', 38, 'algebra-11'),
|
||||
-- Функции и производная
|
||||
('fn-properties', 'ctmath', 'functions', 'Свойства функций: ОДЗ, чётность, монотонность', 41, 'algebra-9-ch2'),
|
||||
('fn-graphs', 'ctmath', 'functions', 'Графики и их преобразования, чтение графиков', 42, 'algebra-9-ch2'),
|
||||
('fn-derivative', 'ctmath', 'functions', 'Производная: монотонность, экстремумы, исследование', 43, 'algebra-10-ch3'),
|
||||
-- Тригонометрия
|
||||
('trig-circle', 'ctmath', 'trigonometry', 'Тригонометрический круг, значения, простейшие уравнения', 51, 'algebra-10-ch1'),
|
||||
('trig-identities', 'ctmath', 'trigonometry', 'Тождества и формулы (вывод), обратные функции', 52, 'algebra-10-ch1'),
|
||||
('trig-equations', 'ctmath', 'trigonometry', 'Тригонометрические уравнения, отбор корней', 53, 'algebra-10-ch1'),
|
||||
-- Прогрессии и текстовые задачи
|
||||
('seq-progressions', 'ctmath', 'word-sequences', 'Арифметическая и геометрическая прогрессии', 61, 'algebra-9-ch4'),
|
||||
('word-problems', 'ctmath', 'word-sequences', 'Текстовые: проценты, движение, работа, смеси', 62, 'math-6-ch2'),
|
||||
-- Планиметрия
|
||||
('plan-triangles', 'ctmath', 'planimetry', 'Треугольники, площади, теоремы синусов/косинусов, окружности', 71, 'geometry-8'),
|
||||
('plan-quadrilaterals','ctmath','planimetry', 'Четырёхугольники и правильные многоугольники', 72, 'geometry-8-ch1'),
|
||||
('plan-circle', 'ctmath', 'planimetry', 'Окружность: углы, касательные; координатный метод', 73, 'geometry-8-ch4'),
|
||||
-- Стереометрия
|
||||
('ster-basics', 'ctmath', 'stereometry', 'Расположение прямых/плоскостей, сечения', 81, 'geometry-10'),
|
||||
('ster-polyhedra', 'ctmath', 'stereometry', 'Многогранники: объёмы, площади, сечения, подобие', 82, 'geometry-10'),
|
||||
('ster-rotation', 'ctmath', 'stereometry', 'Тела вращения: цилиндр, конус, шар/сфера', 83, 'geometry-11'),
|
||||
('ster-angles-distances','ctmath','stereometry', 'Углы и расстояния; координатно-векторный метод', 84, 'geometry-11'),
|
||||
-- Продвинутое
|
||||
('adv-parameters', 'ctmath', 'advanced', 'Задачи с параметрами', 91, NULL),
|
||||
('adv-combined', 'ctmath', 'advanced', 'Комбинированные задачи, нестандартные приёмы', 92, NULL),
|
||||
('adv-functional', 'ctmath', 'advanced', 'Функциональные методы, целые числа (бонус)', 93, NULL);
|
||||
@@ -0,0 +1,28 @@
|
||||
-- 078_prep_tracks.sql
|
||||
-- Система «подготовка к направлению» (ЦТ и др.): флаг ученика + коллекция колод.
|
||||
--
|
||||
-- МАСТЕР-ФЛАГ: запись student_prep(user_id, track) открывает ученику ВЕСЬ контент
|
||||
-- трека сразу — коллекцию флешкарт (flashcard_decks.collection), курс и пробники
|
||||
-- (content_type='course'/'exam'). Маппинг трек→контент — services/prepTracks.js;
|
||||
-- динамический резолв доступа — services/contentAccess.js (без материализации
|
||||
-- правил content_access) и flashcardController.deckAccess/listDecks.
|
||||
--
|
||||
-- Управление флагом — учитель (своим ученикам) и админ (см. prepController.js).
|
||||
|
||||
-- 1) Флаг подготовки ученика по треку. track — ключ направления ('ct-math', …).
|
||||
CREATE TABLE IF NOT EXISTS student_prep (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
track TEXT NOT NULL,
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
UNIQUE (user_id, track)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_student_prep_user ON student_prep(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_student_prep_track ON student_prep(track);
|
||||
|
||||
-- 2) Коллекция (папка/направление) колоды. NULL = вне коллекции (поведение как раньше).
|
||||
ALTER TABLE flashcard_decks ADD COLUMN collection TEXT;
|
||||
|
||||
-- 3) Разметить существующие ЦТ-колоды коллекцией 'ct-math' (заголовки «ЦТ · …»).
|
||||
UPDATE flashcard_decks SET collection = 'ct-math' WHERE title LIKE 'ЦТ ·%';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Витрина тестов для ученика: флаг «тест доступен ученикам».
|
||||
-- Учитель/админ помечает свой тест доступным → он появляется в каталоге у учеников
|
||||
-- (дашборд, виджет «Тесты»). По умолчанию 0 — тест виден только автору в конструкторе.
|
||||
ALTER TABLE tests ADD COLUMN available_to_students INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 080_wishes.sql — трекер пожеланий по улучшению системы.
|
||||
-- Любой пользователь подаёт пожелание; видит только свои. Админ видит все и ведёт по статусам.
|
||||
CREATE TABLE IF NOT EXISTS wishes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT,
|
||||
category TEXT NOT NULL DEFAULT 'other', -- ui | content | feature | bug | other
|
||||
status TEXT NOT NULL DEFAULT 'new', -- new | planned | in_progress | done | declined
|
||||
admin_note TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_user ON wishes(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_wishes_status ON wishes(status);
|
||||
@@ -119,6 +119,22 @@ function parentAuth(req, res, next) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* requirePermissionForStudents(key) — применяет проверку права ТОЛЬКО к ролям
|
||||
* ученика (student/free_student); учитель и админ проходят всегда.
|
||||
* Нужно для роутов, которыми пользуются и учителя, и ученики (ассистент,
|
||||
* материалы, игры, флеш-карты, exam-prep): ученическое право не должно ломать
|
||||
* доступ учителю (у учителя нет записи по ключу → isEnabled вернул бы false).
|
||||
*/
|
||||
function requirePermissionForStudents(key) {
|
||||
const guard = requirePermission(key);
|
||||
return (req, res, next) => {
|
||||
const r = req.user?.role;
|
||||
if (r === 'student' || r === 'free_student') return guard(req, res, next);
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
/* Alias: requireAuth = authMiddleware */
|
||||
const requireAuth = authMiddleware;
|
||||
|
||||
@@ -151,4 +167,4 @@ function optionalAuth(req, res, next) {
|
||||
next();
|
||||
}
|
||||
|
||||
module.exports = { authMiddleware, requireAuth, optionalAuth, requireRole, requirePermission, perm, parentAuth, effectiveRoles };
|
||||
module.exports = { authMiddleware, requireAuth, optionalAuth, requireRole, requirePermission, requirePermissionForStudents, perm, parentAuth, effectiveRoles };
|
||||
|
||||
@@ -115,6 +115,28 @@ const PERMISSIONS = {
|
||||
label: 'Управление геймификацией',
|
||||
desc: 'Начислять XP/монеты ученикам, управлять достижениями',
|
||||
},
|
||||
'classroom.host': {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
label: 'Вести онлайн-уроки',
|
||||
desc: 'Запускать синхронные онлайн-уроки (classroom) с доской для класса',
|
||||
requireConfirmOff: true,
|
||||
},
|
||||
'livequiz.host': {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
label: 'Запускать живые викторины',
|
||||
desc: 'Создавать и проводить синхронные викторины в реальном времени',
|
||||
},
|
||||
'simbuilder.use': {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
label: 'Конструктор симуляций',
|
||||
desc: 'Создавать и редактировать собственные интерактивные симуляции',
|
||||
requireConfirmOff: true,
|
||||
},
|
||||
'flashcards.manage': {
|
||||
role: 'teacher', roles: ['teacher'], default: 1,
|
||||
label: 'Общие колоды флеш-карт',
|
||||
desc: 'Создавать и раздавать общие колоды флеш-карт классам',
|
||||
},
|
||||
|
||||
/* ── Student (also applies to free_student — same keys, same defaults) ── */
|
||||
'tests.free': {
|
||||
@@ -160,6 +182,38 @@ const PERMISSIONS = {
|
||||
desc: 'Использовать режим "Задания" в симуляциях (квиз-режим)',
|
||||
requires: ['simulations.access'],
|
||||
},
|
||||
'homework.submit': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Сдавать домашние задания',
|
||||
desc: 'Загружать работы и пересдавать домашние задания',
|
||||
},
|
||||
'materials.save': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Сохранять материалы',
|
||||
desc: 'Сохранять доску/заметки/рисунки в раздел «Мои материалы»',
|
||||
},
|
||||
'assistant.use': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'ИИ-ассистент',
|
||||
desc: 'Задавать вопросы ИИ-ассистенту «Квантик»',
|
||||
},
|
||||
'flashcards.access': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Раздел флеш-карт доступен роли',
|
||||
desc: 'Включает раздел флеш-карт для роли. Какие именно колоды видны — настраивается по классам в «Доступ · контент»',
|
||||
requireConfirmOff: true,
|
||||
},
|
||||
'exam.access': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Подготовка к экзаменам доступна роли',
|
||||
desc: 'Включает разделы подготовки к экзаменам/ЦТ для роли. Какие именно модули видны — настраивается в «Доступ · контент»',
|
||||
requireConfirmOff: true,
|
||||
},
|
||||
'games.play': {
|
||||
role: 'student', roles: ['student', 'free_student'], default: 1,
|
||||
label: 'Учебные игры',
|
||||
desc: 'Играть в учебные мини-игры (Виселица, Кроссворд)',
|
||||
},
|
||||
};
|
||||
|
||||
/* Группы для секций в админ-UI (один источник; byRole проставляет group). */
|
||||
@@ -169,15 +223,20 @@ const GROUP = {
|
||||
'students.invite': 'Класс и ученики', 'sessions.reset': 'Класс и ученики',
|
||||
'results.export': 'Класс и ученики', 'classes.manage': 'Класс и ученики',
|
||||
'schedule.manage': 'Класс и ученики', 'announcements.send': 'Класс и ученики',
|
||||
'classroom.host': 'Класс и ученики', 'livequiz.host': 'Класс и ученики',
|
||||
'library.upload': 'Библиотека', 'library.folders': 'Библиотека',
|
||||
'templates.manage': 'Курсы и шаблоны', 'templates.public': 'Курсы и шаблоны',
|
||||
'courses.manage': 'Курсы и шаблоны', 'courses.interactive': 'Курсы и шаблоны',
|
||||
'simbuilder.use': 'Курсы и шаблоны', 'flashcards.manage': 'Курсы и шаблоны',
|
||||
'shop.manage': 'Геймификация', 'gamification.manage': 'Геймификация',
|
||||
// student
|
||||
'tests.free': 'Тесты и активность', 'board.post': 'Тесты и активность',
|
||||
'homework.submit': 'Тесты и активность', 'materials.save': 'Тесты и активность',
|
||||
'assistant.use': 'Тесты и активность', 'games.play': 'Тесты и активность',
|
||||
'profile.edit': 'Профиль',
|
||||
'shop.purchase': 'Геймификация', 'gamification.challenges': 'Геймификация',
|
||||
'theory.access': 'Контент', 'simulations.access': 'Контент', 'simulations.quiz': 'Контент',
|
||||
'flashcards.access': 'Контент', 'exam.access': 'Контент',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,11 +13,19 @@ router.patch('/free-student-features', requireRole('admin'), ctrl.updateF
|
||||
/* Everything below is admin-only */
|
||||
router.use(requireRole('admin'));
|
||||
|
||||
/* ⚠️ Сброс системы «чистый запуск» — деструктивно, только admin */
|
||||
router.get('/reset-system/plan', requireRole('admin'), ctrl.getResetPlan);
|
||||
router.post('/reset-system', requireRole('admin'), ctrl.resetSystem);
|
||||
|
||||
router.get('/assistant', ctrl.getAssistant);
|
||||
router.put('/assistant', ctrl.saveAssistant);
|
||||
router.post('/assistant/test', ctrl.testAssistant);
|
||||
router.post('/assistant/reindex', ctrl.reindexTextbooks);
|
||||
router.get('/assistant/models', ctrl.getProviderModels);
|
||||
router.post('/assistant/scan', ctrl.scanModels);
|
||||
router.post('/assistant/probe', ctrl.probeModel);
|
||||
router.post('/assistant/models/apply', ctrl.applyModels);
|
||||
router.post('/assistant/health', ctrl.runHealth);
|
||||
router.get('/imggen', ctrl.getImggen);
|
||||
router.put('/imggen', ctrl.saveImggen);
|
||||
router.post('/imggen/test', ctrl.testImggen);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* Квантик-ассистент. Все маршруты — под авторизацией (router-level), фича-гейт
|
||||
* 'pet' навешивается при монтировании в server.js. */
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermissionForStudents } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const ctrl = require('../controllers/assistantController');
|
||||
|
||||
@@ -16,8 +16,10 @@ router.get('/context', ctrl.getContext);
|
||||
router.post('/seen', ctrl.markSeen);
|
||||
router.post('/dismiss', ctrl.dismiss);
|
||||
router.patch('/settings', ctrl.setSettings);
|
||||
router.post('/ask', askLimiter, ctrl.ask);
|
||||
router.post('/flashcards', fcLimiter, ctrl.flashcardsFromText);
|
||||
router.post('/ask', requirePermissionForStudents('assistant.use'), askLimiter, ctrl.ask);
|
||||
router.post('/ask/stream', requirePermissionForStudents('assistant.use'), askLimiter, ctrl.askStream);
|
||||
router.post('/flashcards', requirePermissionForStudents('assistant.use'), fcLimiter, ctrl.flashcardsFromText);
|
||||
router.post('/questions', requireRole('teacher', 'admin'), fcLimiter, ctrl.questionsFromText);
|
||||
router.post('/feedback', ctrl.feedback);
|
||||
router.get('/memory', ctrl.getMemory);
|
||||
router.delete('/memory', ctrl.clearMemory);
|
||||
|
||||
@@ -10,7 +10,7 @@ const profileLimiter = rateLimit({ windowMs: 60_000, max: 10, message: 'Сли
|
||||
|
||||
const registerSchema = { body: {
|
||||
email: { type: 'string', required: true, maxLen: 255, match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ },
|
||||
password: { type: 'string', required: true, minLen: 6, maxLen: 128 },
|
||||
password: { type: 'string', required: true, minLen: 8, maxLen: 128 }, // политика — 8 (как в контроллере и на фронте)
|
||||
name: { type: 'string', required: true, minLen: 1, maxLen: 100 },
|
||||
}};
|
||||
const loginSchema = { body: {
|
||||
@@ -19,7 +19,7 @@ const loginSchema = { body: {
|
||||
}};
|
||||
const profileSchema = { body: {
|
||||
name: { type: 'string', minLen: 1, maxLen: 100 },
|
||||
newPassword: { type: 'string', minLen: 6, maxLen: 128 },
|
||||
newPassword: { type: 'string', minLen: 8, maxLen: 128 }, // политика — 8 (как в контроллере)
|
||||
currentPassword: { type: 'string', maxLen: 128 },
|
||||
}};
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ router.delete('/:id', requireRole('teacher','admin'), requirePermission('
|
||||
router.post('/:id/new-code', requireRole('teacher','admin'), requirePermission('classes.manage'), ctrl.regenerateCode);
|
||||
router.get('/:id/journal', requireRole('teacher','admin'), ctrl.classJournal);
|
||||
router.get('/:id/journal/csv', requireRole('teacher','admin'), ctrl.classJournalCsv);
|
||||
router.get('/:id/outstanding', requireRole('teacher','admin'), assignCtrl.classOutstanding);
|
||||
router.post('/:id/members', requireRole('teacher','admin'), ctrl.addMember);
|
||||
router.delete('/:id/members/:uid', requireRole('teacher','admin'), ctrl.kickMember);
|
||||
router.post('/:id/assignments', requireRole('teacher','admin'), validate(assignmentSchema), assignCtrl.createAssignment);
|
||||
|
||||
@@ -2,7 +2,7 @@ const router = require('express').Router();
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const c = require('../controllers/classroomController');
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get('/my/history', ...auth, c.getMyHistory);
|
||||
router.get('/class/:classId/history', ...auth, c.getClassHistory);
|
||||
|
||||
// Session lifecycle
|
||||
router.post('/', ...teacher, c.createSession);
|
||||
router.post('/', ...teacher, requirePermission('classroom.host'), c.createSession);
|
||||
router.get('/online-students', ...teacher, c.getOnlineStudents);
|
||||
router.get('/my/session', ...auth, c.getMySession);
|
||||
router.get('/class/:classId/active', ...auth, c.getActiveSession);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use strict';
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const rateLimit = require('../middleware/rateLimit');
|
||||
const ctrl = require('../controllers/clientErrorController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
// Не больше 20 отчётов в минуту с пользователя — защита от флуда циклящихся ошибок.
|
||||
router.post('/', rateLimit({ windowMs: 60_000, max: 20, byUser: true, message: 'Слишком много отчётов об ошибках' }), ctrl.report);
|
||||
|
||||
module.exports = router;
|
||||
@@ -5,7 +5,7 @@
|
||||
* НЕ blanket requireRole на роутере: список/чтение доступны и ученику (published). */
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const { requireFeature } = require('../middleware/features');
|
||||
const c = require('../controllers/customSimController');
|
||||
|
||||
@@ -22,9 +22,9 @@ router.get('/:id', c.get);
|
||||
// @public-by-design: router-level authMiddleware (above) + ownership/published check in handler
|
||||
router.get('/:id/related', c.related);
|
||||
|
||||
router.post('/', gate, requireRole('teacher', 'admin'), c.create);
|
||||
router.post('/', gate, requireRole('teacher', 'admin'), requirePermission('simbuilder.use'), c.create);
|
||||
// @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler
|
||||
router.put('/:id', gate, requireRole('teacher', 'admin'), c.update);
|
||||
router.put('/:id', gate, requireRole('teacher', 'admin'), requirePermission('simbuilder.use'), c.update);
|
||||
// @public-by-design: router-level authMiddleware (above) + per-row ownership check in handler
|
||||
router.delete('/:id', gate, requireRole('teacher', 'admin'), c.remove);
|
||||
|
||||
|
||||
+121
-21
@@ -1,10 +1,13 @@
|
||||
'use strict';
|
||||
const router = require('express').Router();
|
||||
const db = require('../db/db');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermissionForStudents } = require('../middleware/auth');
|
||||
const access = require('../services/contentAccess');
|
||||
|
||||
router.use(authMiddleware);
|
||||
// Ролевой доступ к подготовке к экзаменам: ученик без права exam.access закрыт;
|
||||
// учитель/админ проходят всегда. Видимость конкретных модулей — в «Доступ · контент».
|
||||
router.use(requirePermissionForStudents('exam.access'));
|
||||
|
||||
/* Гейт доступа: любой маршрут с :examKey проверяется по allowlist.
|
||||
Админ/учитель проходят всегда; ученик — только при наличии правила. */
|
||||
@@ -15,6 +18,56 @@ router.param('examKey', (req, res, next, examKey) => {
|
||||
next();
|
||||
});
|
||||
|
||||
/* ── Mock/variant picker: какие variant считаются «пробниками» ──────
|
||||
ctmath: год-пачки (variant=год 2011–2024 и 0) — это тематический ПУЛ для
|
||||
тренажёра по темам, а НЕ чистые 30-задачные варианты (у части до 114 задач).
|
||||
Чистые варианты-пробники нумеруются 3-значно (101, 102, …), а год-пачки —
|
||||
4-значными годами (≥2011) и 0, поэтому фильтр — ДИАПАЗОН [101;1999], а не
|
||||
просто порог (год 2024 > 101 и иначе бы прошёл!). В пикере пробников,
|
||||
mock/start и просмотре вариантов показываем только чистые. Тренажёр по темам
|
||||
отбирает по subtopic и этот фильтр НЕ использует — пул задач не теряется.
|
||||
Для остальных треков (math9: варианты 1..80) диапазона нет — показываются все. */
|
||||
const MOCK_VARIANT_RANGE = { ctmath: [101, 1999] };
|
||||
const isMockVariant = (examKey, v) => {
|
||||
const r = MOCK_VARIANT_RANGE[examKey];
|
||||
return r ? (v >= r[0] && v <= r[1]) : (v >= 1);
|
||||
};
|
||||
// Границы вариантов для практики/тренажёра: тот же фильтр, что у пикера, чтобы
|
||||
// год-пачки (variant=год≥2011) и variant=0 НЕ попадали в выборку задач и не
|
||||
// дублировали выверенные варианты-пробники. Для треков без диапазона — всё, кроме 0.
|
||||
const MV_LO = ek => (MOCK_VARIANT_RANGE[ek] ? MOCK_VARIANT_RANGE[ek][0] : 1);
|
||||
const MV_HI = ek => (MOCK_VARIANT_RANGE[ek] ? MOCK_VARIANT_RANGE[ek][1] : 1e15);
|
||||
|
||||
/* Человекочитаемая подпись варианта (номер в БД остаётся техническим, напр. 101).
|
||||
Для ctmath варианты-пробники именуются по источнику; при добавлении новых
|
||||
вариантов (104+) — дописывать сюда. Иначе fallback «Вариант N». */
|
||||
const VARIANT_LABEL = {
|
||||
ctmath: {
|
||||
101: 'РТ-2024/25 · этап I',
|
||||
102: 'РТ-2024/25 · этап II',
|
||||
103: 'РТ-2024/25 · этап III',
|
||||
104: 'РТ-2023/24 · этап I',
|
||||
105: 'РТ-2023/24 · этап II',
|
||||
106: 'РТ-2023/24 · этап III',
|
||||
107: 'РТ-2022/23 · этап I',
|
||||
108: 'РТ-2022/23 · этап II',
|
||||
109: 'РТ-2022/23 · этап III',
|
||||
110: 'ЦТ-2014',
|
||||
111: 'ЦЭ-2024',
|
||||
112: 'ЦТ-2015',
|
||||
113: 'ЦТ-2016',
|
||||
114: 'ЦТ-2018',
|
||||
115: 'ЦТ-2019',
|
||||
116: 'ЦТ-2020',
|
||||
117: 'ЦТ-2021',
|
||||
118: 'ЦТ-2017',
|
||||
119: 'ЦТ-2013',
|
||||
120: 'ЦТ-2012',
|
||||
121: 'ЦТ-2011',
|
||||
},
|
||||
};
|
||||
const examVariantLabel = (examKey, v) => VARIANT_LABEL[examKey]?.[v] || `Вариант ${v}`;
|
||||
|
||||
/* ── Statements (prepared once) ────────────────────────────────── */
|
||||
const SQL = {
|
||||
listTracks: db.prepare(`
|
||||
@@ -90,6 +143,7 @@ const SQL = {
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph
|
||||
FROM exam_tasks
|
||||
WHERE exam_key = ?
|
||||
AND variant BETWEEN ? AND ?
|
||||
AND task_type IN ('mc','open')
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
@@ -120,6 +174,7 @@ const SQL = {
|
||||
t.textbook_slug, t.textbook_paragraph
|
||||
FROM exam_tasks t
|
||||
WHERE t.exam_key = ?
|
||||
AND t.variant BETWEEN ? AND ?
|
||||
AND t.task_type IN ('mc','open')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM exam_attempts a
|
||||
@@ -309,7 +364,7 @@ const SQL = {
|
||||
FROM exam_tasks t
|
||||
LEFT JOIN exam_attempts a
|
||||
ON a.exam_task_id = t.id AND a.user_id = ?
|
||||
WHERE t.exam_key = ? AND t.subtopic IS NOT NULL
|
||||
WHERE t.exam_key = ? AND t.variant BETWEEN ? AND ? AND t.subtopic IS NOT NULL
|
||||
GROUP BY t.subtopic
|
||||
) stat ON stat.subtopic = tp.slug
|
||||
WHERE tp.exam_key = ?
|
||||
@@ -323,7 +378,7 @@ const SQL = {
|
||||
SELECT id, task_idx, variant, task_type, text_html, figure_html, opts_json,
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph
|
||||
FROM exam_tasks t
|
||||
WHERE t.exam_key = ? AND t.subtopic = ?
|
||||
WHERE t.exam_key = ? AND t.variant BETWEEN ? AND ? AND t.subtopic = ?
|
||||
AND t.task_type IN ('mc','open')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM exam_attempts a
|
||||
@@ -337,7 +392,7 @@ const SQL = {
|
||||
SELECT id, task_idx, variant, task_type, text_html, figure_html, opts_json,
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph
|
||||
FROM exam_tasks t
|
||||
WHERE t.exam_key = ? AND t.subtopic = ?
|
||||
WHERE t.exam_key = ? AND t.variant BETWEEN ? AND ? AND t.subtopic = ?
|
||||
AND t.task_type IN ('mc','open')
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
@@ -396,6 +451,7 @@ const SQL = {
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph
|
||||
FROM exam_tasks t
|
||||
WHERE t.exam_key = ?
|
||||
AND t.variant BETWEEN ? AND ?
|
||||
AND t.subtopic IN (${ph})
|
||||
AND t.task_type IN ('mc','open')
|
||||
AND NOT EXISTS (
|
||||
@@ -404,7 +460,7 @@ const SQL = {
|
||||
)
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?
|
||||
`).all(examKey, ...slugs, userId, count);
|
||||
`).all(examKey, MV_LO(examKey), MV_HI(examKey), ...slugs, userId, count);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -416,6 +472,28 @@ router.get('/tracks', (req, res) => {
|
||||
res.json({ tracks });
|
||||
});
|
||||
|
||||
/* ── Админ: управление экзамен-модулями (вкл/выкл) ──
|
||||
Отдельные пути (без :examKey, чтобы не задеть гейт content_access). */
|
||||
router.get('/admin/tracks', requireRole('admin'), (_req, res) => {
|
||||
const tracks = db.prepare(`
|
||||
SELECT exam_key, title, subject_slug, grade, enabled, variants_count, sort_order,
|
||||
(SELECT COUNT(*) FROM exam_tasks t WHERE t.exam_key = exam_tracks.exam_key) AS task_count
|
||||
FROM exam_tracks
|
||||
ORDER BY sort_order, exam_key
|
||||
`).all();
|
||||
res.json({ tracks });
|
||||
});
|
||||
|
||||
router.patch('/admin/track', requireRole('admin'), (req, res) => {
|
||||
const key = String(req.body && req.body.exam_key || '').trim();
|
||||
if (!key) return res.status(400).json({ error: 'exam_key required' });
|
||||
if (!db.prepare('SELECT 1 FROM exam_tracks WHERE exam_key = ?').get(key))
|
||||
return res.status(404).json({ error: 'Unknown exam track' });
|
||||
const enabled = req.body && req.body.enabled ? 1 : 0;
|
||||
db.prepare('UPDATE exam_tracks SET enabled = ? WHERE exam_key = ?').run(enabled, key);
|
||||
res.json({ ok: true, exam_key: key, enabled });
|
||||
});
|
||||
|
||||
/* ── GET /api/exam-prep/:examKey/info ──
|
||||
Track metadata + global counts + this user's aggregate progress. */
|
||||
// @public-by-design: router-level authMiddleware (line 6) covers this route
|
||||
@@ -456,10 +534,10 @@ router.get('/:examKey/info', (req, res) => {
|
||||
router.get('/:examKey/variants', (req, res) => {
|
||||
const { examKey } = req.params;
|
||||
if (!SQL.getTrack.get(examKey)) return res.status(404).json({ error: 'Unknown exam track' });
|
||||
const rows = SQL.listVariants.all(req.user.id, examKey);
|
||||
const rows = SQL.listVariants.all(req.user.id, examKey).filter(r => isMockVariant(examKey, r.variant));
|
||||
const variants = rows.map(r => ({
|
||||
n: r.variant,
|
||||
label: `Вариант ${r.variant}`,
|
||||
label: examVariantLabel(examKey, r.variant),
|
||||
total: r.total,
|
||||
solved: r.solved,
|
||||
viewed_sol: r.viewed_sol,
|
||||
@@ -476,6 +554,7 @@ router.get('/:examKey/variants/:n/tasks', (req, res) => {
|
||||
const { examKey } = req.params;
|
||||
const n = parseInt(req.params.n, 10);
|
||||
if (!Number.isFinite(n) || n < 1) return res.status(400).json({ error: 'Bad variant number' });
|
||||
if (!isMockVariant(examKey, n)) return res.status(404).json({ error: 'Variant not found or empty' });
|
||||
|
||||
const rows = SQL.getVariantTasks.all(examKey, n);
|
||||
if (!rows.length) return res.status(404).json({ error: 'Variant not found or empty' });
|
||||
@@ -629,25 +708,45 @@ function pickRandomByDifficulty(examKey, count, excludeSlugs) {
|
||||
const exClause = exParams
|
||||
? `AND (subtopic IS NULL OR subtopic NOT IN (${exParams.map(() => '?').join(',')}))`
|
||||
: '';
|
||||
const mvLo = MV_LO(examKey), mvHi = MV_HI(examKey);
|
||||
|
||||
const COLS = `id, task_idx, variant, task_type, text_html, figure_html, opts_json,
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph`;
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
for (let d = 1; d <= 5; d++) {
|
||||
const limit = dist[d - 1];
|
||||
if (limit === 0) continue;
|
||||
const sql = `
|
||||
SELECT id, task_idx, variant, task_type, text_html, figure_html, opts_json,
|
||||
answer, solution_html, topic, subtopic, difficulty, textbook_slug, textbook_paragraph
|
||||
SELECT ${COLS}
|
||||
FROM exam_tasks
|
||||
WHERE exam_key = ? AND task_type IN ('mc','open')
|
||||
WHERE exam_key = ? AND variant BETWEEN ? AND ? AND task_type IN ('mc','open')
|
||||
AND difficulty = ?
|
||||
${exClause}
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?`;
|
||||
const args = exParams
|
||||
? [examKey, d, ...exParams, limit]
|
||||
: [examKey, d, limit];
|
||||
? [examKey, mvLo, mvHi, d, ...exParams, limit]
|
||||
: [examKey, mvLo, mvHi, d, limit];
|
||||
for (const r of db.prepare(sql).all(...args)) { if (!seen.has(r.id)) { seen.add(r.id); out.push(r); } }
|
||||
}
|
||||
// Backfill to `count` from any difficulty — covers tracks whose tasks don't
|
||||
// span all 5 difficulty levels (otherwise empty levels would shrink the batch).
|
||||
if (out.length < count) {
|
||||
const ids = [...seen];
|
||||
const notIn = ids.length ? `AND id NOT IN (${ids.map(() => '?').join(',')})` : '';
|
||||
const sql = `
|
||||
SELECT ${COLS}
|
||||
FROM exam_tasks
|
||||
WHERE exam_key = ? AND variant BETWEEN ? AND ? AND task_type IN ('mc','open')
|
||||
${exClause}
|
||||
${notIn}
|
||||
ORDER BY RANDOM()
|
||||
LIMIT ?`;
|
||||
const args = [examKey, mvLo, mvHi, ...(exParams || []), ...ids, count - out.length];
|
||||
out.push(...db.prepare(sql).all(...args));
|
||||
}
|
||||
out.sort((a, b) => (a.difficulty || 0) - (b.difficulty || 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -684,7 +783,7 @@ router.get('/:examKey/practice/next', (req, res) => {
|
||||
weakSlugs = SQL.weakTopicSlugs.all(req.user.id, examKey).map(r => r.slug);
|
||||
if (weakSlugs.length === 0) {
|
||||
// No weak topics yet → fall back to unsolved so the button still works
|
||||
rows = SQL.practiceUnsolved.all(examKey, req.user.id, count);
|
||||
rows = SQL.practiceUnsolved.all(examKey, MV_LO(examKey), MV_HI(examKey), req.user.id, count);
|
||||
strategy = 'unsolved-fallback';
|
||||
} else {
|
||||
rows = SQL.weakBatchTasks(examKey, weakSlugs, req.user.id, count);
|
||||
@@ -694,8 +793,8 @@ router.get('/:examKey/practice/next', (req, res) => {
|
||||
}
|
||||
}
|
||||
} else if (strategy === 'unsolved') {
|
||||
rows = SQL.practiceUnsolved.all(examKey, req.user.id, count);
|
||||
if (!rows.length) rows = SQL.practiceRandom.all(examKey, count);
|
||||
rows = SQL.practiceUnsolved.all(examKey, MV_LO(examKey), MV_HI(examKey), req.user.id, count);
|
||||
if (!rows.length) rows = SQL.practiceRandom.all(examKey, MV_LO(examKey), MV_HI(examKey), count);
|
||||
} else {
|
||||
// Random: difficulty-ordered batch, position 1 = difficulty 1.
|
||||
rows = pickRandomByDifficulty(examKey, count, excludeSlugs);
|
||||
@@ -703,7 +802,7 @@ router.get('/:examKey/practice/next', (req, res) => {
|
||||
if (!rows.length && excludeSlugs.length) {
|
||||
rows = pickRandomByDifficulty(examKey, count, []);
|
||||
}
|
||||
if (!rows.length) rows = SQL.practiceRandom.all(examKey, count);
|
||||
if (!rows.length) rows = SQL.practiceRandom.all(examKey, MV_LO(examKey), MV_HI(examKey), count);
|
||||
}
|
||||
|
||||
const refMap = getTopicRefMap(examKey);
|
||||
@@ -931,7 +1030,7 @@ router.get('/:examKey/topics', (req, res) => {
|
||||
const { examKey } = req.params;
|
||||
if (!SQL.getTrack.get(examKey)) return res.status(404).json({ error: 'Unknown exam track' });
|
||||
|
||||
const rows = SQL.listTopicsWithCounts.all(req.user.id, examKey, examKey);
|
||||
const rows = SQL.listTopicsWithCounts.all(req.user.id, examKey, MV_LO(examKey), MV_HI(examKey), examKey);
|
||||
|
||||
// Build {sections: [{slug,title,subtopics:[...]}]}
|
||||
const byParent = new Map();
|
||||
@@ -988,10 +1087,10 @@ router.get('/:examKey/topics/:slug/tasks', (req, res) => {
|
||||
|
||||
let rows;
|
||||
if (excludeSolved) {
|
||||
rows = SQL.topicTasksUnsolved.all(examKey, slug, req.user.id, count);
|
||||
if (!rows.length) rows = SQL.topicTasksAny.all(examKey, slug, count);
|
||||
rows = SQL.topicTasksUnsolved.all(examKey, MV_LO(examKey), MV_HI(examKey), slug, req.user.id, count);
|
||||
if (!rows.length) rows = SQL.topicTasksAny.all(examKey, MV_LO(examKey), MV_HI(examKey), slug, count);
|
||||
} else {
|
||||
rows = SQL.topicTasksAny.all(examKey, slug, count);
|
||||
rows = SQL.topicTasksAny.all(examKey, MV_LO(examKey), MV_HI(examKey), slug, count);
|
||||
}
|
||||
|
||||
const refMap = getTopicRefMap(examKey);
|
||||
@@ -1139,7 +1238,7 @@ router.post('/:examKey/mock/start', (req, res) => {
|
||||
|
||||
if (source === 'variant') {
|
||||
variant = Number(req.body?.variant);
|
||||
if (!Number.isInteger(variant) || variant < 1) {
|
||||
if (!Number.isInteger(variant) || !isMockVariant(examKey, variant)) {
|
||||
return res.status(400).json({ error: 'Variant number required' });
|
||||
}
|
||||
const rows = SQL.getTasksByVariant.all(examKey, variant);
|
||||
@@ -1210,6 +1309,7 @@ router.get('/mock/:id', (req, res) => {
|
||||
id: sess.id,
|
||||
exam_key: sess.exam_key,
|
||||
variant: sess.variant,
|
||||
variant_label: sess.variant != null ? examVariantLabel(sess.exam_key, sess.variant) : null,
|
||||
source: sess.source,
|
||||
status: sess.status,
|
||||
started_at: sess.started_at,
|
||||
|
||||
@@ -5,7 +5,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
const fc = require('../controllers/flashcardController');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermission, requirePermissionForStudents } = require('../middleware/auth');
|
||||
const { requireOwnership } = require('../middleware/ownership');
|
||||
|
||||
/* ── multer для картинок карточек ───────────────────────────────────────
|
||||
@@ -30,6 +30,9 @@ const fcUpload = multer({
|
||||
});
|
||||
|
||||
router.use(authMiddleware);
|
||||
// Ролевой доступ к разделу флеш-карт: ученик без права flashcards.access закрыт;
|
||||
// учитель/админ проходят всегда (создают и раздают колоды).
|
||||
router.use(requirePermissionForStudents('flashcards.access'));
|
||||
|
||||
router.post ('/upload', fcUpload.single('file'), fc.uploadImage);
|
||||
|
||||
@@ -45,8 +48,8 @@ router.post ('/decks/:id/cards/bulk', fc.addCardsBulk);
|
||||
router.put ('/decks/:id/reorder', requireOwnership({ table: 'flashcard_decks', ownerField: 'user_id' }), fc.reorderCards);
|
||||
// Шаринг колоды (назначение классу/ученику) — только владелец/админ (проверка в хендлере).
|
||||
router.get ('/decks/:id/shares', fc.listShares);
|
||||
router.post ('/decks/:id/share', requireRole('teacher','admin'), fc.addShare);
|
||||
router.delete('/decks/:id/share', requireRole('teacher','admin'), fc.removeShare);
|
||||
router.post ('/decks/:id/share', requireRole('teacher','admin'), requirePermission('flashcards.manage'), fc.addShare);
|
||||
router.delete('/decks/:id/share', requireRole('teacher','admin'), requirePermission('flashcards.manage'), fc.removeShare);
|
||||
router.get ('/decks/:id/study', fc.getStudySession);
|
||||
router.put ('/cards/:id', fc.updateCard);
|
||||
router.delete('/cards/:id', fc.deleteCard);
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { authMiddleware, requirePermissionForStudents } = require('../middleware/auth');
|
||||
const { requireFeature } = require('../middleware/features');
|
||||
const c = require('../controllers/gamesController');
|
||||
|
||||
const hangman = requireFeature('hangman');
|
||||
const crossword = requireFeature('crossword');
|
||||
// Ролевой доступ к учебным играм: ученик без права games.play закрыт, учитель/админ — нет.
|
||||
const playable = requirePermissionForStudents('games.play');
|
||||
|
||||
router.get('/hangman/word', hangman, authMiddleware, c.hangmanWord);
|
||||
router.post('/hangman/complete', hangman, authMiddleware, c.hangmanComplete);
|
||||
router.get('/crossword/generate', crossword, authMiddleware, c.crosswordGenerate);
|
||||
router.post('/crossword/complete', crossword, authMiddleware, c.crosswordComplete);
|
||||
router.get('/hangman/word', hangman, authMiddleware, playable, c.hangmanWord);
|
||||
router.post('/hangman/complete', hangman, authMiddleware, playable, c.hangmanComplete);
|
||||
router.get('/crossword/generate', crossword, authMiddleware, playable, c.crosswordGenerate);
|
||||
router.post('/crossword/complete', crossword, authMiddleware, playable, c.crosswordComplete);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermission } = require('../middleware/auth');
|
||||
const c = require('../controllers/liveController');
|
||||
|
||||
const teacher = [authMiddleware, requireRole('teacher', 'admin')];
|
||||
|
||||
router.post('/', ...teacher, c.create);
|
||||
router.post('/', ...teacher, requirePermission('livequiz.host'), c.create);
|
||||
router.get('/:id', ...teacher, c.getSession);
|
||||
router.put('/:id/question', ...teacher, c.setQuestion);
|
||||
router.get('/:id/results', ...teacher, c.results);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole, requirePermissionForStudents } = require('../middleware/auth');
|
||||
const c = require('../controllers/studentMaterialsController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
@@ -10,7 +10,8 @@ router.use(authMiddleware);
|
||||
router.post('/:id/share', requireRole('teacher', 'admin'), c.share);
|
||||
|
||||
router.get('/', c.list);
|
||||
router.post('/', c.create);
|
||||
// Сохранение в «Мои материалы»: ученик без права materials.save закрыт, учитель/админ проходят.
|
||||
router.post('/', requirePermissionForStudents('materials.save'), c.create);
|
||||
|
||||
// Collections (folders) — literal '/collections' prefix before '/:id'
|
||||
router.post('/collections', c.createCollection);
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
/* /api/prep — управление мастер-флагом «подготовка к направлению».
|
||||
* Все роуты под authMiddleware. Мутации/чтение чужого статуса — учитель (своих)
|
||||
* или админ (проверка владения в контроллере: canManageStudent/canManageClass). */
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const prep = require('../controllers/prepController');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
|
||||
router.use(authMiddleware);
|
||||
|
||||
router.get ('/tracks', prep.listTracks); // справочник направлений (любой авторизованный)
|
||||
router.get ('/me', prep.myTracks); // свой статус (ученик)
|
||||
|
||||
// Управление флагами учеников/классов — только учитель/админ (владение — в контроллере).
|
||||
router.get ('/student/:id', requireRole('teacher', 'admin'), prep.studentTracks);
|
||||
router.post ('/student/:id', requireRole('teacher', 'admin'), prep.setStudent);
|
||||
router.delete('/student/:id', requireRole('teacher', 'admin'), prep.unsetStudent);
|
||||
router.get ('/class/:id', requireRole('teacher', 'admin'), prep.classStatus);
|
||||
router.post ('/class/:id', requireRole('teacher', 'admin'), prep.setClass);
|
||||
|
||||
module.exports = router;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user