# Редизайн лаб курса под три режима менти Статус: принято, в работе (ветка `feature/course-labs-redesign`). Источник: issue #7; аудит курса против стенда и штурм педагогики лаб с пользователем 2026-07-23. Опирается на [спеку редизайна пути менти](./2026-07-19-mentee-path-redesign.md) (реализована, issue #9 закрыт) и термины из `CONTEXT.md` (три режима менти, эталонный мир, модельное время). ## Проблема Курс в `docs/course/` (7 уроков + 4 метадокумента) целиком построен на старом пути `make generated-history-analytics && make up`: ни один документ не знает про `world_init`, `import`, эталонный мир и `make superset-init`. После редизайна пути менти курс в текущем виде не заработает. При этом педагогическое ядро уроков цело: SQL, DAG'и, мониторинг и Superset актуальны — ломаются только блоки подготовки стенда и обвязка. А два новых режима роста мира (`next-day`, `continue`) вообще не имеют уроков, хотя спека пути менти прямо называет их «разные педагогики и разные лабы». ## Цели - Менти проходит курс по новому пути: `make up` → Airflow UI (`ddl_init` → `world_init` с пустой формой) → `make superset-init`. - Оба режима роста мира получают по обязательному уроку-лабе с собственным учебным паттерном. - Числа в уроках совпадают у всех менти число-в-число (следствие `import` эталонного мира) и сверены с живым стендом. - Смешанных состояний не бывает: вся перестройка — один PR. ## Целевая модель ### Маршрут курса Маршрут остаётся линейным, все уроки — обязательные: - **Урок 6 (Superset) становится обязательным.** Его опциональность родом из PRD («делаем, если останется ресурс») — экономия ресурса на написание, а урок давно написан; причина истекла. Ожидания работодателей к дата-инженеру теперь включают витринно-BI-слой. - **Две новые лабы после урока 6, обе обязательные:** `07_lab_next_day.md`, затем `08_lab_continue.md`. Порядок осознанный: next-day детерминирована (числа сойдутся у всех — можно печатать точные ожидания), continue — живая и недетерминированная, «второй шаг свободы». - Обе лабы строятся по шаблону `LESSON_STANDARD.md` (шапка + 6 секций). ### Новая семантика сброса Для лаб «верни как было» — это не откат правки в git: мир вырос. Канонический сброс — тот же путь, что и первый вход, и описывается он в одном месте (README курса), уроки ссылаются: 1. `make clean` (сносит и данные, и Superset — это надо назвать явно); 2. `make up` → Airflow UI: `ddl_init` → `world_init` с пустой формой; 3. `make superset-init`. Идём UI-путём менти, а не мейнтейнерским `make startup-history-import`. Старая команда сброса `make generated-history-analytics && make up` уходит из учебных текстов (остаётся мейнтейнеру и CI). ### Лаба 07 — next-day Паттерн одной фразой: **пакетный инкремент дня и его границы** (прод-аналог — «ночью приехал вчерашний день»). - **Руки:** триггер беспараметрного `world_next_day` → день 4 в витринах; сверка чисел manifest (точные ожидаемые значения); `make generated-history-chain-check` на стыке; день-к-дню на дашборде. - **Центральное открытие — переходящие визиты:** менти SQL-запросом находит визиты, чьи события лежат по обе стороны полуночи, и осознаёт, почему суточная нарезка режет живые сессии — откуда берутся проверки стыков, late data и пересчёт вчерашнего хвоста. Этого нет ни в одном уроке 0–6. Важно для текста лабы: запрос идёт в `dds.event` напрямую (`GROUP BY click_id` + `HAVING toDate(min(event_ts)) <> toDate(max(event_ts))`) — витрина `dm.v_session_overview` не годится, она группирует по `event_date` и режет переходящий визит на две строки. Это новый приём, который лаба сама и учит (уроки давали argMax/JOIN, но не агрегацию визита целиком). Годится любой стык дней: стыки 1→2 и 2→3 внутри эталонного мира есть точно; живы ли визиты на стыке 3→4 (заморозка мира могла закрыть открытые сессии) — проверяется на стенде при реализации. Здесь же — короткая врезка про соответствие терминов: «визит» из manifest = `click_id` в SQL (в схеме нет таблицы «визитов», есть `dds.click`). - **Управляемая правка:** включить расписание `world_next_day` в Airflow UI → день 5 приезжает сам → выключить обратно. Спека пути менти прямо проектировала paused-расписание под этот шаг лабы; заодно менти трогает paused/`catchup`. - **Цена роста — коротким наблюдением:** в длительностях задач Airflow генерация дня ~постоянна (инкрементальные счётчики manifest, issue #5), а `full_refresh` ETL растёт с миром — осознанный долг, ссылка на #8. - **Самопроверка — крючок про идемпотентность:** «триггерни дважды — что будет? почему прод-джобы за день устроены иначе?». - Эталонный путь: `airflow/dags/world_next_day_dag.py`. ### Лаба 08 — continue Паттерн одной фразой: **живой поток и свежесть данных**. - **Ядро — потоковый приём ≠ потоковая обработка:** Kafka и STG пополняются сами (MV урока 1), витрины DM стоят до прогона `etl_pipeline`; запустил — догнали и снова отстают. Свежесть данных как явное понятие: «какую свежесть обещаем потребителю?». - **Руки:** `make generator-continue` → мониторинг урока 5 оживает (target генератора в UP, алерт `Kafka No Messages Produced` гаснет, events/min шевелится, офсеты урока 0 растут) → наблюдение расслоения свежести → догон через `etl_pipeline`. - **Управляемая правка — останови поток и продолжи:** `make generator-down` → алерт срабатывает, потребитель дочитывает лаг до нуля; `make generator-continue` → мир продолжается с места остановки (популяция и модельные часы пережили рестарт). Это суть режима continue и рабочий паттерн устойчивости. По шаблону LESSON_STANDARD лаба всё равно завершается явным шагом «верни как было» — канонический сброс (см. выше): мир после continue недетерминирован, и к следующему прохождению стенд возвращается к эталону. - **Финальное наблюдение — «мир расходится»:** после continue числа менти перестают совпадать с эталонными, и это правильно; возвраты пользователей вживую разводят `users < sessions` (на статике было невозможно — см. `CONTEXT.md`). - **Модельное время — только врезкой:** «стенд ускорен в 60 раз, чтобы сутки потока уложились в ~полчаса; "сейчас" дашборда может обгонять настенные часы», ручка `GEN_MODEL_TIME_SPEED` — одной строкой. Это механизм тренажёра, не рабочий паттерн — секционного веса не даём. Висячую ссылку `CONTEXT.md` про «урок 7» поправить на эту врезку. Симметрия курса: 07 — про границы времени в пакетном мире, 08 — про свежесть в потоковом; обе лабы растят один и тот же импортированный мир двумя способами. ### Существующие уроки и метадокументы По вердиктам аудита: - **Все уроки 0–6:** блок подготовки в §2 заменить на новый путь (import); сам блок вынести в одно каноническое место (README курса), уроки ссылаются на него. - **Все цифры уроков 0–6 недействительны.** Старые значения считались на старом мире (другой профиль, другой объём); с переходом на эталонный мир каждое цитируемое число каждого урока пересчитывается заново. Это полноправный этап работы, а не финальная галочка — даже в уроках, где правки текста минимальны. - **Урок 1:** управляемая правка «добавь колонку и получи свежие сообщения» переводится с перегенерации backfill на дозаливку через `world_next_day` (детерминированно). Известная цена: прогон DAG'а занимает минуты — урок называет её честно; сам DAG подаётся кнопкой- анонсом без разбора («подробно — в лабе 07»), чтобы не красть у лабы её материал. - **Урок 4:** добавить лесенку `ddl_init` → `world_init` → `world_next_day` как контекст оркестрации (менти уже прошёл её руками). - **Урок 5:** словарь «backfill-only стенд» заменить на «база import / живой поток»; сценарий алерта `Kafka No Messages Produced` переписать в этих терминах. - **Урок 6:** снять пометку «опционально»; цифры сверить с эталонным миром. - **README курса:** переписать блоки подготовки и «Проверка чистого маршрута»; таблицу уроков дополнить лабами. - **PRD:** не переписывать — одна датированная поправка по его же конвенции (три режима менти, урок 6 обязателен, лабы 07–08). - **LEARNING_PLAN:** переписать маршрут и статус, сохранить таблицу аудита эталонных путей; дополнить её `world_next_day_dag.py`. - **LESSON_STANDARD:** заменить команду сброса на import-путь. ## Чего здесь не делаем - Не трогаем код стенда: DAG'и, SQL, генератор, инфраструктура — вне скоупа; работа только с документами курса, README курса и `CONTEXT.md` (одна ссылка). - Не чиним рост стоимости `full_refresh` (issue #8) — в лабе 07 он только показывается. - Не пишем урок про модельное время — понижено до врезки в лабе 08. - Не переносим приватные менторские материалы — граница из README курса остаётся. - Не переделываем эталонный код уроков 0–6 сверх замены обвязки: аудит подтвердил, что ядро актуально. ## Проверка - Чистый стенд: менти проходит весь курс 0–8 по текстам уроков, ни разу не встретив `generated-history-analytics` и `backfill`. - Каждая цитируемая цифра уроков и лаб сверена с живым стендом после `import` эталонного мира (обязательный финальный шаг реализации). - Лаба 07: после `world_next_day` числа manifest совпадают с напечатанными в лабе; `make generated-history-chain-check` зелёный; SQL-запрос из лабы находит хотя бы один переходящий визит (на любом стыке дней). - Детерминизм инкремента подтверждён до печати чисел: два независимых прогона `world_next_day` от свежего import дают одинаковые числа manifest и контрольную сумму. - Лаба 08: сценарий стоп/продолжение проходит без потерь (лаг стекает к нулю, после продолжения числа согласованы с manifest). - Все внутренние ссылки курса живы; `make test` / `make lint` зелёные (доки код не трогают — проверка от регрессий по касанию). ## Решения и отклонённые варианты - **Лабы — секциями внутри уроков 4/5** — отклонено: уроки распухают, лабы теряют самостоятельность; выбраны отдельные файлы. - **Урок 6 оставить опциональным, финал лабы 07 — только SQL** — отклонено: причина опциональности истекла, рынок ждёт BI-навыков; урок 6 становится обязательным, лабы опираются на дашборд. - **Модельное время как секция или отдельный урок** — отклонено: механизм тренажёра, не рабочий паттерн; врезка. - **Управляемая правка лабы 08 через ручку ×K** — отклонено по той же причине; выбран стоп/продолжение потока. - **Дробить работу на несколько PR** — отклонено: половинчатый курс (часть уроков про import, часть про backfill) хуже любого из крайних состояний; этапность — коммитами внутри одной ветки. - **Цифры «ориентировочно», без сверки со стендом** — отклонено: воспроизводимость число-в-число — главный козырь эталонного мира. ## Влияние на документацию Вся работа и есть документация: `docs/course/**` (7 уроков + 2 лабы + 4 метадокумента), одна ссылка в `CONTEXT.md`. Корневой `README.md` и `docs/OPERATIONS.md` уже описывают новый путь — не трогаем. Issue #7 ссылается на эту спеку и ведёт чек-лист шагов.