Files
Maxim Dolgolyov c9f3eed8ed fix(exam): классификатор § — fallback при 0 совпадений + учёт opts_json; таксономия в репо
- classify(): bestScore стартует с 0 (нужно совпадение>0), иначе берётся явный fallback
  (последнее правило), а не первое. Чинит свал theory-statements→§15 и word-problems→проценты.
- optsText(): анализ текста вариантов ответа (формат пар [label, html]) — theory-statements
  размечаются по содержанию утверждений.
- alg-word-problems fallback → algebra-7-ch3 §16 (задачи уравнением), не проценты.
- Таксономия §: перенесена с gitignore-пути data/ на отслеживаемый
  backend/scripts/exam-textbook-sections.json + генератор gen-exam-textbook-sections.js.
- Результат: 784/800 (98%) размечено, спреды по подтемам корректны.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:29:40 +03:00

139 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: точная привязка задач экзамена math9 к § учебников
> Составлен Opus 2026-06-03 по итогам discovery. Исполнитель — Sonnet, по фазам, с верификацией.
> Эталон §-таксономии: `plans/exam-textbook-links/taxonomy.md` (читаемая) и
> `backend/scripts/data/g9_textbook_sections.json` (машиночитаемая: {chapter_slug, subject, grade, para_id, num, title}).
## Контекст (проверено по коду/БД)
- `exam_tasks`: 800 задач `math9`, у ВСЕХ заполнен `subtopic` (16 подтем) и `topic`.
- Текущая связь — КОСВЕННАЯ и грубая: `exam_tasks.subtopic``exam_topics.slug` → один
`textbook_slug`+`textbook_paragraph` на всю подтему (миграция 028). Контроллер
`backend/src/routes/exam-prep.js` штампует `topic_ref` через `getTopicRefMap()`/`shapeTask()`.
Фронт `frontend/js/exam-prep/task-card.js:42` строит ссылку `/textbook/<slug>#sec-p<N>`.
- Покрытие сейчас: 546/800 → конкретный §, 158 → только хаб `algebra-9` (alg-numbers/arithmetic/
powers/polynomials/word-problems), 96 (theory-statements) → никуда.
- Экзамен 9 кл. проверяет программу 5–9, а интерактивный учебник 9 кл. — только материал 9 года.
Учебники 5–11 все есть (см. dump в discovery). Для math9 релевантны 5–9.
### Готчи нумерации § (ВАЖНО для классификатора и ссылок)
- **algebra-7, algebra-9, geometry-7, geometry-9** — СКВОЗНАЯ нумерация `sec-pN` по всему учебнику,
но каждый § физически лежит в своём файле-главе. Ключ ссылки = (slug-главы, pN).
- **geometry-8** — ПОГЛАВНАЯ нумерация (каждая глава заново `sec-p1`). Поэтому ОБЯЗАТЕЛЬНО
(slug-главы, pN-внутри-этого-файла). JSON-таксономия уже хранит правильную пару.
- **algebra-8** — сквозная (`sec-p1..p18` по 3 главам).
- **math-5 / math-6** — рендерятся движком `frontend/js/math6_engine.js`
(`<section id="sec-"+p.id>`, p.id из `window.M6.paras`). Статических `sec-pN` в файле нет.
Якорь = `sec-<p.id>`. Если нужно вести в math-5/6 — para_id брать из конфига M6 главы.
### КРИТИЧНО: deep-link сейчас НЕ РАБОТАЕТ
- Статические страницы algebra/geometry (`algebra_9_ch3.html` и т.п.) подключают только
katex/api.js/xp.js — **`textbook-tracker.js` там НЕ подключён**. Их `init()` всегда
вызывает `goTo('p10')` и `location.hash` ИГНОРИРУЕТ. Используют `.psel-card[data-id]`.
- `textbook-tracker.js` (есть на math-5/6) `handleHashNav()` матчит только `/^#(p\d+)$/`
(т.е. `#pN`, НЕ `#sec-pN`) и кликает `.para-pill[data-para]`.
- exam-prep строит `#sec-pN`. → ссылка ведёт на главу, но НЕ открывает нужный §.
- Сервер отдаёт `/textbook/:slug` обычным `sendFile` (инъекция только при `?embed=1`,
см. `backend/src/server.js:437`). Есть `_renderEmbed()` + `_embedCache`.
## Цель
Каждая из 800 задач math9 ведёт на наиболее подходящий § учебника 5–9, и клик реально открывает этот §.
---
## Фаза 1 — Починить навигацию deep-link (prerequisite; без неё связь невидима)
Централизованный хелпер, внедряемый СЕРВЕРОМ в HTML учебника для всех режимов `/textbook/:slug`.
- `backend/src/server.js`: обобщить выдачу — всегда читать файл и инжектить
`<script id="__ls_deeplink__">…</script>` перед `</head>` (или перед `</body>`), кэш по
(filePath, mode='plain'|'embed'). Сейчас в обычном режиме `sendFile` — заменить на read+inject+send
c теми же no-store заголовками. Embed-инъекция остаётся как есть, плюс этот же хелпер.
- Хелпер (vanilla, идемпотентный): на `DOMContentLoaded` (+ небольшой setTimeout, чтобы билдеры/
движок успели построить карточки) и на `hashchange`:
- распарсить `location.hash`: принять `#sec-pN` И `#pN` → нормализовать к `id = 'p'+N`;
- навигация по приоритету: `document.querySelector('.psel-card[data-id="'+id+'"]')?.click()`
→ иначе `.para-pill[data-para="'+id+'"]'`?.click() → иначе `window.goTo?.(id)`
иначе `getElementById('sec-'+id)?.scrollIntoView()`.
- не запускать дважды; не конфликтовать с textbook-tracker (тот матчит `#pN` — наш хелпер
дополнительно поддержит `#sec-pN`). Если на странице есть textbook-tracker и хэш в форме
`#pN`, можно отдать навигацию ему (наш хелпер сработает как фолбэк, клик идемпотентен).
- НЕ ломать embed-bridge (`ls_tb_nav`/`ls_tb_apply`). Хелпер ставить ПОСЛЕ страничных скриптов.
- Проверка: `/textbook/algebra-9-ch3#sec-p13` открывает §13 (метод интервалов), не §10;
`/textbook/geometry-8-ch2#sec-p4` открывает «Площадь треугольника».
## Фаза 2 — Схема: per-task anchor
- Миграция `backend/src/db/migrations/0NN_exam_task_textbook.sql`:
`ALTER TABLE exam_tasks ADD COLUMN textbook_slug TEXT;`
`ALTER TABLE exam_tasks ADD COLUMN textbook_paragraph INTEGER;`
(ADD COLUMN — без пересборки; runner оборачивает в транзакцию.)
- `npm run migrate` на живой БД.
## Фаза 3 — Эвристический классификатор (ядро)
`backend/scripts/tag-exam-textbook.js` (по образцу существующего `tag-exam-tasks.js`):
- Грузит `backend/scripts/data/g9_textbook_sections.json` (§-таксономия 7–9).
- Для каждой задачи math9: `stripText(text_html)` (как в tag-exam-tasks.js) + `subtopic`
выбрать (slug, para):
1. По `subtopic` → набор кандидатных глав/§ (карта ниже).
2. Внутри кандидатов — keyword-скоринг по тексту (ключевые слова из § titles + синонимы:
«дискриминант/Виета»→квадратные, «процент»→math-6 проценты, «синус/косинус/теорема»→geometry-9,
«прогресс/разность d/знаменатель q»→прогрессии §15/§17, «параллелограмм/ромб/трапеция»→geometry-8-ch1, …).
3. Фолбэк: primary § подтемы (если уверенного матча нет).
- Флаги: `--exam math9`, `--dry-run`, `--report` (распределение по §, сколько без §).
Идемпотентно перезаписывает `exam_tasks.textbook_slug/paragraph`.
### Карта subtopic → primary §/главы (исправляет ошибки 028)
- `alg-numbers` → math-6 (рацион. числа) / math-5 (натуральные) по тексту; иначе хаб `math-6`.
- `alg-arithmetic` → math-5/6.
- `alg-powers``algebra-7-ch1` §1 (натур.), §2 (целый показатель), §3 (станд. вид).
- `alg-expressions``algebra-7-ch2` §45 / `algebra-9-ch1` §5 (преобр. рацион.).
- `alg-polynomials``algebra-7-ch2` §8–14 (одночлены/многочлены/ФСУ/разложение).
- `alg-fractions``algebra-9-ch1` §1–5 (рацион. дроби) ИЛИ math-6 (обыкн. дроби) по тексту.
- `alg-equations` → линейные `algebra-7-ch3` §15; квадратные `algebra-8-ch2` §79;
дробно-рацион. `algebra-9-ch3` §10; системы `algebra-7-ch4` §23–24 — по тексту.
- `alg-inequalities``algebra-8-ch3` §13–18; метод интервалов `algebra-9-ch3` §13 / `algebra-8-ch3` §17.
- `alg-functions` → линейная `algebra-7-ch3` §19–20; свойства/графики `algebra-9-ch2` §69.
- `alg-progressions``algebra-9-ch4`: арифм. §15/§16, геом. §1719 (по «d»/«q»).
- `alg-word-problems` → по теме: проценты→`math-6-ch2`; уравнением→`algebra-7-ch3` §16;
системой→`algebra-7-ch4` §25; квадратным→`algebra-8-ch2` §11.
- `geom-triangles` → признаки `geometry-7-ch2`; Пифагор `geometry-8-ch2` §11; подобие `geometry-8-ch3`;
тригонометрия пр. треуг. `geometry-9-ch1` — по тексту.
- `geom-quadrilaterals``geometry-8-ch1` §4–16 (паралл./прямоуг./ромб/квадрат/трапеция);
вписанные/описанные `geometry-9-ch2` §9.
- `geom-circle``geometry-8-ch4` (вписанные углы и т.п.) / `geometry-9-ch2`.
- `geom-coordinates` → уравнение окружности `algebra-9-ch3` §12; координаты — geometry/algebra по тексту.
- `theory-statements` → распределить по теме утверждения (тот же скоринг); иначе оставить NULL
(фолбэк на subtopic-уровень, см. Фазу 5).
## Фаза 4 — Контроллер: предпочесть task-level
`backend/src/routes/exam-prep.js`:
- В SELECT задач (`getVariantTasks`, `practiceRandom/Unsolved`, `pickRandomByDifficulty`,
`topicTasks*`, `weakBatchTasks`, `getTasksByIds`) добавить `t.textbook_slug, t.textbook_paragraph`.
- `shapeTask()` и формирование `topic_ref` в `/variants/:n/tasks`: если у задачи есть
`textbook_slug``topic_ref = { title, slug: task.textbook_slug, paragraph: task.textbook_paragraph }`
(title — из таксономии § или из подтемы), иначе фолбэк на `refMap.get(subtopic)`.
- Title для task-level: можно прокинуть из классификатора в отдельную таблицу/JSON или
собрать map para→title из таксономии при старте (как `getTopicRefMap`).
## Фаза 5 — Улучшить subtopic-фолбэк (миграция данных)
- Новая миграция: переписать `exam_topics.textbook_slug/paragraph` корректно (исправить
`alg-equations`→дробно-рацион. только частный случай и т.п.), заполнить хаб-only разумным §.
Это фолбэк для задач, которым классификатор не дал task-level.
## Фаза 6 — Тесты + верификация
- `backend/tests/exam-textbook-links.test.js`: миграция применилась (колонки есть);
после классификатора ≥90% задач имеют `textbook_slug` (или явно объяснённый остаток);
`GET /:examKey/variants/:n/tasks` отдаёт `topic_ref` с `paragraph` для размеченных задач;
ссылка формата `/textbook/<slug>#sec-p<N>` для валидного slug из таблицы textbooks.
- Прогон `node tag-exam-textbook.js --report` — приложить распределение.
- `node --test tests/*.test.js` в рамках baseline (3 Auth + флака «intro» chemistry8).
- Перезапуск сервера (подхватить миграцию + новые SELECT-поля; `_topicRefCache` для фолбэка).
## Ограничения исполнителю (Sonnet)
- БД: встроенный `node:sqlite` (DatabaseSync), НЕ better-sqlite3.
- Большие файлы (server.js, exam-prep.js, HTML) — ТОЛЬКО точечный `Edit`, НЕ Write целиком.
- ⛔ Без эмоджи; иконки — только inline SVG `.ic`.
- Миграции — ADD COLUMN/UPDATE; НЕ пересобирать users/центральные таблицы.
- `fetch` origin перед работой (в master параллельно коммитят другие сессии); `git add` поимённо.
- Поиск: ast-index/Read, НЕ Grep tool. Каждый Edit верифицировать.
- Коммитить поэтапно осмысленными сообщениями + push (CLAUDE.md), трейлер
`Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.