--- name: reference_exam_textbook_links description: "Как устроена привязка задач экзамена math9 к § учебников (per-task классификатор, deep-link), и как её перегенерировать" metadata: node_type: memory type: reference originSessionId: 60467058-b40e-4bd9-9f7f-d1e362e8039a --- Связь «задание экзамена → § учебника» (фича «Учить тему» в exam-prep). Сделано 2026-06-03. **Модель связи (двухуровневая):** - Per-task: `exam_tasks.textbook_slug` + `exam_tasks.textbook_paragraph` (миграция 057). Контроллер `backend/src/routes/exam-prep.js` (`shapeTask`/`/variants/:n/tasks`) ПРЕДПОЧИТАЕТ task-level; фолбэк — subtopic-уровень `exam_topics.textbook_slug/paragraph` (миграции 028 + фикс 058). - Фронт `frontend/js/exam-prep/task-card.js` строит ссылку `/textbook/#sec-p`. **Классификатор (эвристика, детерминированный):** `backend/scripts/tag-exam-textbook.js` - Карта `subtopic → кандидатные §` + keyword-скоринг по тексту задачи И вариантам ответа (`opts_json`, формат пар `[label, html]`). Требует совпадения >0, иначе берётся явный fallback (последнее правило). - Таксономия §: `backend/scripts/exam-textbook-sections.json` (НЕ в `data/` — тот gitignore!), генерится `node backend/scripts/gen-exam-textbook-sections.js` из `frontend/textbooks/*.html`. Перегенерировать при изменении § учебников, затем перезапустить классификатор. - Запуск: `node backend/scripts/tag-exam-textbook.js --exam math9 [--dry-run] [--report]`. - Результат: **784/800 (98%)** размечено; 70 на хабах math-5/6 (движковые, без статич. §), 16 NULL (чисто-формульные theory → фолбэк на subtopic). **Готчи нумерации § (важно для (slug,para)):** algebra-7/8/9 и geometry-7/9 — сквозная нумерация `sec-pN`; **geometry-8 — ПОГЛАВНАЯ** (каждая глава заново `sec-p1`); **math-5/6 рендерятся движком** `math6_engine.js` (нет статич. `sec-pN`, линкуются на уровне главы, para=null). Экзамен 9 кл. покрывает программу 5–9, поэтому ссылки ведут в учебники 5–9 (см. [[reference_textbook_sources]]). **Deep-link был СЛОМАН, починен:** статич. страницы algebra/geometry игнорировали `location.hash` (init всегда `goTo('p10')`), textbook-tracker матчил только `#pN`. Решение: `server.js` всегда инжектит `frontend/js/textbook-deeplink.js` в `/textbook/:slug` (и embed) — по `#sec-pN`/`#pN` кликает `.psel-card[data-id]` (фолбэк `.para-pill[data-para]`→goTo→scrollIntoView). Универсально, идемпотентно. План/находки: `plans/exam-textbook-links/PLAN.md` + `taxonomy.md`. Тесты: `backend/tests/exam-textbook-links.test.js` (9/9). Сделано Sonnet (фазы 2–6) + Opus-ревью (фикс классификатора, навигация, перенос таксономии из gitignore).