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

13 KiB
Raw Permalink Blame History

План: точная привязка задач экзамена 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.subtopicexam_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 главы.
  • Статические страницы 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, и клик реально открывает этот §.


Централизованный хелпер, внедряемый СЕРВЕРОМ в 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-powersalgebra-7-ch1 §1 (натур.), §2 (целый показатель), §3 (станд. вид).
  • alg-expressionsalgebra-7-ch2 §45 / algebra-9-ch1 §5 (преобр. рацион.).
  • alg-polynomialsalgebra-7-ch2 §8–14 (одночлены/многочлены/ФСУ/разложение).
  • alg-fractionsalgebra-9-ch1 §1–5 (рацион. дроби) ИЛИ math-6 (обыкн. дроби) по тексту.
  • alg-equations → линейные algebra-7-ch3 §15; квадратные algebra-8-ch2 §7–9; дробно-рацион. algebra-9-ch3 §10; системы algebra-7-ch4 §23–24 — по тексту.
  • alg-inequalitiesalgebra-8-ch3 §13–18; метод интервалов algebra-9-ch3 §13 / algebra-8-ch3 §17.
  • alg-functions → линейная algebra-7-ch3 §19–20; свойства/графики algebra-9-ch2 §69.
  • alg-progressionsalgebra-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-quadrilateralsgeometry-8-ch1 §4–16 (паралл./прямоуг./ромб/квадрат/трапеция); вписанные/описанные geometry-9-ch2 §9.
  • geom-circlegeometry-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_slugtopic_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>.