diff --git a/.scratch/handoffs/20260723-1248-course-labs-redesign.md b/.scratch/handoffs/20260723-1248-course-labs-redesign.md new file mode 100644 index 0000000..1990662 --- /dev/null +++ b/.scratch/handoffs/20260723-1248-course-labs-redesign.md @@ -0,0 +1,50 @@ +# Handoff: редизайн лаб курса (issue #7) + +Дата: 2026-07-23. Ветка: `feature/course-labs-redesign` (создана, коммитов нет). +Одноразовый документ для продолжения работы в новой сессии (ADR-0003). + +## Состояние + +Работа идёт по плейбуку `/claude-subagent-playbook` (оркестратор Claude, +исполнитель Codex, слепые ревью). Пройдено: + +1. **Аудит курса против стенда** — субагентом; отчёт (EN): + `/tmp/claude-1000/-home-dementev-sources-clickstream-ch-kafka-superset-demo/28f16040-6abd-4d92-a5b1-71ffd4ebcb99/scratchpad/audit-course-vs-stand.md` + (при потере scratchpad не критично: главное перенесено в спеку). +2. **Штурм педагогики лаб** с пользователем (`/brainstorm-with-docs`); + конспект решений (EN): тот же каталог, `brainstorm-labs-design.md`. +3. **Спека написана** — `docs/specs/2026-07-23-course-labs-redesign.md` + (источник истины: маршрут, ядра лаб 07/08, вердикты по урокам 0–6 и + метадокам, границы, проверка). Ещё НЕ закоммичена. + +## Ключевые решения (детали — в спеке) + +- Один PR на всю работу; дочерних issues нет, #7 — единственный. +- Урок 6 (Superset) становится обязательным; лабы 07 (next-day) и + 08 (continue) — обязательные, после урока 6. +- Сброс для лаб и всего курса: чистый стенд + повторный `import`. +- Цифры уроков сверяются на живом стенде после `import` (финальный шаг). + +## Следующие шаги (конвейер плейбука) + +1. ▶ **Адверсарное ревью спеки** — Claude-субагент (deep-reasoner, без + контекста оркестратора), проверка спеки против кода/доков; вердикт + файлом в scratchpad. Этап «постановка» — самый дорогой для тихой + ошибки. +2. Триаж находок → правки спеки → коммит спеки в ветку + (`/conventional-commits`, docs(course)). +3. Обновить issue #7: ссылка на спеку + чек-лист шагов. +4. Самодостаточный файл-задача для Codex (EN, в scratchpad) → реализация + `codex exec` фоном → саморевью → два слепых ревью кода → триаж → + сверка цифр на живом стенде → один PR. + +## Подсказки для агента + +- Скиллы: `/claude-subagent-playbook` (рабочий конвейер), + `/conventional-commits` (коммит), `/ai-text-lint` (тексты уроков перед + финалом — требование LESSON_STANDARD §2). +- Память проекта: субагентам модель/effort задавать явно; фоновым + запускам — ScheduleWakeup-подстраховка; о каждом запуске сообщать + пользователю (этап + чего ждём). +- Пользователь планирует `/compact` после ревью спеки — вся фактура + должна жить в файлах, не в контексте. diff --git a/docs/specs/2026-07-23-course-labs-redesign.md b/docs/specs/2026-07-23-course-labs-redesign.md new file mode 100644 index 0000000..d778c21 --- /dev/null +++ b/docs/specs/2026-07-23-course-labs-redesign.md @@ -0,0 +1,219 @@ +# Редизайн лаб курса под три режима менти + +Статус: принято, в работе (ветка `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 +ссылается на эту спеку и ведёт чек-лист шагов.