From d6e7a030327da5dfcf4f03764af05f08801e7fde Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Sun, 19 Jul 2026 23:18:17 +0300 Subject: [PATCH] =?UTF-8?q?docs(specs):=20=D1=81=D0=BF=D0=B5=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=BF=D1=83=D1=82=D0=B8=20=D0=BC=D0=B5=D0=BD=D1=82=D0=B8=20?= =?UTF-8?q?=E2=80=94=20=D0=BC=D0=B8=D1=80=20=D0=B8=D0=B7=20=D0=B0=D1=80?= =?UTF-8?q?=D1=82=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=B0=20=D0=B8=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B5=20=D0=B2=D0=B5=D1=82=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - находки ручного HITL 2026-07-19 требовали проектного решения: путь менти через backfill медленный, бедный и путаный; нужна база import эталонного мира и две ветки роста. - Что: - спека docs/specs/2026-07-19-mentee-path-redesign.md: целевая модель (import + next-day + continue), эталонный 3-дневный мир в git (xz), переименование пульта в world_init с дефолтом import, отдельный world_next_day, инкрементальные счётчики manifest, один учебный профиль; форма работ — 4 дочерних issue. - CONTEXT.md: термины «мир (стенда)», «эталонный мир», «три режима менти». - .scratch/hitl-findings.md восстановлен из среза 0e312b3 как рабочий материал фичи (до разбора в issues). - Проверка: - вычитка против hitl-findings и решений обсуждения 2026-07-19. --- .scratch/hitl-findings.md | 236 ++++++++++++++++++ CONTEXT.md | 22 ++ docs/specs/2026-07-19-mentee-path-redesign.md | 155 ++++++++++++ 3 files changed, 413 insertions(+) create mode 100644 .scratch/hitl-findings.md create mode 100644 docs/specs/2026-07-19-mentee-path-redesign.md diff --git a/.scratch/hitl-findings.md b/.scratch/hitl-findings.md new file mode 100644 index 0000000..17373d6 --- /dev/null +++ b/.scratch/hitl-findings.md @@ -0,0 +1,236 @@ +# Находки ручной HITL-проверки пути менти + +Сессия 2026-07-19. Проверяем путь менти своими глазами: Airflow UI -> +Superset -> Kafka UI. Стенд чистый (`make clean` + `make up`), профиль `ci`. + +## Рамочная модель: три режима менти (подтверждено, launch.py:117-137) + +Основной режим менти — **загрузка из архива**, дальше две ветки от одного +восстановленного состояния (`GEN_STATE_RESET=false`). Это и есть каркас, +в который ложатся все находки ниже. Из каждой ветки можно сделать отдельную +лабораторную. + +- **База: `import`** артефакта (целевой размер 3 дня, `daily-wave`) -> + мир на `T_end`. Общий фундамент обеих веток. См. F7, F8. +- **Ветка A: `next-day`** (`GEN_RUN_MODE=next-day`) — пакетно добавить + сутки. Лаба «инкрементальная обработка»: инкремент vs full-refresh, + чистота стыков, расписание. Сюда бьют F1, F3, F6, F9. +- **Ветка B: `continue`** (`GEN_RUN_MODE=live`) — непрерывный живой поток + от той же границы. Лаба «потоковый приём»: near-real-time ETL, + мониторинг Grafana, стык backfill/live. **Требует ×60** — оправдывает + скорость учебного профиля (уточнение к F8). + +Важно: `next-day` и `continue` — разные педагогики, не схлопывать. +**Под каждую ветку — свой урок/лаба** (учебный контент, `docs/course/`): +одна про пакетную инкрементальную обработку (`next-day`), другая про +потоковый приём (`continue`). Общая база `import` — их совместное начало. +`backfill`/`reset` (`STATE_RESET=true`, «с нуля»): в **целевой** модели +`backfill` — инструмент мейнтейнера для сборки артефакта, а не первый шаг +менти. Сегодня ещё наоборот — backfill остаётся каноническим первым +прогоном менти (см. F1/F2); этот сдвиг и есть суть редизайна. + +## F1. Форма `generator_control` помечает необязательные параметры обязательными (BUG) + +- **Где:** Airflow UI -> `generator_control` -> Trigger DAG w/ config. +- **Симптом:** все поля формы (`duration`, `seed`, `model_time_speed`, + `artifact_path`, `expected_t_end`) показаны с красной `*` и обязательны. + Браузерная валидация `required` не даёт отправить форму с пустым полем. + Споткнулись первым на пустом `duration`. +- **Причина:** в `dags/generator_control_dag.py` эти `Param(...)` объявлены + с `type="string"` без `"null"`. Airflow для типа без `null` вешает на input + HTML-атрибут `required`. +- **Противоречие с документами:** `docs/OPERATIONS.md` и описания самих + Param говорят «пусто — взять из профиля / не сохранять». То есть поля + задуманы необязательными, но UI это запрещает. +- **Влияние на менти:** канонический первый прогон «backfill с профилем, + остальное пусто» через UI невозможен без обходного заполнения. +- **Кандидаты решения:** сменить тип необязательных Param на + `type=["null","string"]` (идиома Airflow для необязательной строки) — + проверить актуальность через Context7; либо, как минимум, поправить + формулировку в runbook. Предпочтителен первый. +- **Обход в этой сессии:** заполнили все поля значениями профиля `ci` + (`duration=6h`, `GEN_SEED=4242`, `GEN_MODEL_TIME_SPEED=1`, + `artifact_path=/opt/airflow/data/ci_backfill.json`, + `expected_t_end=2026-01-01T06:00:00+00:00` — для backfill игнорируется). + +## F2. Superset: гео-карта заменена столбцами — ПОДТВЕРЖДЕНИЕ, не дефект + +- **Наблюдение менти:** на дашборде вместо гео-карты — столбчатый + «Top Countries by Events». На первый взгляд неожиданно. +- **Проверка:** это осознанное решение задачи 10 (`done`). Legacy-виз + `world_map` (choropleth) не даёт настроить tooltip/легенду/шкалу, а + гео-распределение сильно перекошено (US-доминанта из статического сида + `geo_by_click_id`, своя генерация гео — отдельный шаг по ADR-0006). + Топ-N стран столбцами читается лучше карты. Критерии приёмки задачи 10 + это фиксируют. +- **Вывод:** в UI задача 10 приземлилась корректно. Дефекта нет. + +## Остальной дашборд (backfill-путь, профиль ci) + +Всё сходится с данными и здорово: +- KPI 16 054 событий / 445 пользователей / Avg 10.6 / Conversion 7.1%. +- Events over Time ровный за `[00:00, 06:00)`; воронка монотонно убывает. +- «Rows by Layer» — 4 ровных столбика ~16k: события не теряются на + переходах STG->ODS->DDS->DM (наглядный контроль целостности). +- Связанные фильтры между визами работают (проверил менти глазами). + +## F3. Пульт `generator_control` неудобен для менти и не годится в расписание (DESIGN) + +- **Наблюдение менти:** `next-day` спрятан в универсальном DAG с дефолтом + `backfill` и тяжёлой формой; `expected_t_end` для менти — лишнее + усложнение (на автосхеме мира границу знать неоткуда). +- **Предложение менти:** сделать `next-day` отдельным DAG, который + «достаточно триггернуть, ничего не нажимая лишнего». +- **Почему важно:** это прямой вход в задачу про расписание. У планового + запуска не должно быть формы с обязательными полями и дефолта `backfill` + (см. постановку про расписание). Отдельный беспараметрный DAG `next-day` + решает и удобство менти, и пригодность к `schedule`. +- **Связка:** усиливает F1 (форма требует необязательные поля) — общий + корень в том, что один DAG обслуживает и ручной backfill, и то, что + хочется автоматизировать. + +## F4. Airflow Grid: Auto-refresh Error (JS) + +- **Симптом:** всплывающая ошибка `can't access property "find", + p.dagRuns is undefined` при авто-обновлении Grid во время запуска. +- **Оценка:** похоже на известный косметический баг UI Grid (гонка + авто-refresh, пока у DAG ещё нет прогонов). Работе не мешал. Проверить + версию Airflow и известные issue; при подтверждении — низкий приоритет. + +## F5. Быстрый разлогин в Airflow И Superset (BUG, корень TBD) + +- **Симптом:** обе веб-морды стремительно разлогинивают в рамках сессии. +- **Что исключено:** ротация секрета. `AIRFLOW__WEBSERVER__SECRET_KEY` + фиксирован (литерал по умолчанию, `AIRFLOW_SECRET_KEY` в `.env` не задан), + `SUPERSET_SECRET_KEY` — жёстко зашитый литерал. Значит, «каждый gunicorn + worker подписывает своим ключом» — не причина. +- **Куда копать:** время жизни сессии (Airflow + `session_lifetime_minutes`, Superset `PERMANENT_SESSION_LIFETIME` — в + `configs/superset` не задан), настройки cookie (SameSite/Secure на + localhost с разными портами), окружение браузера. +- **Влияние на менти:** сильно портит опыт — заставляет постоянно + перелогиниваться. Кандидат в отдельную задачу. + +## F6. `next-day` собирается долго (~10,6 мин на дне 2), CPU-bound (PERF/DESIGN) + +- **Наблюдение менти:** «как долго собирается следующий день… и это на + мощном процессоре». +- **Замер по метаданным Airflow (этот прогон):** задача `run_next_day` = + **638 с (~10,6 мин)** (18:03:26 -> 18:14:04). Соседние задачи мелкие: + `precheck_next_day` 15 с, `trigger_etl` (ETL full-refresh по 110k + событий) 30 с, `check_after_etl` 6 с. Узкое место — именно генерация + плюс накопительный пересчёт внутри `run_next_day`, не ETL. +- **Три причины:** + 1. модельный день = 24 ч против 6 ч у backfill -> ~вчетверо больше + событий за прогон; + 2. генерация — однопоточный Python, CPU-bound: много ядер не помогают, + упор в скорость одного ядра; + 3. задокументированный накопительный пересчёт по всей истории Kafka + растёт с числом дней (на дне 2 мал, но копится). +- **Связка с задачей про расписание:** это ровно та причина, по которой + перед включением `schedule` нужно решить retention/формат накопительного + состояния (иначе плановый ежедневный `next-day` — растущий многоминутный + CPU-burn). Менти пощупал стоимость руками. См. F3. + +## F7. Опыт менти беден на 6h; «история из файла» упирается в размер (DESIGN) + +- **Мысль менти:** 6 часов backfill — мало для опыта; хотели хранить + больше и не тратить время менти на генерацию, а грузить из файла. +- **Состояние:** механизм есть — глагол `import` и runbook + `docs/runbooks/startup-history.md` (задача 07 `done`). Но готового + артефакта в репозитории нет: менти всё равно либо генерирует (медленно, + см. F6), либо ищет «где взять файл». +- **Замер:** артефакт за 6 ч = **49 МБ** (плоский JSON). Экстраполяция: + день ~200 МБ, неделя ~1,4 ГБ, месяц ~6 ГБ. В git такое не кладут. +- **Причина раздутости:** артефакт содержит и `state`, и `raw_topics` — + похоже, тащит сырые сообщения Kafka целиком (~8 МБ на модельный час). +- **Развилка (пересекается с F6/расписанием):** + 1. целевой размер демо-мира (день/неделя?); + 2. формат/сжатие артефакта (`xz` — см. замер ниже; нужен ли `import`-у + `raw_topics`, или хватит компактного `state`); + 3. где хранить раз не git (git-lfs / релизный ассет / внешнее хранилище / + «сгенерировать один раз и закэшировать локально»). +- **Вывод:** «богатый мир из файла» и «растить мир расписанием» — один + общий вопрос: как дёшево хранить и переносить много истории. Решать + вместе. Runbook про размер/хранение сейчас молчит — дополнить. + +### Решение по целевому размеру (менти, 2026-07-19, обсуждаемо) + +- **Целевой стартовый размер демо-мира — 3 модельных дня.** «Больше + одного, но не слишком далеко». Срок обсуждаем. +- **Почему 3:** периодичность видна от 2 дней; 3 дают чёткий паттерн + + один «средний» день без краевых эффектов; появляется сравнение + день-к-дню в Superset. Размер ~600 МБ плоского JSON, gzip ~30–60 МБ. +- **Следствие про профиль:** на `ci` (ровная интенсивность, jitter=0) + три дня будут плоскими и скучными. Суточную волну даёт `daily-wave`. + Эталонный артефакт для менти собирать на `daily-wave`, а не `ci`. + Генерация 3 дней разово мейнтейнером — ок (backfill без сна, минуты), + менти только `import`. + +### Замер сжатия (2026-07-19) + +- Артефакт 6h: raw 49 МБ. **`xz -9e` → 1,1 МБ (48×), 13,5 с.** gzip -9 → + 3,0 МБ (17×). xz почти втрое лучше. +- Пересчёт на 3 дня: raw ~590 МБ → **xz ~13 МБ** — кладётся в обычный git + без lfs. +- Паттерн-образец: `~/sources/airflow-greenplum-solution` — `xz -9` жмёт + сид один раз в `bookings/seed/demo.sql.xz` (в git), на загрузке + `xz -dc | ...` стримит. Переносим один в один: одноразовое медленное + сжатие мейнтейнером, потом простой git. +- Вывод менти: сжимать долго один раз не страшно; когда сид отладим — + артефакт кладём в обычный git. + +## F8. Два профиля (`ci`/`daily-wave`) путают; менти нужен один (DESIGN) + +- **Наблюдение менти:** планировался один удобный профиль с учебной + ценностью; зачем два — непонятно. +- **Что есть (`generator/src/clickstream_generator/launch.py`):** + - `ci` — 6h, скорость ×1, jitter=0. Профиль **автотестов/CI**: быстрый, + маленький, плоский (суточной волны не видно). + - `daily-wave` — 2d, скорость ×60 (сутки ~24 мин), задача 14 + «суточная волна за минуты занятия». **Учебный** профиль с волной. +- **Где протекло:** форма backfill перечисляет профили по алфавиту, + первым идёт `ci` → менти по умолчанию подсовывается тестовый плоский + профиль. Сегодняшний прогон шёл на `ci`, оттого дашборд ровный. +- **Куда вести:** при `import` готового артефакта менти профиль не + выбирает вовсе — выбор профиля уходит мейнтейнеру (сборка артефакта на + учебном профиле) и CI. Технически можно оставить один базовый учебный + профиль (волна, ×60), а CI переопределяет длительность на 6h (backfill + без сна, скорость на генерацию артефакта не влияет). Тогда отдельный + `ci` менти не нужен. +- **Проверить перед слиянием:** не зависит ли live-тест (runtime-seam) + от скорости ×1. Если нет — профили честно схлопываются в один. + +## F9. Стоимость `next-day`: параллелить не то, инкремент — то (PERF/DESIGN) + +Разбор по коду (`generator/src/clickstream_generator/service.py:_run_next_day`). + +- **Три куска стоимости:** + 1. Генерация нового дня (строки 545–583) — O(события дня), постоянна, + не растёт. + 2. Полная перечитка Kafka (строка 591, `KafkaDataTopicReader.load()` + + `ManifestCounters.add_batch`) — читает все data-топики с начала в + RAM, пересчитывает счётчики/суммы с нуля. **Растёт с историей.** + 3. ETL `full_refresh` (отдельный DAG) — перестраивает DDS/DM по всему + миру. Тоже растёт. +- **Параллелить — неправильный рычаг:** + - Генерацию нельзя без потери детерминизма (один поток ГПСЧ, + переходящие сессии, шагающее модельное время; воспроизводимость по + seed — базовая ценность). И она не растёт. + - Перечитку можно раскидать по потокам, но это лишь постоянный + множитель. Корень — O(N²) по N дням (день k перечитывает k дней). +- **Оптимизировать — правильный рычаг:** + - Счётчики инкрементальные: хранить накопленное состояние + `ManifestCounters` в manifest/state, добавлять только новый день + (его `sent_counts` уже посчитаны при генерации). O(N²) -> O(N). + - Убирает и рост RAM: сейчас `reader.load()` держит всю историю в + памяти (3 дня ~неск. ГБ распарсенного JSON — риск OOM). + - Формат: уникальность (`click_ids`/`user_ids`) сейчас через `set` по + всей истории — перенести множества в state; контрольную сумму + сделать катящейся (комбинировать посуточные), не хэш всего заново. +- **Связка:** это и есть решение «новый формат накопительного состояния + manifest», которое runbook называет обязательным перед расписанием + (см. F6). Оптимизация = сердцевина retention-задачи, не отдельная. +- **Режимы:** разовая сборка артефакта — медленно не страшно; + плановый ежедневный `next-day` — инкремент обязателен (через месяц + каждый запуск перечитывал бы 30 дней). diff --git a/CONTEXT.md b/CONTEXT.md index 79775d2..b985e33 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -90,6 +90,28 @@ user_domain_id (пользователь, постоянный) Механизм реализован через Airflow DAG `generator_control` и служебный чистый путь `make generated-history-analytics`; решение про часы — [ADR-0005](./docs/adr/0005-generator-model-clock.md). +### Мир (стенда) + +Совокупное состояние данных стенда: события в Kafka, слои ClickHouse, +состояние генератора (модельные часы, живая популяция) и manifest с +контрольными числами. «Вырастить мир» — добавить в него модельное время +(`next-day` или живой поток), «восстановить мир» — залить его из артефакта. + +### Эталонный мир (reference world) + +Канонический стартовый мир менти: 3 модельных дня на профиле `daily-wave`, +собран мейнтейнером один раз, хранится сжатым артефактом в git. Все менти +импортируют один и тот же файл — числа в лабах воспроизводимы +число-в-число. Решение — [спека редизайна пути менти](./docs/specs/2026-07-19-mentee-path-redesign.md). + +### Три режима менти + +`import` (база: загрузить эталонный мир) и две ветки от восстановленного +состояния: `next-day` (пакетно добавить модельный день) и `continue` +(живой поток от той же границы). Это разные педагогики и разные лабы; +`backfill` — не режим менти, а инструмент мейнтейнера для сборки +эталонного мира. См. ту же спеку. + ### Модельное время и масштаб (×K) **Модельное время стенда** отвязано от настенных часов: генератор крутит diff --git a/docs/specs/2026-07-19-mentee-path-redesign.md b/docs/specs/2026-07-19-mentee-path-redesign.md new file mode 100644 index 0000000..41e6b53 --- /dev/null +++ b/docs/specs/2026-07-19-mentee-path-redesign.md @@ -0,0 +1,155 @@ +# Редизайн пути менти: мир из артефакта и две ветки роста + +Статус: принято, в работе (ветка `feature/mentee-path`). +Источник: ручной HITL-прогон пути менти 2026-07-19 — находки F1–F9 +(рабочая копия — `.scratch/hitl-findings.md`; постоянный след — история git, +срез `0e312b3`). Решения приняты в обсуждении с пользователем 2026-07-19. + +## Проблема + +Сегодняшний первый шаг менти — `backfill`: генерация истории с нуля. +Это медленно (день модельной истории — минуты и десятки минут CPU), +а результат беден (6 часов плоского профиля `ci`). Вокруг — ворох трения: + +- пульт `generator_control` подсовывает `backfill` дефолтом и носит имя, + непонятное осваивающему стенд; +- два профиля (`ci`/`daily-wave`) путают: менти достаётся плоский тестовый; +- `next-day` спрятан в общем пульте и не годится для расписания: + каждый прогон перечитывает всю историю Kafka заново — стоимость растёт + квадратично по дням, память — линейно (риск OOM); +- готового артефакта в репозитории нет: механизм `import` есть, а + импортировать нечего. + +## Цели + +- Менти получает богатый мир за секунды-минуты, а не генерирует его. +- Путь менти проходится в Airflow UI пустыми формами, без справочника. +- У всех менти одинаковый мир: числа в лабах воспроизводимы число-в-число. +- Мир можно растить двумя осознанными способами: пакетно по дням и живым + потоком. +- Плановый `next-day` перестаёт дорожать с возрастом мира. + +## Целевая модель + +**Основной режим менти — `import` эталонного мира.** От восстановленного +состояния расходятся две ветки — это три режима менти (термины — в +`CONTEXT.md`): + +- **База: `import`** — загрузить эталонный мир из артефакта в git. +- **Ветка A: `next-day`** — пакетно добавить следующий модельный день. +- **Ветка B: `continue`** — живой поток от той же границы (скорость ×60). + +`backfill` уходит мейнтейнеру: это инструмент сборки эталонного мира, +не первый шаг менти (аналогия из соседнего курса Airflow: по умолчанию +загружают готовый первый месяц, а не собирают его генератором). + +### Путь менти (UX) + +1. `make up` — поднять стенд (единственная команда терминала). +2. Airflow UI: `ddl_init` — создать схему. +3. Airflow UI: `world_init` — Trigger с пустой формой. Дефолтная операция — + `import`, путь к артефакту — дефолтом на эталонный мир. DAG сам заливает + Kafka, запускает ETL и сверяет витрины. +4. Superset — готовый трёхдневный мир. +5. Дальше по выбору лабы: `world_next_day` (кнопка без параметров) или + `make generator-continue` (живой поток). + +### Решения + +1. **Эталонный мир**: 3 модельных дня на профиле `daily-wave` (суточная + волна видна, есть «средний» день и сравнение день-к-дню). Собирает + мейнтейнер один раз через `backfill`; хранится в git по фиксированному + пути `data/startup_history/` сжатым `xz` (~13 МБ; замер 2026-07-19: + `xz -9e` жмёт артефакт в 48 раз). Формат не меняется: `import` требует + `raw_topics`, сжатие снимает вопрос размера. Паттерн-образец — + `airflow-greenplum-solution` (сид `demo.sql.xz` в git, стрим-распаковка + при загрузке). +2. **Пульт переименовывается: `generator_control` → `world_init`.** + Дефолт формы — `import` (сейчас `backfill` — корень находки F3); + `backfill` и `check` остаются в выпадашке, но уходят из учебных + инструкций в runbook мейнтейнера. Список DAG'ов читается лесенкой: + `ddl_init` → `world_init` → `world_next_day`. Цена: история прогонов + старого dag_id теряется (на учебном стенде не жалко), доки и + контрактный тест правятся синхронно. +3. **`world_next_day` — отдельный беспараметрный DAG**: триггернул — день + добавился. `schedule` прописан, но DAG paused и `catchup=False`: + процесс тяжёлый, автозапуск включается осознанно (шаг лабы), живой + рост мира и так даёт ветка `continue`. +4. **Инкрементальные счётчики manifest — предусловие расписания.** + Накопительное состояние счётчиков (суммы, множества uid/click_id, + катящаяся контрольная сумма) переезжает в state/manifest; новый день + только добавляется. Полная перечитка Kafka уходит: стоимость дня + становится ~постоянной (сейчас день 2 — 638 с и растёт), риск OOM + снимается. Разбор по коду — hitl-findings, F9. +5. **Один учебный профиль**: `daily-wave` (волна, ×60). `ci` (×1, плоско, + 6h) — служебный для автотестов, менти не предлагается. Перед слиянием + проверить, что runtime-seam-тест не завязан на скорость ×1. + +## Чего здесь не делаем + +- **Не параллелим генерацию** — ломает детерминизм (один поток ГПСЧ, + переходящие визиты); корень стоимости не в ней, а в перечитке (F9). +- **Не трогаем гео-фактуру** — отдельная спека по ADR-0006. +- **Не мигрируем лабы курса** (`docs/course/`) — они отстали от реальности + и потребуют редизайна под три режима; отдельная работа, зафиксирована + issue в трекере. +- **Не чиним здесь F5** (быстрый разлогин Airflow/Superset) **и F4** + (JS-ошибка Grid) — отдельные issues вне фичи. +- **Не включаем расписание по умолчанию** и не занимаемся retention сверх + инкрементальных счётчиков. + +## Форма работ + +Четыре дочерних issue (GitHub, по контракту `docs/agents/issue-tracker.md`; +корневой issue ссылается сюда): + +1. **Эталонный мир**: сборка 3-дневного артефакта, `xz`-хранение в git, + стрим-распаковка на `import`, дефолтный путь в форме. +2. **Поверхность DAG'ов**: переименование в `world_init`, дефолт `import`, + вынос `world_next_day` беспараметрным DAG'ом с paused-расписанием, + синхронная правка доков и контрактных тестов. +3. **Инкрементальные счётчики manifest** (без них расписание не включать). +4. **Один учебный профиль**: `daily-wave` — учебный дефолт, `ci` — + служебный; проверка независимости runtime-seam-теста от ×1. + +Зависимости: (1) и (4) сцеплены (эталонный мир собирается на `daily-wave`); +(3) блокирует включение расписания из (2), но не сам DAG. + +## Проверка + +- Чистый стенд: путь менти из раздела UX проходится пустыми формами; + от `make up` до дашборда — без документации и без терминала (кроме + `make up`). +- `import` эталонного мира — секунды-минуты, не десятки минут. +- После `import` у любого менти совпадают manifest-числа эталонного мира + (события, визиты, пользователи, контрольная сумма). +- `world_next_day` на дне N по времени ~равен дню 1; + `make generated-history-chain-check` зелёный на стыках. +- `make test` / `make lint` зелёные; профиль `ci` продолжает обслуживать + автотесты. + +## Решения и отклонённые варианты + +- **Без артефакта, просто ускорить backfill** — отклонено: время менти и + воспроизводимость (общий файл надёжнее «одинаковой генерации у всех»). +- **Мир «из коробки» при `make up`** — отклонено: механизм становится + невидимым, магия в учебном стенде мстит; явный `import` — учебная + ценность и общее начало обеих лаб. +- **Расписание включённым по умолчанию** — отклонено: эпизодический стенд, + тяжёлый процесс, сюрпризы catchup. +- **Три тонких DAG вместо пульта** — отклонено: плодит сущности; пульт с + правильным дефолтом + один тонкий `world_next_day` достаточно. +- **git-lfs / релизные ассеты для артефакта** — отклонено: после `xz` + артефакт помещается в обычный git. + +## Влияние на документацию + +- `README.md`, `docs/OPERATIONS.md`, `docs/ARCHITECTURE.md`, + `docs/REPO_MAP.md` — новый путь менти и имена DAG'ов (в тех же PR, что + и изменения). +- `docs/runbooks/startup-history.md` — `backfill`/сборка эталонного мира + переезжают сюда как процедура мейнтейнера; дополнить про размер и + хранение. +- `CONTEXT.md` — термины «мир (стенда)», «эталонный мир», «три режима + менти» (добавлены этой же спекой). +- Лабы `docs/course/` — сознательно не здесь (см. «Чего не делаем»).