---
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/<slug>#sec-p<N>`.

**Классификатор (эвристика, детерминированный):** `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).
