Skip to content

Latest commit

 

History

History
948 lines (820 loc) · 96.6 KB

File metadata and controls

948 lines (820 loc) · 96.6 KB

📋 BACKLOG — идеи и незавершённые процессы

Единое место для:

  • 🚧 Незавершённые процессы — начатые работы, которые поставлены на паузу. Здесь фиксируем состояние «как есть», что осталось и где грабли, чтобы вернуться без потерь.
  • 💡 Идеи на будущее — то, что хотим сделать, но руки ещё не дошли.

Правило: когда процесс завершён — переносим краткий итог в CHANGELOG.md и удаляем отсюда. Когда идея взята в работу — двигаем в «Незавершённые процессы».


🚧 Незавершённые процессы

0. Банк МЦНМО «Задачи по геометрии» — ✅ ЗАЛИТ (2026-06-28, v3.9.110)

Статус: основное сделано и на ПРОДЕ — итог в CHANGELOG.md [3.9.110]. Здесь — только хвост-полировка.

Сделано: 17 634 задачи (origin='mccme') + 776 фасетных тегов (geometry_tags, object/method/fact) + 2514 чертежей в geometry_tasks. Миграция 1782659636. UI — переключатель «Мои / Банк МЦНМО» + фасетные фильтры в GeometryTaskList. Импортёр — pocketbase/scripts/import-mccme/build-records.mjs (gitignored, idempotent по mccme_id; конвертер формул 0% битых; sharp-склейка multi-чертежей). Старые 249 задач целы.

Осталось (полировка, не срочно):

  1. Кросс-ссылки @H<id> между задачами сейчас — текст «(задача МЦНМО №N)». Можно резолвить в коды MCCME-NNNNN / гиперссылки внутри каталога.
  2. Счётчики у фасетов — показывать число задач рядом с каждым тегом в селекторах.
  3. Вложенные подтемы — поле geometry_subtopics.parent заведено, UI-дерева ещё нет.
  4. Именные теоремы (named, 168) и источники (source, 342) — как теги НЕ залиты (лили только object/method/fact). При желании — досев из mccme-tags.json логики.
  5. Дифф→difficulty — у банка difficulty не проставлен (пусто); можно вывести из источника (Прасолов/олимпиады → 4–5).

1. Починка «лоссовых» корней \sqrt: N} в БД (оракул-верификация)

Статус: пауза. Боевая БД консистентна, лоссовые поля НЕ тронуты. Дата паузы: 2026-06-02.

