Единое место для:
- 🚧 Незавершённые процессы — начатые работы, которые поставлены на паузу. Здесь фиксируем состояние «как есть», что осталось и где грабли, чтобы вернуться без потерь.
- 💡 Идеи на будущее — то, что хотим сделать, но руки ещё не дошли.
Правило: когда процесс завершён — переносим краткий итог в
CHANGELOG.mdи удаляем отсюда. Когда идея взята в работу — двигаем в «Незавершённые процессы».
Статус: основное сделано и на ПРОДЕ — итог в 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 задач целы.
Осталось (полировка, не срочно):
- Кросс-ссылки
@H<id>между задачами сейчас — текст «(задача МЦНМО №N)». Можно резолвить в кодыMCCME-NNNNN/ гиперссылки внутри каталога. - Счётчики у фасетов — показывать число задач рядом с каждым тегом в селекторах.
- Вложенные подтемы — поле
geometry_subtopics.parentзаведено, UI-дерева ещё нет. - Именные теоремы (
named, 168) и источники (source, 342) — как теги НЕ залиты (лили только object/method/fact). При желании — досев изmccme-tags.jsonлогики. - Дифф→difficulty — у банка
difficultyне проставлен (пусто); можно вывести из источника (Прасолов/олимпиады → 4–5).
Статус: пауза. Боевая БД консистентна, лоссовые поля НЕ тронуты. Дата паузы: 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). Запись таких =
молча-неверные формулы (рендерятся без ошибки, но математически неверны).
Что осталось доделать:
- Заменить brace-stripping в
mathCanonicalнаnormalizeSqrtSingleToken(оборачивать\sqrt 2→\sqrt{2}ТОЛЬКО для одиночного токена) + убирать только пробелы, структурные скобки сохранять. Тогда\sqrt 26/\sqrt 15и смещённые скобки не совпадут с эталоном → корректно пропустятся. (Заготовка функции уже частично написана в комментариях/диффе, не закоммичена.) - Прогнать пилот
--limit 25(dry-run), глазами проверить логfix-roots-oracle.log.json. - Бэкап БД →
--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'.
Базовый флоу готов и задеплоен (CHANGELOG [3.9.116]): 📷 в «Моих работах» → фото бланка → gemini-2.5-flash → верификация → attempt(source='scan'). Возможные улучшения, если/когда понадобятся:
- Проверка на живом почерке — модель тестировалась только на синтетическом бланке (рукописные шрифты); первые реальные бланки проверить по фото внимательно. Если точность разочарует — ректификация по чёрным реперным квадратам + разрезание на строки (этап 3, скорее всего не понадобится).
- QR-код работы/варианта на распечатке (qrcode.react уже есть) — авто-выбор варианта при сканировании, останется выбрать только ученика.
- Консенсус двух моделей — прогонять параллельно gpt-5.4-mini (<1 ₽ суммарно), расхождения подсвечивать жёлтым в верификации (снижает нагрузку на глаза).
- Пакетная загрузка — несколько фото сразу → очередь на верификацию.
- Бейдж «📷 скан» у попыток source='scan' в
TeacherResultsDashboard/журнале.
Контекст. Завершён план чистки кода: ✅ code splitting (lazy-роуты), ✅ выпил
Puppeteer, ✅ разбор pocketbase.js на pb/-модули. Остался последний пункт —
логически-нагруженный, отложен на свежую сессию (нужно понимать логику каждого
компонента; тесты покрывают не всё → высокий риск субтильных регрессий).
Что делать (по одному, build+npm test после каждого):
- Дедуп ~29 генераторов
Trig*/Oral*. Образец паттерна —TrigMixedGenerator(GENERATOR_REGISTRY+hookMap+INITIAL_GEN_CONFIGS). Цель: общий каркас, чтобы новый генератор = одна запись + хук. Многие уже зовут общийuseWorksheetActions/ActionButtons.- ✅ Сделано (2026-06-04): вынос трёх повторявшихся болерплейт-блоков в общие
модули (−244 строки, 23 генератора/print-layout'а):
MathInline→components/shared/MathInline.jsx(15 копий);@page-печать → utilutils/printPage.js::printPaged({size,margin,delayBeforePrint})(11 копий); footer MC-теста (TrigMCSaveModal+TrigMCPrintLayout) →components/trig/TrigMCSection.jsx(13 копий, значения хукаuseTrigMCModalпередаются пропсами — кнопку «Тест» не трогали).TrigGeneratorLayout(каркас) уже был общим раньше. Тесты 333/333, build зелёный. - ⏳ Осталось: более глубокая registry-унификация (общий settings-schema, чтобы генератор = запись+хук). Высокий риск — у каждого генератора своя панель настроек/превью.
- ✅ Сделано (2026-06-04): вынос трёх повторявшихся болерплейт-блоков в общие
модули (−244 строки, 23 генератора/print-layout'а):
- God-компоненты >1000 строк — резать по одному, выделяя под-компоненты/хуки.
- ✅ Сделано (2026-06-04): вынос самодостаточных «головных» под-компонентов
(presentational, до главного компонента — без замыканий на его state):
EgeProfileVariantGenerator1129 → 841: KIM-print-блок →EgeProfileKimPrint.jsx(KimProfileVariantPrint/ProfileAnswersPage+ внутр.KimProfileCoverPage/KimProfileTask/KimProfileTaskPage+paginateTasks+ PX-константы +PART1_LAST).StudentDetailPage1110 → 921:AttemptDetails+ScoreChart→StudentDetailCharts.jsx.
- ✅ Сделано (2026-06-04, продолжение): внутренний вынос (замыкания → явный объект deps):
GeometryTaskList1121 → 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-формы).
- ✅ Сделано (2026-06-04): вынос самодостаточных «головных» под-компонентов
(presentational, до главного компонента — без замыканий на его state):
Грабли/правила (из этой сессии):
- Проверять, что компонент реально подключён к роуту (memory
feedback_verify_route_before_edit). - Новые роут-компоненты — через
React.lazy()(memoryproject_facts/ CLAUDE.md). - Не трогать публичные контракты хуков/
apiбез проверки всех мест вызова. - Сеть до VPS/Pi флапает — деплой-скрипты гонять с ретраями (идемпотентны).
Инфраструктура exam_type='oge' готова (миграция + UI + ogeTopics). Дальше (когда дойдём):
- Засев тем ОГЭ (1–25; часть 1 = №1–19, часть 2 = №20–25) — по аналогии с ЕГЭ.
- Отдельный генератор вариантов ОГЭ (по образцу
EgeVariantGenerator). - Часть 2 с критериями оценивания.
- Массовый импорт задач с
math-oge.sdamgia.ru(категория «Степени и корни» = id 54).
Контекст. Сейчас векторы считает 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, Googlegemini-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 ГБ).
Статус: идея на подумать. Что именно делать — НЕ решено. Зафиксировано, чтобы не терять разбор данных; реализовывать только если придумаем адекватную ценность, а не «потому что можем».
Проблема. 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₁): рёбра основания по 2DAB=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.
Разбор продукта с методической точки зрения (полный цикл обучения: диагностика → планирование → объяснение → отработка → контроль → анализ → коррекция → повторение → аттестация). Главное направление — «учительское фло во времени» (КТП + журнал + календарь + заметки) — вынесено отдельным пунктом ниже. Здесь — остальные идеи по убыванию методической ценности.
🟢 Быстрые победы (малая правка — большая отдача):
Обратная связь ученику по ошибке.ОТВЕРГНУТО (решение пользователя, 12.07.2026).StudentResultPageСОЗНАТЕЛЬНО показывает ученику только его ошибку, без верного ответа/решения/теории: обратную связь даёт учитель. Иначе ученики списывают решение вместо того, чтобы учиться (самообучение требует самодисциплины и доступно не всем; тест = контроль намеренно). НЕ предлагать снова; фичи разбора ошибок — только в учительский контур (тепловая карта, работа над ошибками, маршрутные листы).- Перевод результата в школьную оценку 2–5 — настраиваемые пороги рядом с уже
готовым «проходным баллом» (
passing_score, миграция 1779000009). Учителю в обычной жизни нужны оценки, а не проценты. - Дедлайн на выдаче (сейчас сессия только бинарно «приём открыт/закрыт») + бейдж «сдал вовремя / просрочено» в «Моих работах». Закрывает ДЗ-боль; основа будущего «журнала сдачи».
🟡 Среднее (новые генераторы/режимы на готовой инфраструктуре):
- Генератор вариантов ВПР (5–8 кл.) — инфраструктура уже есть (тег
vpr,exam_type, импортvpr5/vpr6с sdamgia вTaskImporter). Не хватает только генератора по образцу ОГЭ/ЕГЭ. Весь фокус сейчас на выпускных классах — ВПР даёт массовую аудиторию средней школы. - Разноуровневая генерация для одного класса — собрать работу в 3 уровнях
(базовый/средний/продвинутый) под дифференциацию ВНУТРИ класса. Сейчас «параллельные
варианты» — равной сложности (анти-списывание), а не разноуровневые. Сложность у
задач (
difficulty1–5) уже есть. - Интервальное повторение формул поверх ТДФ-карточек (flashcards уже есть): показывать формулу тем чаще, чем хуже ученик её помнит (spaced repetition). Сильный инструмент долговременного запоминания.
🔵 Крупное (новые направления):
- Линейка отработки 5–9 класса по программе (дроби, проценты, пропорции, линейные/квадратные уравнения, начала геометрии). Устный счёт покрывает арифметику, систематической линейки по программе средней школы нет.
- Режим фронтального опроса на уроке (Kahoot/Quizizz-стиль): задача на проекторе → ученики отвечают с телефонов в реальном времени → мгновенная диаграмма ответов класса. Марафон близок, но это синхронный инструмент «в моменте урока».
- Персональная траектория ученика: тепловая карта (где ошибается) → автоподбор
ИНДИВИДУАЛЬНОГО листа по его слабым темам (вектор-поиск для этого уже есть). Сейчас
«работа над ошибками» (
ClassRemediationModal, C4) — на класс, не на ученика. - Классификация ТИПА ошибки (вычислительная / на знак / ОДЗ / понятийная / невнимательность), а не только темы. Тепловая карта показывает «где», но не «почему» — а именно тип ошибки определяет коррекцию.
Статус: обсуждение/проработка концепции. Код не писали. Зафиксировано направление и ключевые решения, чтобы вернуться без потерь.
🚨 Главное решение (ловушка): НЕ строить второй электронный журнал. Учитель РФ обязан вести официальный (ЭлЖур / Дневник.ру / региональные ГИС) — конкурировать бессмысленно. «Журнал» в 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 — пересчёт при сдвигах).
Фазирование (ценность с первого шага):
- Сущность «Класс/группа» + миграция
student_class. Без неё ничего не стоит. - Дедлайны на выдаче + «журнал сдачи» поверх существующих сессий/результатов (дёшево; честный журнал из реальных данных, без выдуманных оценок). = MVP, продающий идею.
- КТП как ось (темы по неделям + экспорт Word/PDF для завуча).
- Календарь-линза + автопересчёт дат.
- Заметки (Notion-стиль) на уроках/темах + методкопилка.
- Замыкание петли: материалы+результаты в строке урока, кнопка коррекции из плана.
Открытые вопросы (решить с пользователем):
- Глубина журнала: честный «журнал сдачи» или полноценные оценки 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, КТП/курсы, уроки, заметки, журнал. Это «как именно учитель преподаёт».
Ключевые архитектурные решения:
- Поле
owner(relation→teachers, required) на личных коллекциях; правилоowner = @request.auth.id || role = superadmin. Фонд — без владельца. - Общий фонд = «читаемый-в-основном» + личный оверлей. Учитель видит
commons ∪ свои личные задачи. Правка общей задачи = «предложить в фонд» (модерация) ЛИБО «форкнуть себе». Удаления из фонда — мягкие (архив, не hard delete). Это прямая защита от катастрофы (личные работы ссылаются на ID задач фонда). Инстинкт уже есть: PB не даёт удалить использованную задачу (400) — возвести в принцип. - 🚨 UI-only авторизации перестаёт хватать — ЭТО форсирующий фактор. Как только у
каждого учителя СВОИ ученики (ПДн детей!) и свой журнал, UI-only = учитель А читает
учеников учителя Б через DevTools = утечка ПДн. Введение personal-данных = триггер
сделать правила PB настоящими (
owner = @request.auth.id) хотя бы на личных+PII коллекциях. CLAUDE.md это предусматривает. Тонкость: сессию выдачи владеет учитель, но ЧИТАЕТ назначенный ученик → viewRule сессии = «владелец ИЛИ назначенный ученик». - Миграция из single-tenant — разовая, дешёвая: одна миграция проставляет
owner = <id основного учителя>на всех существующих личных записях; фонд не трогаем. - «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_sessions → deadline, grade_thresholds(json). Журнал — НЕ таблица,
а пересборка attempts вдоль «класс × дата».
Новые страницы/роуты (новая группа меню «Моё пространство», секция workspace,
визуально первая — инверсия центра тяжести с каталога; каждый роут через React.lazy()
ROUTE_META+ при необходимостиeditOnly):/app/todayTodayDashboard (позвоночник «Сегодня/Неделя», кандидат в дефолт/app) ·/app/groupsGroupManager ·/app/groups/:idGroupDetail ·/app/ktpKtpList ·/app/ktp/:courseIdKtpEditor ·/app/calendarTeacherCalendar ·/app/journalGradeJournal ·/app/notesNotesWorkspace.
Дизайн (нужен макет, не «ещё таблица»): (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) + nullablestudents.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— nullablework_sessions.deadline(date). Применена после бэкапаbackup_2026-06-07_12-08-18. SessionPanel—DatePicker«Дедлайн сдачи» (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). UIcomponents/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. APIpb/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).materialsjson иdate_factзаведены в схему, UI — на будущее (фаза 6).
✅ ФАЗА 5 ОТГРУЖЕНА (2026-06-07). Заметки на блочном редакторе в проде (v3.9.52):
- Миграция
1779000014_create_teacher_notes.js—teacher_notes(owner→teachers, title, body json [документ BlockNote], is_inbox, links json). Бэкапbackup_2026-06-07_21-40-16. APIpb/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 базовые задачи решить, гасить ли (осторожно — они в выданных работах).
Каркас органайзера собран и в проде (фазы 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 URLmathb-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_sessionsviewRule = «владелец ИЛИ назначенный ученик» (тонкость: ученик читает сессию). Миграция-подстраховка: проставить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-формулы в заметках (сейчас блок-формула).
Второе приложение пользователя: ~/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».
Зачем смотрели: гипотеза пользователя — в гаокао непропорционально много функций; «китайцы что-то знают про будущее». Проверили по источникам — гипотеза верна, и это НЕ тангенс к органайзеру, а его усиление (наполняет смыслом ось времени + граф связей).
Почему функции (расшифровка «секрета»): реформа 2017/2018 (新课标) сместила центр с «тем» на 6 ключевых компетенций (мат. абстракция, лог. рассуждение, мат. моделирование, простр. воображение, вычисления, анализ данных); экзамен — с зубрёжки на применение+рассуждение. Всё это про ЗАВИСИМОСТИ величин, а функция = атом зависимости («вход→выход») = ментальная модель данных/алгоритмов/ML. Ставка не на «функции-тему», а на функциональное (реляционное) мышление как базовую грамотность вычислительного века. Совпадает с российским трендом: ОГЭ практический блок 1–5 (у Lemma уже есть!) + «функциональная грамотность» PISA. Не копируем чужое — попадаем в общий тренд, где Китай раньше/системнее.
Переносимые практики (механика, НЕ культура-прессинг) → маппинг на Lemma:
- 变式 Bianshi — обучение через вариацию (ГЛАВНОЕ). Не одна задача, а выстроенная последовательность вариаций («一题多变, одна задача — много изменений»). Два вида: процедурная (меняем числа/шаги = что наши генераторы делают СЕЙЧАС = дрилл) и концептуальная (меняем ПРЕДСТАВЛЕНИЕ / контекст / что неизвестно → строим реляционное понимание). → Апгрейд ~29 генераторов: от процедурной к концептуальной вариации. Та же функция как формула↔график↔таблица↔словами; менять «что дано/найти»; менять контекст (чистая матем.↔реальная). KaTeX+GeoGebra уже есть → мульти-представления на одной задаче. Самый родной для Lemma апгрейд: генератор из «бесконечного задачника» → инструмент построения понимания.
- 6 компетенций вместо «просто тем» → лёгкая метадата «компетенция/представление» на задаче → копилот собирает лист, сбалансированный ПО КОМПЕТЕНЦИЯМ, не только теме. Вскрывает пробел: у нас сильно «вычислить/решить», слабо «прочитать график/построить модель». (Связано с идеей «классификация типа ошибки», но в позитивном ключе.)
- 通性通法 — общие методы важнее трюков → акцент на разбор РЕШЕНИЯ как переносимого метода (ложится на «обратную связь ученику» + теорию).
- Вертикальная связность (承上启下), функция = её позвоночник → подтверждает идею ГРАФА ПРЕРЕКВИЗИТОВ (сердце копилота из блока архитектуры). Связность — не украшение, а ядро; функции — естественный главный ствол графа.
- 教研 jiaoyan — коллективная доработка уроков → методическое ОБОСНОВАНИЕ будущего org-слоя (школа/методобъединение): он нужен не для админки, а чтобы учителя ВМЕСТЕ оттачивали КТП и наборы вариаций. Не строим сейчас — но знаем зачем.
Как сходится в нашу рамку: bianshi→апгрейд генераторов; связность+функции→граф пререквизитов; компетенции→измерение «баланс навыков» для копилота; jiaoyan→смысл org; функц. грамотность→контентная линия (совпадает с ОГЭ-реформой). Китай не добавляет сущность — наполняет смыслом ось времени и граф связей, что уже спроектированы.
Контентная линия-кандидат: «функциональное мышление» 5–11 (мульти-представления зависимостей, чтение графиков, моделирование реальных ситуаций). Совпадает и с гаокао, и с российской функц. грамотностью.
Идея пользователя. Дать учителю (на будущее, когда приложение пойдёт в продакшен и станет коммерческим) возможность подключить свой 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 = только про генеративные/чат фичи.
Как может быть реализовано (эскиз, не финал):
-
Хранение настроек. Новое поле в коллекции
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 = следующий шаг (тарификация «каждый платит за свой токен»).
- 🚨 Токен — секрет. Не хранить в открытом виде во фронте/БД. Варианты:
(а) шифровать на сервере (pdf-service владеет ключом шифрования из env), фронт
отправляет ключ один раз при сохранении настройки, дальше — только
-
Серверная ручка
/latex-fix(и будущие AI-ручки). Принимать опциональный override: либо из тела запроса ({ text, ai: {url, model} }+ ключ берётся из расшифрованной настройки учителя по auth-токену), либо ручка сама подтягивает настройку залогиненного учителя. Приоритет: настройка учителя → env (fallback на дефолтный Timeweb). Сохранить текущее поведение 503, если ничего не настроено.⚠️ Кэш/latex-fixсейчас поsha256(text)— при мультипровайдере ключ кэша должен включатьmodel/provider, иначе ответ одной модели подменит другую.- Тайм-ауты/ретраи: у Claude/GPT задержки и форматы ошибок другие — нормализовать.
-
UI настроек. Раздел «AI / Интеграции» в настройках (доступ
editOnly/superadmin по правилам Authorization из CLAUDE.md): поля endpoint/model/key, кнопка «Проверить соединение» (тестовый прогон latex-fix на короткой формуле), индикатор статуса, маска ключа. При выключенном — AI-кнопки в UI прячутся/дизейблятся (черезenabledфлаг). -
Абстракция провайдера (рефакторинг под будущее). Сейчас один промпт (
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]]).
Контекст / зачем. Возникло при работе над показателем решаемости (v3.9.90,
[[difficulty_signal]]). Для задач части 2 профиля (№13–19) из условия метод
решения не виден, а часто и в принципе не выводится. А вот из solution_md —
виден: «оценка+неравенство», «замена переменной», «выход на монотонность»,
«признак делимости», «ГМТ» и т.п. Идея: классифицировать/искать задачи по идее
решения, а не по условию. Это удачное применение вектора/LLM (в отличие от
переноса сложности, который отвергнут — см. [[difficulty_signal]]).
Два слоя (рекомендуется делать оба, начать с тегов):
- LLM-теги методов (главное, человекочитаемо). Прогон
solution_md(+опц.criteria_md) через существующий LLM-шлюз (Timeweb deepseek, тот же, что/latex-fix; см. [[sdamgia_parser]], [[project_ollama_define]]) → структурный список тегов идей. Хранить в новом полеtasks.method_tags(json) или через справочник тегов. Даёт фильтр/поиск каталога по методу — сразу полезно учителю. Прогон одноразовый + при импорте; кэш по sha256(solution) как в/latex-fix. - Эмбеддинг решений (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 + «похожие по решению».
Проблема (цифры с прода, 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]]). Поэтому:
- Backfill, а не импорт по умолчанию. Парсим решу-задачу (
/parse-sdamgia), эмбедим (bge-m3, Mac), ищем ближайшую в Лемме. cos ≥ ~0.94 И совпал ответ → проставляемsdamgia_idсуществующей задаче. Нет совпадения → импорт новой (born-local). Так дубли создаются только для реально новых задач. - Сначала по журналу, не по всему Решу. Целевой список — ~1 215
problem_idиз журнала без связи (1434 − 219). Ограниченный объём + сразу даёт сигнал решаемости. Сплошной обход каталога Решу — позже и точечно (десятки тысяч задач, 99% не в Лемме → сплошной импорт = взрыв мусора). - Очередь ревью + провенанс. Авто-матч по тексту даёт ложные срабатывания
(у базы много задач-«близнецов»: тип один, числа разные). Поэтому: высокий 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).