Контекст. После старого парсинга sdamgia у части задач корни хранятся как \sqrt: N} — открывающая { стала :, границы аргумента (начало/конец аргумента) потеряны. Таких ~1191 задача (все имеют sdamgia_url). Детерминированно по самой БД не чинятся: \sqrt 26 (= √2·6) вместо \sqrt{26}, смещённые скобки и т.п.

Что уже сделано (закреплено):

  • ✅ Безопасное подмножество (формат начало аргумента … конец аргумента без \sqrt:, 306 полей / 188 задач) — починено скриптом pocketbase/scripts/fix-broken-roots.mjs (детерминированно, сбалансировано, KaTeX-валидно). Бэкап backup_2026-06-02_23-13-34.

Подход к лоссовым — «оракул» (pocketbase/scripts/fix-roots-oracle.mjs): перепарс задачи через /parse-sdamgia возвращает потерянные маркеры начало/конец аргументаfixLatexRoots разворачивает их в правильные {…} = ЭТАЛОН. Поле БД чиним дешёвым in-place fixLatexRoots (сохраняя born-local картинки и структуру) и ПРИМЕНЯЕМ только если математика поля совпала с эталоном. Иначе — пропуск.

🚫 НЕ запускать fix-roots-oracle.mjs --confirm — в скрипте незакрытая бага! mathCanonical() вырезает все {} → даёт ложные совпадения (в пилоте ошибочно «принял» \sqrt 26 и структурно-сломанный \sqrt{(4 \sqrt 5})^2). Запись таких = молча-неверные формулы (рендерятся без ошибки, но математически неверны).

Что осталось доделать:

  1. Заменить brace-stripping в mathCanonical на normalizeSqrtSingleToken (оборачивать \sqrt 2\sqrt{2} ТОЛЬКО для одиночного токена) + убирать только пробелы, структурные скобки сохранять. Тогда \sqrt 26/\sqrt 15 и смещённые скобки не совпадут с эталоном → корректно пропустятся. (Заготовка функции уже частично написана в комментариях/диффе, не закоммичена.)
  2. Прогнать пилот --limit 25 (dry-run), глазами проверить лог fix-roots-oracle.log.json.
  3. Бэкап БД → --confirm. Остаток (что не совпало с эталоном) — вручную через RefreshFromSdamgiaModal или отдельно.

Грабли / находки:

  • Корень бага в парсере: cleanLatexFormula не ловит ПРОБЕЛЬНЫЙ вариант маркеров начало аргумента у \sqrt (regex только под soft-hyphen и слитный формат). Перепарс воспроизводит баг → чистого результата сразу не даёт, нужен fixLatexRoots поверх. Парсер трогать пока не договаривались — но баг локализован точно.
  • Перепарс ЦЕЛИКОМ перезаписывать поле нельзя — теряются born-local картинки и структура решения. Поэтому чиним in-place, перепарс — только как эталон.
  • KaTeX throwOnError НЕ ловит кириллицу-в-формуле и \sqrt 26 (рендерятся). Поэтому главный страж — совпадение с эталоном, KaTeX/баланс/кириллица — дополнительные.
  • Доступ к боевой БД из скриптов: superuser PocketBase (коллекция _superusers, не teachers). Временного создавал/удалял через ssh root@147.45.158.148 'cd /opt/pocketbase && ./pocketbase superuser upsert|delete EMAIL PASS --dir /opt/pocketbase/pb_data'.

💡 Идеи на будущее

Сканирование бланков №1 — этап 2 (база v3.9.116 в проде, 2026-07-02)

Базовый флоу готов и задеплоен (CHANGELOG [3.9.116]): 📷 в «Моих работах» → фото бланка → gemini-2.5-flash → верификация → attempt(source='scan'). Возможные улучшения, если/когда понадобятся:

  1. Проверка на живом почерке — модель тестировалась только на синтетическом бланке (рукописные шрифты); первые реальные бланки проверить по фото внимательно. Если точность разочарует — ректификация по чёрным реперным квадратам + разрезание на строки (этап 3, скорее всего не понадобится).
  2. QR-код работы/варианта на распечатке (qrcode.react уже есть) — авто-выбор варианта при сканировании, останется выбрать только ученика.
  3. Консенсус двух моделей — прогонять параллельно gpt-5.4-mini (<1 ₽ суммарно), расхождения подсвечивать жёлтым в верификации (снижает нагрузку на глаза).
  4. Пакетная загрузка — несколько фото сразу → очередь на верификацию.
  5. Бейдж «📷 скан» у попыток source='scan' в TeacherResultsDashboard/журнале.

Рефакторинг #6 — божественные компоненты + дедуп генераторов (план, 2026-06-04)

Контекст. Завершён план чистки кода: ✅ code splitting (lazy-роуты), ✅ выпил Puppeteer, ✅ разбор pocketbase.js на pb/-модули. Остался последний пункт — логически-нагруженный, отложен на свежую сессию (нужно понимать логику каждого компонента; тесты покрывают не всё → высокий риск субтильных регрессий).

Что делать (по одному, build+npm test после каждого):

  1. Дедуп ~29 генераторов Trig*/Oral*. Образец паттерна — TrigMixedGenerator (GENERATOR_REGISTRY + hookMap + INITIAL_GEN_CONFIGS). Цель: общий каркас, чтобы новый генератор = одна запись + хук. Многие уже зовут общий useWorksheetActions/ActionButtons.
    • Сделано (2026-06-04): вынос трёх повторявшихся болерплейт-блоков в общие модули (−244 строки, 23 генератора/print-layout'а): MathInlinecomponents/shared/MathInline.jsx (15 копий); @page-печать → util utils/printPage.js::printPaged({size,margin,delayBeforePrint}) (11 копий); footer MC-теста (TrigMCSaveModal+TrigMCPrintLayout) → components/trig/TrigMCSection.jsx (13 копий, значения хука useTrigMCModal передаются пропсами — кнопку «Тест» не трогали). TrigGeneratorLayout (каркас) уже был общим раньше. Тесты 333/333, build зелёный.
    • Осталось: более глубокая registry-унификация (общий settings-schema, чтобы генератор = запись+хук). Высокий риск — у каждого генератора своя панель настроек/превью.
  2. God-компоненты >1000 строк — резать по одному, выделяя под-компоненты/хуки.
    • Сделано (2026-06-04): вынос самодостаточных «головных» под-компонентов (presentational, до главного компонента — без замыканий на его state):
      • EgeProfileVariantGenerator 1129 → 841: KIM-print-блок → EgeProfileKimPrint.jsx (KimProfileVariantPrint/ProfileAnswersPage + внутр. KimProfileCoverPage/ KimProfileTask/KimProfileTaskPage + paginateTasks + PX-константы + PART1_LAST).
      • StudentDetailPage 1110 → 921: AttemptDetails + ScoreChartStudentDetailCharts.jsx.
    • Сделано (2026-06-04, продолжение): внутренний вынос (замыкания → явный объект deps):
      • GeometryTaskList 1121 → 932: массив columns (~195 стр.) + DIFFICULTY_*geometry/GeometryTaskColumns.jsx::buildGeometryColumns({...11 deps}). Полнота deps проверена сверкой множеств идентификаторов (точное совпадение → нет пропущенных). ⚠️ Проверить в браузере: таблица задач геометрии (drag-порядок, кнопки ✎/👁/копир./🗑, превью). Build + тесты 333/333 после каждого.
    • Осталось (2 шт, самые рискованные): TaskEditModal (1201), TaskImporter (1190) — формы с переплетённым state, без головных под-компонентов и без columns-подобной границы. Резать ВНУТРЕННИЕ куски (inline-подкомпоненты/render-секции/хуки) по одному, понимая логику секции, с обязательной проверкой формы в браузере (тесты не покрывают UI-формы).

Грабли/правила (из этой сессии):

  • Проверять, что компонент реально подключён к роуту (memory feedback_verify_route_before_edit).
  • Новые роут-компоненты — через React.lazy() (memory project_facts / CLAUDE.md).
  • Не трогать публичные контракты хуков/api без проверки всех мест вызова.
  • Сеть до VPS/Pi флапает — деплой-скрипты гонять с ретраями (идемпотентны).

ОГЭ (9 класс) — развитие нового типа экзамена

Инфраструктура exam_type='oge' готова (миграция + UI + ogeTopics). Дальше (когда дойдём):

  • Засев тем ОГЭ (1–25; часть 1 = №1–19, часть 2 = №20–25) — по аналогии с ЕГЭ.
  • Отдельный генератор вариантов ОГЭ (по образцу EgeVariantGenerator).
  • Часть 2 с критериями оценивания.
  • Массовый импорт задач с math-oge.sdamgia.ru (категория «Степени и корни» = id 54).

Бенчмарк эмбеддинг-моделей — апгрейд качества векторного поиска (идея, 2026-06-06)

Контекст. Сейчас векторы считает bge-m3 (1024d) — модель выбрана из-за ограниченных ресурсов (считаем на Mac через Ollama). Если ресурсы перестанут быть узким местом — есть модели заметно сильнее на русском ретриве и на «похоже, но не дубль» (наши кейсы A2-повтор, B2-дедуп, A4-параллели).

Кандидаты (self-hosted, через Ollama):

  • qwen3-embedding:8b (4096d) / :4b (2560d) — топ MTEB Multilingual (сер. 2025), главный кандидат. Instruction-tuned (запрос префиксить, документы — нет), поддерживает Matryoshka (можно усечь до 1024d ≈ без миграции схемы).
  • snowflake-arctic-embed2 (1024d) — самый дешёвый апгрейд: ровно наша размерность, миграции vec.db на VPS не требует. Проверить первым.
  • bge-multilingual-gemma2 (9B), e5-mistral-7b-instruct — альтернативы.
  • API-максимум (если уйдём с self-hosted): Voyage voyage-3-large, Google gemini-embedding-001. Нужен отдельный embed()-адаптер с ключом, не Ollama.

Как мерить (инфраструктура уже почти есть — vector-benchmark/):

  • Самое дорогое (ручная разметка) переиспользуется: data/pairs-labeled.csv — 79 пар (44 похожи / 36 нет) с id_a;id_b — ground truth о задачах, от модели не зависит. В этих парах всего 155 уникальных задач → переэмбедить под новую модель надо ~155 текстов (даже 8B ~пара минут на Mac).
  • Дописать 5-compare.mjs: для каждой модели → embed 155 задач (переиспользуя buildText/cleanLatex) → пересчитать косинус для размеченных пар → тот же sweep порога (4-metrics.mjs) → таблица модель | dim | F1 | P | R | порог | разделимость (avg_cos похожих − avg_cos непохожих). Добавить строку qwen3-8b @1024d (Matryoshka-усечение), чтобы сразу видеть цену усечения.
  • ⚠️ Для Qwen3/e5 включить instruction-префикс на «запросной» стороне — иначе занизим.

Грабли методики:

  • Смещение выборки. Пары в pairs-labeled.csv намайнены как top-1 соседи под bge-m3 (3-pairs.mjs) → у неё фора. Быстрый прогон на текущих 79 парах = только первый сигнал; для честного вывода — заново намайнить NN под каждой моделью, разметить объединение разошедшихся пар (+30-50 пар) и мерить всех на общем множестве. Поэтому смотреть в первую очередь на разделимость, не на абсолютный F1 (N мал).
  • Прод-миграция. vec.db на VPS жёстко 1024d. Переход на 4096d = node index.mjs --full --push + миграция размерности vec_tasks + ×4 размер индекса/RAM. Matryoshka до 1024d снимает это. Сначала проверить, даёт ли вообще крупная модель прирост на наших данных, потом решать про миграцию.

Оценка: один скрипт ~120 строк, ноль новой разметки для первого прохода, ~10 мин машинного времени (на 8B). Перед запуском — ollama pull qwen3-embedding:4b (GGUF ~2.5 ГБ) / :8b (~5-6 ГБ).

Семантический поиск / дедуп для геометрических задач (идея, не решено, 2026-06-07)

Статус: идея на подумать. Что именно делать — НЕ решено. Зафиксировано, чтобы не терять разбор данных; реализовывать только если придумаем адекватную ценность, а не «потому что можем».

Проблема. geometry_tasks — отдельная сущность (~214 задач), для них векторный поиск (bge-m3) почти бесполезен: смысл задачи часто в чертеже, а не в тексте (statement_md бывает пустой/короткий). Две задачи с разными фигурами для текстовой модели выглядят одинаково → «Похожие»/дедуп/параллели по геометрии не работают by design.

Перепись данных (проверено по боевой БД 2026-06-07, 214 задач):

Источник чертежа Кол-во Путь
🟢 Реальная GeoGebra-конструкция (geogebra_base64 = base64 ZIP .ggb) 110 детерминированный парсер geogebra.xml → факты, без ИИ
🟡 Только растровый PNG (geogebra_image_base64), GGB пустой 58 VLM-описание + review-петля (как latex_needs_review)
⚪ Без чертежа, только текст (statement_md 53–292 симв.) 46 bge-m3 и так справляется, ничего не нужно

⚠️ Грабли подсчёта (наступали оба раза): geogebra_base64 почти у всех != "", но это часто пустой апплет-заготовка. И поле — это base64 от ZIP (.ggb), а не сырой XML. Чтобы посчитать реальную геометрию: base64-decode → unzip → читать geogebra.xml → считать <element type="point|segment|polygon|angle|..."> и <command>. Иначе получите ложные 212/214 (так и вышло в первой прикидке).

Что достаётся из реального geogebra.xml (переиспользуя готовый парсер ggbToSvg.js: он уже разбирает point/segment/polygon/angle и даже рисует засечки равенства mkTicks):

  • тип фигуры (из набора точек/полигонов), равные стороны (засечки ИЛИ длины по координатам), особые углы, параллельность/перпендикулярность, семантика построения (середина/биссектриса/…).
  • → каноническое текстовое описание фактов → эмбеддинг + структурный дедуп.

🚨 Стереометрия ломает метрику (важная находка, проверено). Объёмные задачи нарисованы в 2D-режиме как плоская проекция (как на доске/в тетради), НЕ в 3D-режиме GeoGebra. Значит координаты в geogebra.xml — это координаты проекции, и всё метрическое врёт:

  • Доказано на GEO-034 (куб ABCDA₁B₁C₁D₁): рёбра основания по 2D AB=2.83, BC=5.00 — разброс ×1.77, хотя у куба ВСЕ рёбра равны; ∠ABC по проекции = 135° вместо 90°.
  • Метрический детектор «равны ли стороны / прямой ли угол» на таком файле выдаёт ложь.
  • Что в проекции ВАЛИДНО: топология (граф точек↔рёбер → тип фигуры), параллельность, подписи/имена (индекс → призма/параллелепипед; вершина S/P → пирамида), штриховые рёбра, вручную поставленные засечки. И главное — у стереометрии смысл и так в основном в тексте условия (словесно), его bge-m3 ловит.

Поэтому GGB-дорожку (110) надо ветвить по типу:

  • планиметрия → 98 задач: метрика (длины/углы из координат) валидна;
  • стереометрия → 12 задач: метрику ВЫКЛючить, брать только топологию + подписи + текст. (различать по теме geometry_topics «Стереометрия» + эвристика: индексы , слова «куб/пирамида/призма/тетраэдр»). По ключевым словам стереометрии среди всех 214 ~78, но реального GGB из них мало — большинство в PNG/тексте.

Открытые вопросы (на что нет ответа):

  • Стоит ли вообще? 214 задач — мало; ручной «похожие» глазами может быть дешевле кода.
  • Если делать — ценность скорее в структурном дедупе (не плодить близнецы при пополнении базы) и в «параллельных вариантах» для геометрии, чем в семантическом поиске.
  • VLM-дорожку (58 PNG) трогать в последнюю очередь — самая дорогая и с review.
  • Возможный дешёвый старт: только планиметрия-метрика (98 задач, переиспользуя ggbToSvg.js) → каноническое описание → подмешать в buildText индексатора. Без стерео, без VLM.

Методические идеи развития (из разбора «глазами учителя», 2026-06-07)

Разбор продукта с методической точки зрения (полный цикл обучения: диагностика → планирование → объяснение → отработка → контроль → анализ → коррекция → повторение → аттестация). Главное направление — «учительское фло во времени» (КТП + журнал + календарь + заметки) — вынесено отдельным пунктом ниже. Здесь — остальные идеи по убыванию методической ценности.

🟢 Быстрые победы (малая правка — большая отдача):

  • Обратная связь ученику по ошибке. ОТВЕРГНУТО (решение пользователя, 12.07.2026). StudentResultPage СОЗНАТЕЛЬНО показывает ученику только его ошибку, без верного ответа/решения/теории: обратную связь даёт учитель. Иначе ученики списывают решение вместо того, чтобы учиться (самообучение требует самодисциплины и доступно не всем; тест = контроль намеренно). НЕ предлагать снова; фичи разбора ошибок — только в учительский контур (тепловая карта, работа над ошибками, маршрутные листы).
  • Перевод результата в школьную оценку 2–5 — настраиваемые пороги рядом с уже готовым «проходным баллом» (passing_score, миграция 1779000009). Учителю в обычной жизни нужны оценки, а не проценты.
  • Дедлайн на выдаче (сейчас сессия только бинарно «приём открыт/закрыт») + бейдж «сдал вовремя / просрочено» в «Моих работах». Закрывает ДЗ-боль; основа будущего «журнала сдачи».

🟡 Среднее (новые генераторы/режимы на готовой инфраструктуре):

  • Генератор вариантов ВПР (5–8 кл.) — инфраструктура уже есть (тег vpr, exam_type, импорт vpr5/vpr6 с sdamgia в TaskImporter). Не хватает только генератора по образцу ОГЭ/ЕГЭ. Весь фокус сейчас на выпускных классах — ВПР даёт массовую аудиторию средней школы.
  • Разноуровневая генерация для одного класса — собрать работу в 3 уровнях (базовый/средний/продвинутый) под дифференциацию ВНУТРИ класса. Сейчас «параллельные варианты» — равной сложности (анти-списывание), а не разноуровневые. Сложность у задач (difficulty 1–5) уже есть.
  • Интервальное повторение формул поверх ТДФ-карточек (flashcards уже есть): показывать формулу тем чаще, чем хуже ученик её помнит (spaced repetition). Сильный инструмент долговременного запоминания.

🔵 Крупное (новые направления):

  • Линейка отработки 5–9 класса по программе (дроби, проценты, пропорции, линейные/квадратные уравнения, начала геометрии). Устный счёт покрывает арифметику, систематической линейки по программе средней школы нет.
  • Режим фронтального опроса на уроке (Kahoot/Quizizz-стиль): задача на проекторе → ученики отвечают с телефонов в реальном времени → мгновенная диаграмма ответов класса. Марафон близок, но это синхронный инструмент «в моменте урока».
  • Персональная траектория ученика: тепловая карта (где ошибается) → автоподбор ИНДИВИДУАЛЬНОГО листа по его слабым темам (вектор-поиск для этого уже есть). Сейчас «работа над ошибками» (ClassRemediationModal, C4) — на класс, не на ученика.
  • Классификация ТИПА ошибки (вычислительная / на знак / ОДЗ / понятийная / невнимательность), а не только темы. Тепловая карта показывает «где», но не «почему» — а именно тип ошибки определяет коррекцию.

Учительское фло: КТП + журнал + календарь + заметки (проработка, 2026-06-07)

Статус: обсуждение/проработка концепции. Код не писали. Зафиксировано направление и ключевые решения, чтобы вернуться без потерь.

🚨 Главное решение (ловушка): НЕ строить второй электронный журнал. Учитель РФ обязан вести официальный (ЭлЖур / Дневник.ру / региональные ГИС) — конкурировать бессмысленно. «Журнал» в Lemma = личный РАБОЧИЙ журнал учителя (формирующий, аналитический, на своих задачах), а не реестр оценок. Оценки 2–5 — опциональный экспортируемый слой, не источник истины.

Рычаг (чего нет у конкурентов): журналы не владеют контентом; контент-платформы (ЯКласс/Учи.ру) навязывают свою закрытую методику; западные планировщики (Planbook/Chalk) без русских задач/методики; Notion «мёртвый» (не знает про твои задачи и результаты). У Lemma уже есть весь контент урока (задачи, теория, генераторы, результаты, тепловая карта). Не хватает ОСИ ВРЕМЕНИ, которая замкнёт петлю: план урока → материалы (уже генерим) → выдача классу → результаты → анализ → коррекция → обратно в план след. урока. Эту петлю не замкнёт никто, кто не владеет контентом.

Notion-стиль или жёсткая структура — гибрид (граница по природе данных):

  • КТП — жёсткая структура (регламентированная таблица, сдают завучу, нужен предсказуемый экспорт Word/PDF и автопересчёт дат). Свободные блоки тут вредны.
  • Заметки/подготовка к уроку — Notion-стиль (свободный текст, чек-листы, KaTeX уже есть, картинки, ссылки на свои задачи/теорию).
  • Календарь — связующее ПРЕДСТАВЛЕНИЕ, не отдельная сущность (показывает КТП-строки и выдачи на сетке дат).

Текущее состояние данных (проверено 2026-06-07): временно́й/структурной оси НЕТ. «Класс» = строка student_class у ученика (не сущность; нет предмета/часов/УМК/расписания). topics упорядочены (order, section, exam_type), но не привязаны к классу/году/часам. Работы (works) + сессии выдачи (work_sessions) есть (с попытками/проходным баллом, миграция 1779000009), но БЕЗ дат-дедлайнов и без привязки к уроку.

Концепт-модель (новые сущности + сцепка с существующим):

  • Класс/группа (новая сущность; мигрировать student_class): предмет + класс обучения + часов/нед + УМК + расписание. Фундамент — без неё ничего не привязать.
  • Курс/КТП = упорядоченный список ТЕМ по неделям/часам для класса+года. Переиспользует topics (не дублирует — раскладывает по времени).
  • Урок = узел КТП с датой (план/факт) + заметка (свободная) + материалы Lemma (сгенерированный лист/тест/теория + выданная work_session).
  • Журнал = НЕ новая таблица оценок, а существующие результаты (attempts/тепловая карта), пересобранные вдоль «класс × урок/дата». Оценка 2–5 — опциональная надстройка с порогами.
  • Календарь = представление: уроки (КТП) + выдачи (work_sessions с дедлайнами); drag урока → пересчёт дат «по факту» (закрывает боль №1 — пересчёт при сдвигах).

Фазирование (ценность с первого шага):

  1. Сущность «Класс/группа» + миграция student_class. Без неё ничего не стоит.
  2. Дедлайны на выдаче + «журнал сдачи» поверх существующих сессий/результатов (дёшево; честный журнал из реальных данных, без выдуманных оценок). = MVP, продающий идею.
  3. КТП как ось (темы по неделям + экспорт Word/PDF для завуча).
  4. Календарь-линза + автопересчёт дат.
  5. Заметки (Notion-стиль) на уроках/темах + методкопилка.
  6. Замыкание петли: материалы+результаты в строке урока, кнопка коррекции из плана.

Открытые вопросы (решить с пользователем):

  • Глубина журнала: честный «журнал сдачи» или полноценные оценки 2–5 с четвертями? (второе тянет в сторону ЭлЖур + дублируется официально).
  • Откуда КТП: учитель забивает сам или даём ШАБЛОНЫ под УМК (Мерзляк/Макарычев/Атанасян)? (шаблоны = огромная ценность + огромный контент-труд).
  • Привязка к ФГОС (планируемые результаты, коды) в КТП — нужна ли? (бюрократия, но без неё завуч может «не принять» план).
  • Масштаб: один учитель или школа/методобъединение? (УМК-шаблоны и общие планы тянут к школе).

🏛 Архитектура собственности (углубление, 2026-06-07) — держать в голове с самого начала:

Рамка, связывающая всё: «каталог с инструментами» → «рабочее место учителя поверх общего фонда». Сейчас Lemma центрирована на каталоге (домашний экран = «Все задачи», глобальный банк). Органайзер требует перевернуть центр тяжести: дом = ТВОЁ пространство (свои классы/неделя/план), а каталог = общий фонд-библиотека, из которого черпаешь. Это и есть «табличка vs органайзер»: табличка = ты ведёшь данные; органайзер = система ведёт тебя через твой материал (а это возможно только при наличии и общего контента, и личного контекста).

Текущее состояние (проверено 2026-06-07): собственности НЕТ вообще. Ни у works, ни у work_sessions, ни у students, ни у вариантов нет поля owner/teacher. Роли superadmin/editor/viewer = уровень ПРАВ (кто редактирует), а не ПРИНАДЛЕЖНОСТЬ (чьё). Правила PB — UI-only. Сегодня = одно общее пространство на всех.

Линия общее/личное (решение пользователя 2026-06-07):

  • Общий фонд (commons, ownerless): tasks, task_images, topics, task_contexts, geometry_tasks, теория-библиотека, теги, вектор-индекс. Дорого построенный контент, ценен общим, улучшается для всех (как банк издательства).
  • Личное (owner → teacher): классы, ученики, works, варианты ЕГЭ/ОГЭ, work_sessions, КТП/курсы, уроки, заметки, журнал. Это «как именно учитель преподаёт».

Ключевые архитектурные решения:

  1. Поле owner (relation→teachers, required) на личных коллекциях; правило owner = @request.auth.id || role = superadmin. Фонд — без владельца.
  2. Общий фонд = «читаемый-в-основном» + личный оверлей. Учитель видит commons ∪ свои личные задачи. Правка общей задачи = «предложить в фонд» (модерация) ЛИБО «форкнуть себе». Удаления из фонда — мягкие (архив, не hard delete). Это прямая защита от катастрофы (личные работы ссылаются на ID задач фонда). Инстинкт уже есть: PB не даёт удалить использованную задачу (400) — возвести в принцип.
  3. 🚨 UI-only авторизации перестаёт хватать — ЭТО форсирующий фактор. Как только у каждого учителя СВОИ ученики (ПДн детей!) и свой журнал, UI-only = учитель А читает учеников учителя Б через DevTools = утечка ПДн. Введение personal-данных = триггер сделать правила PB настоящими (owner = @request.auth.id) хотя бы на личных+PII коллекциях. CLAUDE.md это предусматривает. Тонкость: сессию выдачи владеет учитель, но ЧИТАЕТ назначенный ученик → viewRule сессии = «владелец ИЛИ назначенный ученик».
  4. Миграция из single-tenant — разовая, дешёвая: одна миграция проставляет owner = <id основного учителя> на всех существующих личных записях; фонд не трогаем.
  5. «owner сейчас, org потом». Плоская модель: учитель владеет своим пространством, фонд глобальный. НИКАКИХ школ/команд/методобъединений пока (не распыляться, ориентир на учителя-математика). НО поле owner + настоящие правила не закрывают дорогу к org (потом добавится org_id/shared_with). Проектировать так, чтобы org МОЖНО надстроить, но не строить.

Что делает это органайзером математика (а не общей табличкой), углубление:

  • Экран «Сегодня» = позвоночник (не «открой КТП», а «вот твой день: 9А 2-й урок — тема X, материалы готовы; 11Б — контрольная, сдали 18/25»). КТП/журнал/календарь — это ПРЕДСТАВЛЕНИЯ, сходящиеся в «Сегодня/Неделя».
  • Закон самонаполнения: каждая сущность преимущественно автозаполняется (КТП из УМК-шаблона, даты из расписания+каникул, журнал из результатов, «Сегодня» само), учитель только ПРАВИТ. Любая сущность, требующая ввода с чистого листа = табличка = бросят.
  • Граф пререквизитов = математическая суперсила. Математика иерархична (квадратные уравнения требуют разложения на множители и т.д.). Связать темы фонда отношением «требует» (заготовка — уже существующая вектор-близость) → органайзер становится КОПИЛОТОМ: «через 2 урока тема, требующая темы X, а 9А завалил X (62%) — добавить повторение?». Соединяет план (личное) + результаты (личное) + структуру предмета (общее) — чего нет ни у журналов, ни у Notion, ни у ЯКласс. Кусочки уже есть (ClassRemediationModal, вектор-поиск, тепловая карта) — связать осью времени+owner.
  • Capture-anywhere: быстрая заметка из любого места → инбокс → прицепить к уроку/ученику.

🛠 ИНЖЕНЕРНАЯ ПОДГОТОВКА (решено с пользователем 2026-06-07). Концепция переведена в техплан; решения по 4 развилкам зафиксированы. Код НЕ начат — нужен отдельный «поехали».

Зафиксированные решения:

  • MVP-старт = фазы 1–2: сущность «Класс/группа» (миграция student_class) + дедлайны на выдаче + «журнал сдачи» поверх существующих work_sessions/attempts.
  • Журнал = честный «журнал сдачи» (сдал/не сдал/просрочено + проценты по реальным данным). НЕ полноценные оценки 2–5 с четвертями (чтобы не превращаться во второй ЭлЖур). Оценка 2–5 — опциональная надстройка с порогами на потом, не источник истины.
  • Заметки = полноценный блочный редактор (Notion-стиль, НЕ урезанный markdown). Рекомендация: BlockNote (@blocknote/react) — Notion-UX из коробки (drag-блоки, slash-меню, чек-листы), на Tiptap/ProseMirror, markdown import/export (совместим с хранением *_md в БД), KaTeX — через кастомный блок. Альтернатива — Tiptap (больше контроля, больше кода).
  • Календарь = react-big-calendar (MIT, лёгкий, dayjs-локализация, неделя/месяц + drag).

Новые зависимости (минимум): react-big-calendar, @blocknote/react (+ble core), docx (экспорт КТП в Word на клиенте). Остальное — на текущем стеке (dayjs уже есть через antd; чек-листы/таблицы — remark-gfm уже подключён; PDF — html2pdf/window.print).

Новые сущности БД (миграции): teaching_groups (owner, name, subject, grade, hours_per_week, umk, schedule json, year) · courses/КТП (owner, group→, year, title) · ktp_entries (course→, topic→ из фонда, week_no, hours, planned_date, fgos_codes, planned_results) · lessons (owner, group→, ktp_entry→, date_plan, date_fact, status, note_md, materials json) · teacher_notes (owner, body_md, links json, is_inbox). Расширить: work_sessionsdeadline, grade_thresholds(json). Журнал — НЕ таблица, а пересборка attempts вдоль «класс × дата».

Новые страницы/роуты (новая группа меню «Моё пространство», секция workspace, визуально первая — инверсия центра тяжести с каталога; каждый роут через React.lazy()

  • ROUTE_META + при необходимости editOnly): /app/today TodayDashboard (позвоночник «Сегодня/Неделя», кандидат в дефолт /app) · /app/groups GroupManager · /app/groups/:id GroupDetail · /app/ktp KtpList · /app/ktp/:courseId KtpEditor · /app/calendar TeacherCalendar · /app/journal GradeJournal · /app/notes NotesWorkspace.

Дизайн (нужен макет, не «ещё таблица»): (1) «Сегодня/Неделя» = dashboard нового типа; (2) календарь-линза с цветовым кодированием уроков/выдач/дедлайнов; (3) КТП — рабочий + печатный (Word/PDF под завуча) режимы; (4) журнал — сетка «ученик × урок» с тепловой заливкой (переиспользует ErrorHeatmap); (5) инверсия навигации («Моё пространство» наверх, «Каталог» = фонд-библиотека). Прогнать через скиллы design.

Фундамент (заложить в MVP, не откладывать): поле owner (relation→teachers) на личных коллекциях + настоящие PB-правила owner = @request.auth.id || role="superadmin" (UI-only авторизации перестаёт хватать с появлением ПДн учеников). Фонд (tasks/topics/ task_images/geometry_tasks/теория/теги/вектор) — без владельца, read-mostly. Сессию владеет учитель, читает назначенный ученик → viewRule = «владелец ИЛИ назначенный ученик». Миграция single→multi-tenant разовая: проставить owner = <id осн. учителя> на личных записях.

✅ ФАЗА 1 ОТГРУЖЕНА (2026-06-07). Сущность «Класс/группа» в проде (l.oipav.ru):

  • Миграция 1779000010_create_teaching_groups.js — коллекция teaching_groups (owner→teachers, name, subject, grade, hours_per_week, umk, year, schedule json, archived) + nullable students.teaching_group. Аддитивно, student_class не тронут. Правила пока @request.auth.collectionName = "teachers" (фильтр по owner НЕ включён — отдельный заход). Применена на VPS после бэкапа backup_2026-06-07_11-54-48.
  • API pb/groups.js (CRUD + привязка учеников + пикер), barrel обновлён.
  • UI components/workspace/{GroupManager,GroupDetail}.jsx: список групп (CRUD/архив/ счётчик учеников), карточка группы (ученики + «привязать по совпадению» по student_class).
  • Навигация: секция workspace, группа меню «Моё пространство» (первая), роуты /app/groups + /app/groups/:groupId. Тесты 333/333, build зелёный.
  • ⚠️ Существующим editor/viewer надо выдать секцию workspace в allowed_sections (superadmin видит всё). owner-фильтрация и настоящие PB-правила — отдельным заходом.

✅ ФАЗА 2 ОТГРУЖЕНА (2026-06-07). Дедлайны + журнал сдачи в проде:

  • Миграция 1779000011_add_deadline_to_sessions.js — nullable work_sessions.deadline (date). Применена после бэкапа backup_2026-06-07_12-08-18.
  • SessionPanelDatePicker «Дедлайн сдачи» (showTime), патчит сессию.
  • API groups.getGroupJournal(groupId) — ученики группы + их попытки (expand session/work/mc_test/trig_mc_test).
  • UI components/workspace/GradeJournal.jsx — сетка «ученик × выдача»: статусы сдал/сдал-с-опозданием/незачёт/просрочено/выполняет/не-сдавал по лучшей попытке + дедлайну + passing_score. Колонки = сессии, где участвовали ученики группы (новые слева). Роут /app/journal, пункт меню «Журнал сдачи». Тесты 333/333, build зелёный.
  • Журнал матчит попытки по relation attempt.student → видит только залогиненных учеников (анонимные device-only не попадают — ожидаемо для MVP).

✅ ФАЗА 3 ОТГРУЖЕНА (2026-06-07). КТП как ось времени в проде:

  • Миграция 1779000012_create_ktp.js — коллекции courses (owner→teachers, group→teaching_groups опц., title, year, archived) + ktp_entries (course→ cascadeDelete, order, title, topic→topics опц. из фонда, hours, week_no, planned_date, planned_results, is_section). Применена после бэкапа backup_2026-06-07_12-23-00.
  • API pb/ktp.js (CRUD courses/entries + reorder). UI components/workspace/ {KtpList,KtpEditor,KtpPrintView}.jsx. Роуты /app/ktp, /app/ktp/:courseId.
  • Экспорт Word (docx@9, ленивый, utils/ktpDocx.js) + печать/PDF (A4 landscape, KtpPrintView). Связь строки КТП с темой фонда — опциональная. Тесты 333/333, build зелёный. Версия 3.9.50 в проде (фазы 1+2+3).

✅ ФАЗА 4 ОТГРУЖЕНА (2026-06-07). Уроки + календарь-линза в проде (v3.9.51):

  • Миграция 1779000013_create_lessons.js — коллекция lessons (owner→teachers, group→teaching_groups, ktp_entry→ktp_entries опц., title, date_plan, date_fact, status planned/done/cancelled, note_md, materials json). Бэкап backup_2026-06-07_12-38-34. API pb/lessons.js.
  • UI components/workspace/TeacherCalendar.jsx (+CSS) на react-big-calendar@1 (dayjs-локализатор ru): уроки + дедлайны выдач на сетке месяц/неделя/день/список. Drag → перенос даты (урок→date_plan, дедлайн→work_sessions.deadline). Создание по клику на слот, редактирование урока, фильтр по группе, тумблер дедлайнов, тёмная тема. Роут /app/calendar. Тесты 333/333, build зелёный.
  • ⚠️ note_md урока — пока обычный textarea (блочный редактор BlockNote = фаза 5). materials json и date_fact заведены в схему, UI — на будущее (фаза 6).

✅ ФАЗА 5 ОТГРУЖЕНА (2026-06-07). Заметки на блочном редакторе в проде (v3.9.52):

  • Миграция 1779000014_create_teacher_notes.jsteacher_notes (owner→teachers, title, body json [документ BlockNote], is_inbox, links json). Бэкап backup_2026-06-07_21-40-16. API pb/notes.js.
  • UI components/workspace/NotesWorkspace.jsx (+CSS) на BlockNote 0.31 (Mantine 7, React-18-совместимо — НЕ ставить 0.51+, она тянет Mantine 9 → React 19!): список + редактор, автосохранение (debounce), инбокс, фильтр Все/Инбокс, русская локаль (import { ru } from '@blocknote/core/locales'). Роут /app/notes. Ленивый чанк.
  • ⚠️ Грабли установки: @blocknote/mantine@0.51 требует React 19 (через @mantine 9). Решение — пин @blocknote/{core,react,mantine}@0.31.2 (тянет @mantine 7, React 18 ok).
  • KaTeX-блок в заметках сделан (v3.9.54) — кастомный блок math (notesMathBlock.jsx): edit-as-text / render-on-blur, вставка через slash-меню.
  • Общие заметки (v3.9.54)teacher_notes +group/+lesson/+note_date (миграция 1779000015); заметка урока из календаря = обычная заметка (getOrCreateLessonNote/app/notes?note=); класс+дата+фильтр в NotesWorkspace. textarea заметки урока в модалке убрана (единая сущность).

✅ Экран «Сегодня» ОТГРУЖЕН (v3.9.55). TodayDashboard (/app/today): уроки сегодня/неделя + дедлайны + статистика; read-mostly поверх lessons/work_sessions, без миграций. Дом учителя переключён на «Сегодня» (редирект /,/app,/app/*/app/today; раньше «Все задачи») — инверсия центра тяжести из архитектуры. Пункт меню «Сегодня» первый в «Моём пространстве».

✅ ФАЗА 6 (замыкание петли) ОТГРУЖЕНА (v3.9.56). Материалы урока: привязка работ к уроку (lessons.materials json, без миграции) + быстрый переход в редактор работы (/app/works/:id/edit — там выдача, результаты, ClassRemediationModal «работа над ошибками»). 📎 на уроках с материалами в календаре и на «Сегодня». Петля план→выдача→ результаты→коррекция замкнута существующими экранами.

owner — ОТЛОЖЕН осознанно (решение 2026-06-07): нужен только для многопользовательского/коммерческого режима (приватность ПДн между учителями). Пока один superadmin — не требуется. owner уже пишется во все новые записи → включение = смена правил PB на фильтр по owner + страховочная миграция. Дёшево, делаем перед мультиюзером.

✅ ИНТЕГРАЦИЯ ege-journal ОТГРУЖЕНА (v3.9.57). Приёмные коллекции ext_journal_exams/results/task_results (миграция 1779000016, бэкап backup_2026-06-07_22-59-01) + API pb/extjournal.js + вкладка «Решу (внешние)» в журнале (ExternalJournal — оценки 2–5 по внешним работам). Sync в репо ege-journal: scripts/sync-to-lemma.mjs (пуш дом→облако под sync-аккаунтом editor, идемпотентно, dry-run/--push) + .env.lemma.example.

  • Чтобы заработало (ручная настройка пользователя): (1) Lemma → Администрирование → Пользователи → создать journal-sync (role=editor); (2) в ege-journal скопировать .env.lemma.example.env.lemma, заполнить креды + LEMMA_OWNER_ID; (3) node --env-file=.env.lemma scripts/sync-to-lemma.mjs (dry-run) → потом --push.
  • Матчинг учеников — по student_name (у Lemma students нет telegram_id). Связка problem_id ↔ tasks.sdamgia_id хранится, тематическая аналитика по внешним — TODO.

✅ ТЕМАТИЧЕСКИЙ РАЗБОР ВНЕШНИХ РАБОТ ОТГРУЖЕН (v3.9.58). Тепловая карта ExternalThematic (Журнал → Решу → «Тепловая карта (темы)»): ученик × задание №1–21, цвет по % верных + строка «Класс». Ключевое решение: мост «решу-ошибка → тема» строится по task_number (= номер задания базового ЕГЭ) → тема ege_base, БЕЗ sdamgia_id (у базовых задач его 0; проверено). Кнопка «Работа над ошибками» собирает работу из задач слабых тем (ниже порога) → редактор работы.

🔍 sdamgia_id у базовых задач — отдельная (необязательная) задача. Проверено 2026-06-08: 10129/17665 задач с sdamgia_id (импорт sdamgia), но ege_base — 0; решу- problem_id из журнала среди задач не находятся. Для аналитики/коррекции по темам НЕ нужен (task_number достаточно). Нужен только для точного дропа «та самая задача с решу». Варианты backfill (если понадобится): (а) не делать (task_number-мост работает); (б) импортировать решу-проблемы из журнала через парсер как новые ege_base задачи с sdamgia_id (большой импорт + дедуп); (в) хранить sdamgia_id при будущих импортах базы.

✅ ТОЧЕЧНЫЙ РАЗБОР ПО ЗАДАНИЮ ОТГРУЖЕН (v3.9.59). Клик по ячейке/номеру задания в тепловой карте → модалка: кто ошибся/решил, работа/дата, ссылка на ту самую задачу на решу (mathb-ege.sdamgia.ru/problem?id=+problem_id), итог; тумблер ученик/класс; кнопка «Отработать тему». problem_id тут используется напрямую (для ссылки на решу — точному совпадению с банком Lemma не требуется).

✅ СВЯЗЬ ВНЕШНИХ ЗАДАЧ С БАНКОМ (по требованию) ОТГРУЖЕНА (v3.9.61). В разборе по заданию колонка «В Лемме»: «открыть» (TaskPreviewModal — условие+ответ+решение на месте) если есть по sdamgia_id, иначе «импортировать» (editor) → importReshuProblem (парс одной задачи решу → задача ege_base по номеру задания, born-local, многокартиночные ок). API getTaskIdsBySdamgiaIds.

🖼 Фикс парсера многокартиночных условий (v3.9.60)processCondition: при 2+ картинках оставляем inline (было: терялись все кроме первой). Старые задачи лечатся перепарсом. Детали — memory [[sdamgia_parser]].

✅ Backfill sdamgia_id для базы — СДЕЛАНО (массовый импорт каталога) 27.06.2026. Старые id-less базовые задачи (source="РЕШУ ЕГЭ — математика базовая", sdamgia_id ПУСТОЙ) НЕ трогали — вместо backfill существующих залили весь каталог базы заново с id. Скрипт scripts/import-ege-base-catalog.mjs (адаптация import-oge-sdamgia-catalog.mjs: origin mathb-ege.sdamgia.ru/prob_catalog, темы ege_base 1–21 уже были в БД, обяз. sdamgia_id, идемпотентность по sdamgia_id, троттлинг-ретраи, born-local картинки + legacy image). Каталог filter=all_a (с архивом вариантов) дал 4808 уникальных задач с sdamgia_id (parsed 5543 с пересечениями подкатегорий), 0 ошибок. Бэкап перед записью: backup_2026-06-27_21-05-13. Импорт шёл ~2 раунда (фон рвался при завершении сессии — резюм по sdamgia_id безопасен). ✅ Хвост журнала — ЗАКРЫТ (покрытие 100%). Проверено по ext_journal_task_results (VPS sqlite, anon-REST закрыт правилом): 1433 непустых уник. problem_id, из них в банке как sdamgia_idвсе 1433. Архивный filter=all_a перекрыл журнал целиком, отдельный добор НЕ нужен. (Ещё 340 строк журнала вообще без problem_id — норма.) ✅ Векторный индекс — ПОСТРОЕН (27.06.2026): cd vector-benchmark && npm run index → посчитано 4829 новых эмбеддингов, на VPS vec.db теперь 22477 векторов (/similar/health ok). Грабли прогона: (1) Ollama был выключен — поднять ollama serve; (2) better-sqlite3 собран под чужой Node → npm rebuild better-sqlite3 (Node v22/modules 127); (3) часть пачек отвалилась по таймауту VPS — повторный npm run index дослал (досверка по diff). /similar по новой задаче работает (находит near-дубли архива 100%). ✅ Дедуп-кластеры — ПОСТРОЕНЫ (27.06.2026): npm run dedup (~24 мин, O(n²) по 22477) → 694 точных + 2032 параметрических, файл /opt/pocketbase/pdf-service/dedup-clusters.json на VPS, отдаётся GET /pdf/duplicates (645 exact + 2031 param в очереди — размеченные/used скрыты). Видны во вкладке B2 «🔎 Похожие (вектор)». Остатки (по желанию): (а) разобрать кластеры B2 вручную (ревью дублей); (б) старые id-less базовые задачи решить, гасить ли (осторожно — они в выданных работах).

🔭 Учительское фло — направления на будущее (зафиксировано 2026-06-08)

Каркас органайзера собран и в проде (фазы 1–6 + интеграция ege-journal + тематический разбор). Незакрытые направления — здесь, с деталями для старта «с нуля».

1. Массовый предзалив 1443 решу-задач (чтобы «открыть» в Лемме было сразу везде).

  • Сейчас связь работает on-demand (v3.9.61): кнопка «импортировать» в разборе. Массовый предзалив сделает ссылку «открыть» доступной сразу для всех внешних задач.
  • Объём: 1443 уник. problem_id (в журнале exam_tasks, ~67/задание). Каждый → парс + createTask + картинки.
  • Где запускать: на VPS (надёжная сеть к sdamgia + к pdf-service localhost:3001 + к PB). Новый скрипт pocketbase/scripts/import-reshu-bulk.mjs по образцу логики ege-tasks/src/utils/importReshuProblem.js, но серверный (REST + superuser PB-auth, как другие pocketbase/scripts/*.mjs).
  • Алгоритм: взять distinct problem_id+task_number (из ext_journal_task_results или из журнала); для каждого, если задачи с таким sdamgia_id ещё нет (getTaskIdsBySdamgiaIds-эквивалент), POST /parse-sdamgia (problem URL mathb-ege.sdamgia.ru/problem?id=X) → создать ege_base-задачу (тема по task_number, sdamgia_id=X, born-local картинки через /fetch-image). Идемпотентно (skip по sdamgia_id), резюмируемо (лог обработанных), троттлинг 2–3 сек (DDoS-guard решу). Оценка ~1–1.5 ч. Парсер уже чинит многокартиночные (v3.9.60).
  • Грабля: owner новых задач — это общий фонд (tasks ownerless), не личное. ОК.

2. Добивки (мелкие, независимые):

  • Cron авто-синка журнала на Pi. Сейчас sync ручной (node --env-file=.env.lemma scripts/sync-to-lemma.mjs --push в /home/faust/apps/ege-journal-src). Добавить crontab на Pi (напр. раз в день/ночью). Идемпотентно; локальный PB журнала (:8090) должен работать. .env.lemma уже на Pi (права 600).
  • Виджет «недавние результаты» на «Сегодня» (TodayDashboard). Карточка с последними N сдачами/попытками (свежие attempts со статусом submitted) — кто что сдал за последние дни. Данные есть; добавить блок рядом с дедлайнами.
  • Drag&drop строк КТП (KtpEditor). Сейчас перестановка кнопками ↑/↓ + reorderKtpEntries. Добавить HTML5-DnD (паттерн useTaskDragDrop) для перетаскивания строк; на drop — тот же reorderKtpEntries(orderedIds).

3. owner-фундамент (перед мультиюзером/коммерцией).

  • Зачем: приватность ПДн между учителями. Пока один superadmin — не нужно (см. решение выше). Включать при появлении вторго реального учителя.
  • Готовность: owner уже пишется во ВСЕ личные коллекции (teaching_groups, courses, ktp_entries, lessons, teacher_notes, ext_journal_*). Осталось включить правила.
  • Что делать: правила PB личных коллекций → owner = @request.auth.id || @request.auth.role = "superadmin" (list/view/create/update/delete). Для work_sessions viewRule = «владелец ИЛИ назначенный ученик» (тонкость: ученик читает сессию). Миграция-подстраховка: проставить owner = <id осн. учителя> где пусто. Риск: можно скрыть данные — тестировать тщательно, с бэкапом. Фонд (tasks/topics/task_images/geometry_*/теория/теги/ вектор) — НЕ трогать (остаётся ownerless/общий).
  • ext_journal_ owner:* сейчас = sync-аккаунт journal-sync (9x5xcgy0uxgt3t7). Перед включением owner-фильтрации либо перепривязать на осн. учителя (миграция), либо задать LEMMA_OWNER_ID в .env.lemma журнала + переслать (sync идемпотентен → обновит owner).

Прочее (по мелочи): объединить тепловые карты Lemma-сессий (ErrorHeatmap) и внешних в одну; авто-даты уроков из расписания группы (когда заведём расписание в teaching_groups.schedule); KaTeX inline-формулы в заметках (сейчас блок-формула).

Интеграция с ege-journal (внешние результаты решу.ЕГЭ, решено 2026-06-07)

Второе приложение пользователя: ~/Documents/APP/ege-journal — ОТДЕЛЬНЫЙ проект (свой git/CLAUDE.md, React+TS+Vite+Tailwind+Recharts, локальный PocketBase :8090, работает только в домашней сети). Учитель импортит в него xlsx-выгрузки с решу.ЕГЭ и строит статистику. Есть Telegram-бот (bot/index.mjs, grammy) и готовый MCP-сервер (mcp-server/index.mjs: tools ege_list_groups/get_debts/get_scores/get_student/ get_error_patterns/...). Остаётся отдельным — Lemma только ПОЛУЧАЕТ данные.

Модель данных журнала: groups(name) · students(name, group, telegram_id) · exams(exam_id решу, date, group, task_count) · exam_tasks(exam, task_number 1–21, problem_id решу) · student_results(grade 2–5, correct_count, part1_score, did_not_take) · student_answers(task_number, is_correct). Это слой контроля по внешним работам (потасковая верность), которого у Lemma нет.

🔑 Золотая связка: exam_tasks.problem_id (решу) tasks.sdamgia_id (Lemma). Ошибка на решу-задаче → задача в фонде Lemma → её тема/решение/теория → питает петлю «анализ → коррекция» РЕАЛЬНЫМИ данными, не только тестами, выданными через Lemma.

⚡ Ограничение, диктующее архитектуру: журнал — только дома (http://127.0.0.1:8090), Lemma-фронт — в облаке (https://l.oipav.ru, отовсюду). Вкладка l.oipav.ru НЕ может читать локальный http (mixed-content + CORS + дом недоступен снаружи). Поэтому «живое чтение из UI Lemma» невозможно — интеграция строится «из дома наружу».

Зафиксированные решения (2026-06-07):

  • Механизм = пуш из журнала + файл-фолбэк. Журнал (дома) аутентифицируется в облачный PB Lemma (токен учителя) и заливает агрегаты исходящим HTTPS → нет входящих в домашнюю сеть. Плюс ручной экспорт JSON ↔ импорт в Lemma как запасной путь. (MCP-сервер журнала — комплементарный слой для будущего AI-копилота Lemma, не структурный синк.)
  • Ученики — одни и те же люди → единый профиль. Матчинг: по telegram_id (сильный ключ), фолбэк — имя в пределах группы; поле external_journal_student_id на ученике Lemma + UI-реконсиляции для неоднозначных. Группа журнала → teaching_group.
  • Гранулярность = потасковая (полная) — тянем student_answers + problem_id, чтобы зажечь тематическую тепловую карту и автокоррекцию через problem_id↔sdamgia_id.

Что добавляется в Lemma (VPS, owner=учитель): коллекции ext_journal_exams / ext_journal_results / ext_journal_task_results (хранят problem_id). Идемпотентный upsert по ключу (exam_id, student, task_number). Импорт-экран для файл-фолбэка.

Что добавляется в ege-journal (отдельный репо): действие «Синхронизировать с Lemma» (+ опц. cron): login в облачный PB Lemma → upsert агрегатов; экспорт JSON. Outbound-only.

Встраивание в учительское фло: журнал сдачи (фаза 2) получает ВТОРОЙ источник (внешние решу-контрольные рядом с выдачами Lemma, единый вид «класс × дата»); тепловая карта/коррекция (C4) — реальные проваленные задания → темы фонда → автоповтор; TodayDashboard — «11Б: решу-контрольная, средний 3.4, слабое №13».

Китайский след (Gaokao / 中国) — практики и встраивание (исследование, 2026-06-07)

Зачем смотрели: гипотеза пользователя — в гаокао непропорционально много функций; «китайцы что-то знают про будущее». Проверили по источникам — гипотеза верна, и это НЕ тангенс к органайзеру, а его усиление (наполняет смыслом ось времени + граф связей).

Почему функции (расшифровка «секрета»): реформа 2017/2018 (新课标) сместила центр с «тем» на 6 ключевых компетенций (мат. абстракция, лог. рассуждение, мат. моделирование, простр. воображение, вычисления, анализ данных); экзамен — с зубрёжки на применение+рассуждение. Всё это про ЗАВИСИМОСТИ величин, а функция = атом зависимости («вход→выход») = ментальная модель данных/алгоритмов/ML. Ставка не на «функции-тему», а на функциональное (реляционное) мышление как базовую грамотность вычислительного века. Совпадает с российским трендом: ОГЭ практический блок 1–5 (у Lemma уже есть!) + «функциональная грамотность» PISA. Не копируем чужое — попадаем в общий тренд, где Китай раньше/системнее.

Переносимые практики (механика, НЕ культура-прессинг) → маппинг на Lemma:

  1. 变式 Bianshi — обучение через вариацию (ГЛАВНОЕ). Не одна задача, а выстроенная последовательность вариаций («一题多变, одна задача — много изменений»). Два вида: процедурная (меняем числа/шаги = что наши генераторы делают СЕЙЧАС = дрилл) и концептуальная (меняем ПРЕДСТАВЛЕНИЕ / контекст / что неизвестно → строим реляционное понимание). → Апгрейд ~29 генераторов: от процедурной к концептуальной вариации. Та же функция как формула↔график↔таблица↔словами; менять «что дано/найти»; менять контекст (чистая матем.↔реальная). KaTeX+GeoGebra уже есть → мульти-представления на одной задаче. Самый родной для Lemma апгрейд: генератор из «бесконечного задачника» → инструмент построения понимания.
  2. 6 компетенций вместо «просто тем» → лёгкая метадата «компетенция/представление» на задаче → копилот собирает лист, сбалансированный ПО КОМПЕТЕНЦИЯМ, не только теме. Вскрывает пробел: у нас сильно «вычислить/решить», слабо «прочитать график/построить модель». (Связано с идеей «классификация типа ошибки», но в позитивном ключе.)
  3. 通性通法 — общие методы важнее трюков → акцент на разбор РЕШЕНИЯ как переносимого метода (ложится на «обратную связь ученику» + теорию).
  4. Вертикальная связность (承上启下), функция = её позвоночник → подтверждает идею ГРАФА ПРЕРЕКВИЗИТОВ (сердце копилота из блока архитектуры). Связность — не украшение, а ядро; функции — естественный главный ствол графа.
  5. 教研 jiaoyan — коллективная доработка уроков → методическое ОБОСНОВАНИЕ будущего org-слоя (школа/методобъединение): он нужен не для админки, а чтобы учителя ВМЕСТЕ оттачивали КТП и наборы вариаций. Не строим сейчас — но знаем зачем.

Как сходится в нашу рамку: bianshi→апгрейд генераторов; связность+функции→граф пререквизитов; компетенции→измерение «баланс навыков» для копилота; jiaoyan→смысл org; функц. грамотность→контентная линия (совпадает с ОГЭ-реформой). Китай не добавляет сущность — наполняет смыслом ось времени и граф связей, что уже спроектированы.

Контентная линия-кандидат: «функциональное мышление» 5–11 (мульти-представления зависимостей, чтение графиков, моделирование реальных ситуаций). Совпадает и с гаокао, и с российской функц. грамотностью.

⚠️ Оговорка: не карго-культ. Их результаты держатся на тренировке учителей и культуре (软том не перенести). Берём механику (вариация, мульти-представления, связность, моделирование) — НЕ берём прессинг/зубрёжку.

BYO-LLM — свой endpoint и токен учителя для AI-функций (идея, 2026-06-07)

Идея пользователя. Дать учителю (на будущее, когда приложение пойдёт в продакшен и станет коммерческим) возможность подключить свой LLM — указать endpoint + токен + модель — и пользоваться всеми AI-фичами через своего провайдера, а не через захардкоженный Timeweb/deepseek. Мотивация: кто-то подключит Claude/GPT вместо deepseek → качество latex-fix, маршрутных листов и будущих AI-штук вырастет. Это «Bring Your Own Key / Model» (BYOK). Сейчас функционал — настройка, после которой «открывается» AI.

Почему это дёшево встроить (исследование кодовой базы, 07.06.2026):

  • Вся LLM-логика проходит через одну серверную ручку POST /latex-fix (pdf-service.js:996). Она уже OpenAI-совместима (/chat/completions, Authorization: Bearer, {model, messages, temperature}) — значит Claude (через OpenAI-совместимый шлюз/прокси), GPT, deepseek, локальная Ollama подключаются единым контрактом.
  • Конфиг сейчас — три env: TIMEWEB_AI_URL / TIMEWEB_AI_KEY / TIMEWEB_AI_MODEL (systemd override). Захардкожен ровно в одном месте (строки 1006–1008).
  • Фронт зовёт ручку из 4 мест: TaskImporter.jsx:168, TaskEditModal.jsx:113, RefreshFromSdamgiaModal.jsx:65, useTaskImport.js:303. Базовый URL pdf-service вычисляется одинаково (VITE_PDF_SERVICE_URL || /pdf).
  • Эмбеддинги (bge-m3/Ollama) и векторный поиск — отдельный пайплайн, под BYO-LLM НЕ попадают (там не чат, а векторизация; менять провайдера эмбеддингов рискованно — размерность/совместимость индекса). BYO-LLM = только про генеративные/чат фичи.

Как может быть реализовано (эскиз, не финал):

  1. Хранение настроек. Новое поле в коллекции teachers (или отдельная коллекция ai_providers): ai_provider: json { url, model, key_enc, enabled, label }.

    • 🚨 Токен — секрет. Не хранить в открытом виде во фронте/БД. Варианты: (а) шифровать на сервере (pdf-service владеет ключом шифрования из env), фронт отправляет ключ один раз при сохранении настройки, дальше — только enabled+маска sk-...abcd; (б) самый безопасный — хранить ключ ТОЛЬКО на сервере, фронт его никогда не получает обратно. Учитывая что репо публичный и платформа приватная — минимум: ключ не светить в бандле, не логировать, отдавать наружу маску.
    • Per-teacher (личный ключ) vs per-instance (один общий для школы) — на старте достаточно per-superadmin/per-instance; per-teacher = следующий шаг (тарификация «каждый платит за свой токен»).
  2. Серверная ручка /latex-fix (и будущие AI-ручки). Принимать опциональный override: либо из тела запроса ({ text, ai: {url, model} } + ключ берётся из расшифрованной настройки учителя по auth-токену), либо ручка сама подтягивает настройку залогиненного учителя. Приоритет: настройка учителя → env (fallback на дефолтный Timeweb). Сохранить текущее поведение 503, если ничего не настроено.

    • ⚠️ Кэш /latex-fix сейчас по sha256(text) — при мультипровайдере ключ кэша должен включать model/provider, иначе ответ одной модели подменит другую.
    • Тайм-ауты/ретраи: у Claude/GPT задержки и форматы ошибок другие — нормализовать.
  3. UI настроек. Раздел «AI / Интеграции» в настройках (доступ editOnly/superadmin по правилам Authorization из CLAUDE.md): поля endpoint/model/key, кнопка «Проверить соединение» (тестовый прогон latex-fix на короткой формуле), индикатор статуса, маска ключа. При выключенном — AI-кнопки в UI прячутся/дизейблятся (через enabled флаг).

  4. Абстракция провайдера (рефакторинг под будущее). Сейчас один промпт (LATEX_FIX_SYSTEM_PROMPT) зашит в latex-fix. По мере роста AI-фич (маршрутные листы, обратная связь ученику, разбор решений) — вынести «вызов LLM» в общий модуль pdf-service (callLLM({provider, system, user, opts})), чтобы новые фичи получали BYO-LLM бесплатно. Сейчас стоит хотя бы пометить: новые генеративные ручки → через единый хелпер выбора провайдера, не копипастить env-чтение.

Риски / на что смотреть:

  • Безопасность ключей (главное) — см. п.1.
  • Совместимость: не все провайдеры строго OpenAI-совместимы (Anthropic native API отличается форматом messages/headers). Для Claude проще требовать OpenAI-совместимый шлюз, либо добавить адаптер provider_type: openai|anthropic.
  • Стоимость/лимиты на стороне учителя — наша ответственность только «прокинуть запрос».
  • Кэш и приватность: кэш latex-fix общий in-memory — при per-teacher ключах не смешивать.

Связано: [[project_ollama_define]] (Timeweb AI gateway), [[sdamgia_parser]] (latex-fix), CLAUDE.md § «sdamgia parser» (env-переменные), § Authorization (правила добавления настроек/секций). Эмбеддинги — НЕ сюда ([[vector_search]]).

Теги идей по решениям + семантический поиск (часть 2 профиля) (идея, 2026-06-19)

Контекст / зачем. Возникло при работе над показателем решаемости (v3.9.90, [[difficulty_signal]]). Для задач части 2 профиля (№13–19) из условия метод решения не виден, а часто и в принципе не выводится. А вот из solution_md — виден: «оценка+неравенство», «замена переменной», «выход на монотонность», «признак делимости», «ГМТ» и т.п. Идея: классифицировать/искать задачи по идее решения, а не по условию. Это удачное применение вектора/LLM (в отличие от переноса сложности, который отвергнут — см. [[difficulty_signal]]).

Два слоя (рекомендуется делать оба, начать с тегов):

  1. LLM-теги методов (главное, человекочитаемо). Прогон solution_md (+опц. criteria_md) через существующий LLM-шлюз (Timeweb deepseek, тот же, что /latex-fix; см. [[sdamgia_parser]], [[project_ollama_define]]) → структурный список тегов идей. Хранить в новом поле tasks.method_tags (json) или через справочник тегов. Даёт фильтр/поиск каталога по методу — сразу полезно учителю. Прогон одноразовый + при импорте; кэш по sha256(solution) как в /latex-fix.
  2. Эмбеддинг решений (semantic «ещё похожие по идее»). Отдельная таблица vec_solutions (vec0, та же схема, что vec_tasks) на эмбеддингах solution_md (или condition+solution). Переиспользует /similar-машинерию ([[vector_search]]). Это «дай задачи с похожим решением».

Объём — маленький: часть 2 профиля = сотни задач, не тысячи (топики ege_profile, exam_part=2, №13–19). LLM-прогон дёшев и кэшируем. ⚠️ Проверить покрытие solution_md у этих задач (на момент идеи SSH к VPS лежал — не снял цифру). Скорее всего высокое: парсер части 2 всегда тянет решение.

Питает на будущее: подбор задач по методу, работа над ошибками «по идее», параллельные варианты «той же идеи», тематические наборы по технике.

Старт: проверить покрытие solution_md → прототип LLM-тегирования на ~20 задачах (оценить качество тегов) → поле method_tags + UI-фильтр в каталоге → (опц.) vec_solutions + «похожие по решению».

Синхронизация Лемма ↔ Решу ЕГЭ (backfill sdamgia_id) (идея, 2026-06-19)

Проблема (цифры с прода, 18.06.2026; см. [[difficulty_signal]]). Связь задач Леммы с Решу идёт строго по tasks.sdamgia_id = problem_id, НЕ по тексту.

  • Всего задач 17 697; с sdamgia_id — 10 162; без id — 7 535 (лились старым парсером, не сохранявшим решу-id).
  • Журнал Решу (ext_journal_task_results) знает 1 434 уник. problem_id, но совпадает с каталогом лишь 219 задач → решаемость с Решу почти не подключена.

Идея пользователя: идти по каталогу Решу, для каждой задачи искать такую же в Лемме; есть → проставить id, нет → импортировать новой; мусор/дубли причёсывать коллективно учителями в течение года.

Корректировка (важно — иначе раздуем каталог): это задача сопоставления сущностей, и вот тут вектор как раз силён (на этом уже работает дедуп B2//pairs, [[vector_search]]). Поэтому:

  1. Backfill, а не импорт по умолчанию. Парсим решу-задачу (/parse-sdamgia), эмбедим (bge-m3, Mac), ищем ближайшую в Лемме. cos ≥ ~0.94 И совпал ответ → проставляем sdamgia_id существующей задаче. Нет совпадения → импорт новой (born-local). Так дубли создаются только для реально новых задач.
  2. Сначала по журналу, не по всему Решу. Целевой список — ~1 215 problem_id из журнала без связи (1434 − 219). Ограниченный объём + сразу даёт сигнал решаемости. Сплошной обход каталога Решу — позже и точечно (десятки тысяч задач, 99% не в Лемме → сплошной импорт = взрыв мусора).
  3. Очередь ревью + провенанс. Авто-матч по тексту даёт ложные срабатывания (у базы много задач-«близнецов»: тип один, числа разные). Поэтому: высокий cos → авто-связь с пометкой id_source='auto'; средний → очередь учителю (UI как VectorDuplicatesTab); низкий → импорт. Пометка позволяет отозвать ошибочные связи. ⚠️ Без подтверждения ответом не клеить — иначе загрязним и решаемость.

Риски: ложные склейки разных задач; нагрузка на Решу при массовом парсе (через /fetch-image-подобный rate-limit); рост дублей при импорте (смягчается born-local

  • существующий дедуп). Деструктивных операций с БД нет — только добавление id и новых задач (бэкап перед батчем обязателен, см. правило БД).

Старт: батч-скрипт в vector-benchmark/ (parse журнал-проблемы → embed → NN в vec.db → авто/очередь/импорт по порогам, dry-run по умолчанию) → миграция поля tasks.id_source → UI-очередь ревью.

Известные дефекты данных

  • tasks.code НЕ уникален. Найдено при отладке ОГЭ (06.06.2026): фильтр code="3-028" вернул ЧУЖУЮ задачу (другой темы). Формат кода {ege_number}-{seq} считает порядковый номер в пределах темы, но коллекций тем много (ege_base / ege_profile / oge / trig …), и {N}-{seq} пересекается между ними. Правило: нигде не полагаться на уникальность code — фильтровать/находить задачи по id (или topic/context). Если понадобится — рассмотреть префикс кода типом экзамена (oge-3-028) либо составной уникальный индекс (topic, code).