chore(scratch): уборка одноразовых материалов и handoff новой фичи

- Зачем:
  - после слияния feature/data-generator одноразовые материалы .scratch
    (задачи 01-21, PRD, журналы, старые handoff'ы) больше не нужны:
    долговечное живёт в docs/, история — в git (срез 0e312b3).
- Что:
  - удалено всё содержимое .scratch (50 файлов).
  - добавлен handoff 20260719-2229-mentee-path-start.md: направление
    редизайна пути менти (4 задачи) и пилот GitHub Issues.
- Проверка:
  - git show --stat; в .scratch остался только новый handoff.
This commit is contained in:
2026-07-19 22:29:44 +03:00
parent 0e312b3c6c
commit de7f62cbab
48 changed files with 89 additions and 5114 deletions
@@ -1,37 +0,0 @@
Status: ready-for-human
# Минимальный связанный визит в новом ядре
## What to build
Построить первый проверяемый срез нового steady-stream генератора: публичный
вызов генеративного ядра создаёт один визит, который выглядит как нормальная
единица доменной модели `пользователь -> визит -> событие`.
Визит должен выпускать несколько связанных событий: один общий `click_id`,
разные `event_id`, общий device/geo-контекст и согласованные записи для четырёх
Kafka-топиков. Kafka на этом шаге не нужна: задача проверяет форму данных на
выходе генеративного ядра.
Источник решений: `docs/specs/2026-06-09-generator-rework-hierarchical.md`,
`docs/specs/2026-06-10-generator-math-model.md`, `CONTEXT.md`.
## Acceptance criteria
- [x] Тест через публичный интерфейс генератора создаёт один визит с несколькими
browser-событиями под одним `click_id`.
- [x] У всех событий визита разные валидные `event_id`, а `click_id` не берётся
из исходного сида.
- [x] Для каждого browser-события есть связанная location-запись с тем же
`event_id`.
- [x] Для каждого события визита публикуются device- и geo-записи с тем же
`click_id`; их содержимое в рамках визита одинаковое, как в сиде.
- [x] Один `click_id` связан ровно с одним `user_domain_id`.
- [x] Старый тестовый контракт "одно событие = один новый click_id" удалён или
заменён на новый контракт визита.
## Blocked by
None - can start immediately.
## Comments
@@ -1,56 +0,0 @@
Status: ready-for-human
# Уборка сервиса генератора перед активными визитами
## What to build
Разнести текущий `generator/generator.py` на понятные модули, не меняя внешнее
поведение генератора. Цель — подготовить код к задаче 03, где появятся активные
визиты между тиками.
Источник решений: `docs/specs/2026-06-11-generator-service-cleanup.md`,
`docs/specs/2026-06-10-generator-math-model.md`, задачи 01-03 в этом каталоге.
## Acceptance criteria
- [x] Генеративная модель визита отделена от Kafka, состояния, метрик и
сервисного цикла.
- [x] Код загрузки и индексации JSONL-сида вынесен из сервисного слоя.
- [x] Расчёт интенсивности отделён от генерации одного визита.
- [x] Kafka publisher, Kafka-state и создание служебных топиков не смешаны с
генеративной моделью.
- [x] `generator/generator.py` остаётся тонкой точкой входа или совместимым
фасадом, чтобы не ломать запуск и тесты без причины.
- [x] `generate_tick_batch()` не остаётся в чистой генеративной модели визита:
он вынесен в переходный тиковый слой и подготовлен к замене активными визитами
в задаче 03.
- [x] Dockerfile и запуск контейнера обновлены под новую структуру файлов.
- [x] Все существующие тесты генератора проходят.
- [x] README генератора кратко отражает новую структуру файлов.
## Non-goals
- Не реализовывать активные визиты между тиками.
- Не добавлять популяцию пользователей и возвраты.
- Не вводить состояние версии 2.
- Не менять Kafka-топики и формат событий.
- Не удалять `generator_batch_history`, если отдельно не доказано, что он больше
не нужен.
## Blocked by
- `.scratch/feature-data-generator/issues/02-visit-page-path-and-monotonic-time.md`
## Blocks
- `.scratch/feature-data-generator/issues/03-tick-stream-with-active-visits.md`
## Comments
Задача появилась после разбора распухания сервиса: первые два среза уже внесли
новую модель визита, а третий срез добавит состояние активных визитов. Перед ним
нужно отделить модель, тиковый слой и Kafka-интеграцию.
2026-06-11: исходники перенесены не россыпью в `generator/`, а в пакет
`generator/src/clickstream_generator/`. `generator/generator.py` оставлен тонким
фасадом и точкой входа для совместимости.
@@ -1,37 +0,0 @@
Status: ready-for-human
# Путь визита по страницам и монотонное время
## What to build
Расширить минимальный визит до правдоподобного пути по страницам. Визит должен
начинаться со страницы, выбранной по стартовому распределению, дальше идти по
марковской цепочке и назначать событиям запланированные метки времени, которые
строго растут внутри `click_id`.
Задача остаётся без Kafka и без популяции пользователей: она проверяет качество
одного визита как учебного объекта и как основы будущей воронки.
Источник решений: `docs/specs/2026-06-10-generator-math-model.md`, раздел
«События и путь по страницам».
## Acceptance criteria
- [x] Тест через публичный интерфейс генератора показывает, что
`event_timestamp` внутри одного `click_id` строго возрастает.
- [x] Метки времени событий являются запланированными моментами визита, а не
одинаковым `now()` для всего набора событий.
- [x] Паузы внутри визита меньше 30 минут; 95-й перцентиль на симуляции лежит в
масштабе единиц минут.
- [x] Визит не может зациклиться бесконечно: длина ограничена потолком
`GEN_MAX_SESSION_EVENTS` или его новым эквивалентом.
- [x] Доля визитов, дошедших до `/confirmation`, на симуляции сопоставима с
ориентиром сида около 25%.
- [x] Визит может продолжаться после `/confirmation`; `/confirmation` не
считается обязательным последним событием визита.
## Blocked by
- `.scratch/feature-data-generator/issues/01-minimal-connected-visit.md`
## Comments
@@ -1,41 +0,0 @@
Status: ready-for-human
# Поток по тикам с активными визитами
## What to build
Разложить визиты по тикам генератора. Визит может жить дольше одного тика:
генератор хранит активные визиты между вызовами, выпускает только те события,
чьё запланированное время наступило, и не приклеивает все события к текущему
тику.
Задача проверяет основную потоковую механику без Kafka и без восстановления
после рестарта.
Источник решений: `docs/specs/2026-06-10-generator-math-model.md`, раздел
«Раскладка по тикам».
## Acceptance criteria
- [x] Тест через публичный интерфейс генератора показывает, что один `click_id`
может появляться в выходе нескольких последовательных тиков.
- [x] На каждом тике выпускаются только созревшие события активных визитов.
- [x] `event_timestamp` отражает запланированное время события, а не время
фактической отправки тика.
- [x] Завершённые визиты больше не выпускают события в следующих тиках.
- [x] При достижении потолка активных визитов новые рождения в этот тик
пропускаются, а бюджет не копится бесконечно.
- [x] Конфигурация запрещает состояние, где потолок активных визитов не меньше
потолка популяции пользователей.
## Blocked by
- `.scratch/feature-data-generator/issues/02-visit-page-path-and-monotonic-time.md`
- `.scratch/feature-data-generator/issues/02-5-generator-service-cleanup.md`
## Comments
2026-06-11: реализовано через `TickStreamGenerator` в тиковом слое. Проверка:
`uv run --with-requirements generator/requirements.txt pytest generator/tests -q`
— 81 passed. После ревью исправлено: входной `event_budget` снова трактуется
как бюджет событий, а не как число рождений визитов.
@@ -1,49 +0,0 @@
Status: ready-for-human
# Популяция пользователей и возвраты
## What to build
Добавить ограниченную популяцию пользователей с постоянными `user_domain_id`.
Новые визиты должны доставаться либо новым пользователям, либо возвращающимся
пользователям из популяции, доступным после кулдауна и не имеющим активного
визита.
Задача должна превратить поток из набора независимых визитов в живую модель
возвратов пользователей.
Источник решений: `docs/specs/2026-06-10-generator-math-model.md`, разделы
«Популяция пользователей» и «Визиты».
## Acceptance criteria
- [x] На длинной симуляции получается здоровая пирамида
`users < sessions < events`.
- [x] Один пользователь может иметь несколько `click_id` во времени.
- [x] Один `click_id` принадлежит ровно одному `user_domain_id`.
- [x] Пользователь не получает новый визит раньше минимального кулдауна возврата.
- [x] Если доступных возвращающихся пользователей нет, новый визит достаётся
новому пользователю.
- [x] Размер активной популяции не растёт выше заданного потолка; при
переполнении вытесняется давно неактивный пользователь без активного визита.
## Blocked by
- `.scratch/feature-data-generator/issues/03-tick-stream-with-active-visits.md`
## Comments
2026-06-11: реализовано через ограниченную популяцию в `TickStreamGenerator`.
Профиль пользователя переиспользуется между визитами, активный пользователь не
получает второй визит, после завершения действует `GEN_MIN_RETURN_MINUTES`.
Если доступных возвратов нет или срабатывает `GEN_P_NEW_USER`, создаётся новый
пользователь с вытеснением давно неактивного профиля без роста выше
`GEN_POPULATION_MAX`.
Проверка: `uv run --with-requirements generator/requirements.txt pytest
generator/tests -q` — 88 passed. В песочнице `/snap/bin/uv` падает на
`snap-confine`, поэтому полный pytest запускался вне песочницы.
После свежего ревью исправлены две детали: UUID теперь создаются через единый
ГПСЧ генератора, а кулдаун пользователя начинается от запланированного времени
последнего события визита, а не от времени позднего тика.
@@ -1,55 +0,0 @@
Status: ready-for-human
# Интенсивность и калибровка потока
## What to build
Связать иерархическую модель с целевой интенсивностью потока. Старая идея
Пуассона, часового коэффициента и jitter должна задавать бюджет активности, из
которого рождаются визиты; фактические события выходят по запланированным
паузам активных визитов.
На этом шаге также нужно проверить, что параметры модели дают поток,
сопоставимый с профилем сида: длина визита около 10 событий в среднем и по
медиане, доля дошедших до `/confirmation` около 25%, межсессионные паузы
согласованы с формулой из спеки.
Источник решений: `docs/specs/2026-06-10-generator-math-model.md`, разделы
«Связка с моделью интенсивности», «Параметры» и «Критерии приёмки».
## Acceptance criteria
- [x] Средняя интенсивность событий за длинное окно соответствует
`GEN_LAMBDA_BASE_PER_MIN * часовой коэффициент` с разумным отклонением.
- [x] Рождения визитов рассчитываются из бюджета событий и средней длины визита,
а не из старых границ `GEN_MIN/MAX_EVENTS_PER_TICK` в прежнем смысле.
- [x] Доля новых пользователей за длинное окно близка к `GEN_P_NEW_USER`.
- [x] Межсессионные паузы одного пользователя не меньше кулдауна, а среднее на
дефолтах находится в районе расчёта из спеки.
- [x] Распределение длины визита на симуляции сопоставимо с сидом: медиана около
10, среднее около 10, максимум не выше потолка.
- [x] Воронка по шагам `/home -> товары -> /cart -> /payment -> /confirmation`
монотонно затухает на длинной симуляции.
## Blocked by
- `.scratch/feature-data-generator/issues/04-user-population-and-returns.md`
## Comments
2026-06-11: реализована калибровка потока через `TickStreamGenerator`: событийный
бюджет тика копится как бюджет рождений визитов через ожидаемую среднюю длину
визита (~10 событий), а фактические события выходят по запланированным паузам
активных визитов. Дефолт `GEN_LAMBDA_BASE_PER_MIN` в коде снижен до 30
событий/мин и синхронизирован с обычным запуском через `docker-compose.yml`;
`GEN_MIN/MAX_EVENTS_PER_TICK` теперь ограничивают событийный бюджет тика до
пересчёта в рождения визитов.
Калибровка таблицы переходов и пауз проверена тестами: средняя интенсивность
длинного окна близка к целевому бюджету, доля новых пользователей близка к
`GEN_P_NEW_USER`, межсессионные паузы не меньше кулдауна и имеют часовой
масштаб около формульного ориентира из спеки, длина визита сопоставима с сидом,
доля `/confirmation` остаётся около 25%, воронка монотонно затухает.
Проверка: `uv run --with-requirements generator/requirements.txt pytest
generator/tests -q` — 96 passed.
@@ -1,55 +0,0 @@
Status: ready-for-human
# Состояние версии 2 и рестарт
## What to build
Расширить состояние генератора до версии 2: кроме тика и состояния генератора
случайных чисел сохранять популяцию пользователей и активные визиты. После
рестарта генератор должен продолжать коротко прерванные визиты и закрывать
сильно просроченные, не теряя популяцию пользователей.
Задача проверяет поведение состояния через публичные методы сохранения и
восстановления, без реального Kafka-брокера там, где достаточно сериализации.
Источник решений: `docs/specs/2026-06-10-generator-math-model.md`, раздел
«Персистентность через рестарты».
## Acceptance criteria
- [x] Состояние версии 2 сериализуется в JSON и восстанавливает тик, ГПСЧ,
популяцию пользователей и активные визиты.
- [x] Старое состояние версии 1 или битое состояние не валит генератор:
фиксируется предупреждение, генератор начинает с чистого листа.
- [x] После простоя не больше 30 минут активный визит продолжается, а созревшие
события досылаются со своими исходными запланированными метками времени.
- [x] После долгого простоя просроченный активный визит закрывается без досылки
остатка.
- [x] Популяция пользователей переживает простой любой длины.
- [x] `GEN_STATE_RESET=true` явно сбрасывает состояние, как и раньше.
## Blocked by
- `.scratch/feature-data-generator/issues/05-intensity-and-flow-calibration.md`
## Comments
2026-06-11: задачу нельзя считать самостоятельной к реализации, пока не закрыты
предыдущие срезы генератора: 02 (путь визита и монотонное время), 03 (тики и
активные визиты), 04 (популяция и возвраты), 05 (интенсивность и калибровка).
Преждевременный кодовый задел state v2 был откатан, чтобы он не попал в коммиты
следующих задач. Возвращаться к этой задаче нужно после реализации зависимостей
и заново проверять все acceptance criteria на актуальной модели генератора.
2026-06-11: реализовано состояние версии 2 для `TickStreamGenerator`: в снимок
попадают тик, ГПСЧ, компактная популяция пользователей, компактные активные
визиты и остаток бюджета рождений визитов. `GeneratorService` теперь владеет
одним тиковым потоком, сохраняет его снимок и восстанавливает его при старте.
Короткий простой продолжает активные визиты и досылает созревшие события с
исходными метками времени; долгий простой закрывает просроченные визиты без
досылки остатка; популяция сохраняется.
Старое state v1 и битое v2-state деградируют в чистый старт с предупреждением;
`GEN_STATE_RESET=true` по-прежнему пропускает загрузку состояния. Проверка:
`uv run --with-requirements generator/requirements.txt pytest generator/tests -q`
— 112 passed.
@@ -1,52 +0,0 @@
Status: ready-for-human
# Подключение к сервису и документации
## What to build
Подключить новое генеративное ядро к рабочему steady-stream сервису. Генератор
должен запускаться прежними командами, писать сообщения в прежние Kafka-топики,
сохранять полезные метрики Prometheus и больше не документироваться как
концептуально сломанный источник.
На этом шаге нужно синхронизировать пользовательскую документацию и заметку об
известных проблемах с новой моделью.
Источник решений: `docs/specs/2026-06-09-generator-rework-hierarchical.md`,
`docs/specs/2026-06-10-generator-math-model.md`, `generator/README.md`,
`generator/KNOWN_ISSUES.md`.
## Acceptance criteria
- [x] Генератор в режиме steady-stream пишет связанные сообщения в
`browser_events`, `location_events`, `device_events`, `geo_events`.
- [x] Существующие команды запуска и остановки генератора остаются рабочими или
документация явно описывает замену.
- [x] Метрики Prometheus продолжают показывать успешные тики, ошибки публикации
и объём отправленных событий.
- [x] Интеграционный тест или проверка с мок-публикацией подтверждает, что
сервисный контур использует новую модель, а не старую плоскую генерацию.
- [x] `generator/README.md` описывает новые параметры и новую модель без старых
предупреждений о сломанном `click_id`.
- [x] `generator/KNOWN_ISSUES.md` обновлён: старый дефект закрыт или перенесён в
исторический раздел, не как актуальный блокер.
## Blocked by
- `.scratch/feature-data-generator/issues/06-state-v2-and-restart.md`
## Comments
- 2026-06-11: добавлена проверка сервисных тиков с мок-публикацией без Kafka:
steady-stream публикует несколько событий одного визита с общим `click_id`,
разными `event_id` и согласованными записями во всех четырёх топиках; история
тиков остаётся `success`.
- 2026-06-11: `docker-compose.yml` теперь пробрасывает переменные генератора
через `${VAR:-default}`, поэтому команды вида `GEN_STATE_RESET=true docker
compose up -d generator` реально меняют окружение контейнера. Внутренние
`KAFKA_BOOTSTRAP_SERVERS` и `GEN_DATA_DIR` оставлены безопасными значениями
для контейнера.
- 2026-06-11: `generator/README.md` и `generator/KNOWN_ISSUES.md` синхронизированы
с новой моделью, состоянием v2 и историческим статусом старого дефекта.
- Проверка: `uv run --with-requirements generator/requirements.txt pytest
generator/tests -q` — 113 passed; `git diff --check` — без замечаний.
@@ -1,158 +0,0 @@
# Модельное время и стартовая история генератора
Status: Implemented
## Зачем
Нужно довести решение ADR-0005 и ADR-0006 до рабочего процесса: генератор живёт
по модельному времени, умеет быстро создать прошлое, сохранить слепок состояния
и продолжить поток так, чтобы результат был проверяем в ClickHouse.
Источник решений:
- `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`
- `docs/adr/0005-generator-model-clock.md`
- `docs/adr/0006-generation-as-sole-analytics-source.md`
- `docs/research/2026-06-11-subagent-coordinator-experiment.md`
## Общие правила приёмки
- Каждый кодовый срез должен заканчиваться проверкой через ClickHouse, а не
только локальными тестами генератора.
- Кодовые AFK-задачи идут через `/tdd`: один поведенческий тест, минимальная
реализация, зелёная проверка. HITL- и документные задачи идут через
обсуждение и фиксацию решения, без искусственного теста.
- Координатор не работает в `/goal` на всю цепочку. Один worker получает один
issue, не коммитит и не реализует следующие задачи.
- После реализации worker делает саморевью без правок. Координатор
классифицирует находки и возвращает только обязательные исправления.
- В задачах 03 и 05 worker заранее даёт промежуточный статус, если статистический
прогон или стендовая проверка идут долго.
- Если исправление после review gate меняет дизайн, формат state, манифест или
схему проверки, результат исправления проходит повторный review gate.
- Коммиты делает координатор после своих проверок и `git status --short`.
- Ручная проверка дашбордов глазами выполняется только в конце всей цепочки.
## Сквозные инварианты
- Рабочий контракт реализации живёт в
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`, раздел
«Рабочий контракт реализации». Задачи 02–06 берут имена настроек, формат
манифеста, границу `T_end` и правила проверки оттуда.
- `event_timestamp` — модельное время, а не настенные часы компьютера.
- Операционные метки сервиса, история пачек, метрики здоровья и длительность
тика остаются настенным временем, если отдельная задача не докажет обратное.
- Живой ход часов идёт фиксированным модельным шагом
`GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`; промотка прошлого тоже повторяется
точно при тех же настройках и чистом состоянии.
- При ×K модельное время и событийный бюджет идут по модельной длительности
тика, а не по реальной длительности сна процесса.
- Часовой пояс модельных часов явно задан в контракте; дневной коэффициент
считается по нему, а не по неявному локальному времени.
- После сбоя точка возобновления считается из сохранённой связки модельного и
настенного времени, а не простым `datetime.now()`.
- Стартовая история — это события плюс слепок состояния плюс манифест, чтобы не
смешать данные от разных `GEN_SEED`, `T0` и `T_end`.
- Стартовая история покрывает `[T0, T_end)`, живое продолжение начинается из
слепка на `T_end`; на стыке не должно быть дублей и дыр.
- Повторная проверка на чистом стенде должна быть воспроизводимой: либо команда
явно чистит ClickHouse, Kafka-топики данных и состояние генератора, либо
процесс идемпотентен.
## Задачи
Номер в имени файла — просто идентификатор задачи, порядок выполнения он не
задаёт. Порядок и зависимости живут здесь и в секциях «Blocked by» самих задач;
при изменении порядка файлы не переименовываются.
Сделано (ядро фичи, закрыто на триаже 2026-07-04):
- `issues/01-time-and-startup-history-contract.md` — контракт модельного
времени и стартовой истории.
- `issues/02-model-time-to-clickhouse.md` — минимальный поток по модельному
времени до ClickHouse.
- `issues/03-model-speed-and-day-factor.md` — ×K и дневной коэффициент по
модельному времени.
- `issues/04-state-v2-model-resume.md` — восстановление state v2 от модельной
точки возобновления.
- `issues/05-startup-history-backfill-to-clickhouse.md` — промотка прошлого,
стартовая история и живое продолжение до ClickHouse.
- `issues/06-generated-history-as-analytics-source.md` — штатный путь стенда
переводится на стартовую историю как источник аналитики.
- `issues/07-startup-history-portable-artifact-and-usage-docs.md` — портативный
артефакт стартовой истории и runbook.
- `issues/11-generator-launch-verbs-and-profiles.md` — глаголы, длительность и
профили запуска генератора.
- Airflow-пульт генератора, быстрый `daily-wave`, фикс фактуры визита на
восстановлении, читаемый гео-график и миграция курса на генерацию — закрыты
в очереди 2026-07-04.
- Доработки по кросс-линейному ревью 2026-07-05 закрыты в очереди 2026-07-05:
граница миров, свежий стенд и Airflow-пульт, доверенные проверки
startup-history/Superset, курс на чистом стенде. Подробности и коммиты см. в
issue-файлах и `coordinator-journal.md`.
Оставшаяся задача вне текущей очереди:
- `issues/13-...` — доливка истории от слепка; без приоритета,
`needs-triage`. После кросс-линейного ревью 2026-07-05 блокируется
задачами 15 и 17: доливка тиражирует стыки мира и должна опираться на
доверенную проверку фактической границы.
Дополнительная задача закрыта:
- `issues/19-test-and-lint-targets.md` — единые `make test` и `make lint` для
коммит-гейта; `done`. Возникло из coordinator-loop 2026-07-05:
координатору пришлось вручную собирать тестовый набор из частных команд.
Доработки по кросс-линейному ревью 2026-07-05 (закрыто):
- `issues/15-world-boundary-after-cross-review.md` — граница миров после
обновления, сброса и консольных запусков.
- `issues/16-fresh-stand-generator-control.md` — свежий стенд и Airflow-пульт
без неявных ручных шагов.
- `issues/17-trusted-checks-startup-history-superset.md` — доверенные проверки
startup-history и Superset export.
- `issues/18-course-clean-stand-startup-history.md` — курс на чистом стенде по
startup-history.
```mermaid
flowchart LR
i07["07 артефакт + runbook"] --> i11["11 глаголы запуска"]
i11 --> i12["12 DAG-пульт"]
i07 --> i08["08 миграция курса"]
i12 -. "приёмка через пульт" .-> i08
i14["14 быстрый профиль"] --> i08
i09["09 фикс стыка"] --> i13["13 доливка"]
i10["10 гео-карта"]
i15["15 граница миров"] --> i18["18 курс на чистом стенде"]
i16["16 свежий стенд"] --> i18
i17["17 доверенные проверки"] --> i18
i15 --> i13
i17 --> i13
i19["19 make test/lint"]
```
## Контрольные точки
- После задачи 3 нужен внешний review gate по сквозному инварианту времени:
проверить, где ещё остались настенные часы, и не расходятся ли живой путь,
расчёт интенсивности и сохранение состояния.
- После задачи 5 нужен внешний review gate по распределениям и двум путям
генерации: проверить форму данных, стык истории и живого продолжения,
однородность визита до и после восстановления.
Для review gate после задачи 3 reviewer другой родословной сильно желателен.
Для review gate после задачи 5 он обязателен: research показал, что форма
распределений и свойства на стыке двух путей хуже ловятся одной линией проверки.
Если такого reviewer-а нет, координатор останавливает цепочку и явно отдаёт
решение человеку.
## Финальная ручная приёмка
После задачи 6 человек смотрит Superset/Grafana и проверяет, что стенд живёт на
генерации: видны история, возвраты, воронка и суточное «дыхание». Если текущих
панелей не хватает для такого просмотра, создаётся отдельная задача на панель
или runbook, а не расширяется эта цепочка задним числом.
Миграция уроков по ADR-0006 сознательно отслеживается отдельно. Задача 6 должна
создать follow-up, если учебные материалы требуют нетривиальной переделки.
@@ -1,577 +0,0 @@
# Журнал coordinator-loop: generator-model-time-startup-history
Дата старта: 2026-07-04.
## 2026-07-04
- Старт цепочки: 07 -> 11. Дерево перед стартом чистое (`git status --short`
без вывода). Блокеры 07 (`05`, `06`) имеют `Status: done`.
- Issue 07 запущен worker-у. Режим ревью: гейт; требуется проверка по задаче и
по коду разными субагентами через `claude-team-review`.
- Issue 07: принят индекс worker-а со статусом `BLOCKED`. Unit: `uv run
--with-requirements generator/requirements.txt pytest generator/tests -q` —
PASS, 139 passed. Shell syntax: PASS. `git diff --check`: PASS. Стендовая
Docker-проверка импорта: BLOCKED, `docker` отсутствует в среде worker-а.
Review origin от worker-а: оба reviewer-а не созданы из-за sandbox/app-server
blocker. Chain-level решение: не коммитить, добрать два reviewer-индекса
отдельными субагентами координатора.
- Issue 07 review по задаче: `CHANGES_REQUESTED`. Finding: проверка после
импорта не доказывает совпадение ClickHouse с manifest/original; текущий
`generated-history-check` проверяет здоровье диапазона, но не читает
артефакт или `generator_startup_history_manifest`. Возвращено worker-у как
обязательное исправление issue 07.
- Issue 07 review по коду: `CHANGES_REQUESTED`. Findings: `state_version` из
manifest не участвует в live-восстановлении startup-history state; импорт
может задублировать data-топики при повторном или частично упавшем запуске.
Возвращено worker-у: первое чинить, второе чинить или явно классифицировать
как вне скоупа с защитой контракта clean-stand import.
- Issue 07: worker вернул `DONE` после исправлений. Unit: PASS, 142 passed.
Docker export/import/check: PASS; manifest-aware сверка ClickHouse с manifest:
PASS, `events=16054`, `visits=1516`, `users=445`. Повторный импорт на
непустых data-топиках: PASS, отказ до публикации дублей. `git diff --check`:
PASS. Chain-level решение: повторить review gate на обновлённом diff.
- Issue 07 rerun review по задаче: `APPROVED`.
- Issue 07 rerun review по коду: `CHANGES_REQUESTED`. Findings: импорт не
защищён от частичной записи data-топиков до compact state/manifest; артефакт
может не воспроизводить raw Kafka value байт-в-байт из-за повторной
сериализации dict. Возвращено worker-у как обязательная доработка или явное
уточнение контракта с тестами.
- Issue 07: worker вернул финальный индекс `DONE_WITH_CONCERNS`. Исправления:
preflight clean Kafka data-топиков, rollback import-топиков при ошибке,
raw JSON value в артефакте и публикация bytes без повторной сериализации.
Unit: PASS, 143 passed. Docker export/import/check: PASS. Raw Kafka value
compare: PASS, SHA совпал, `cmp=0`. Chain-level concern: после последних
исправлений code rerun ещё не выполнен.
- Issue 07 third code gate: `CHANGES_REQUESTED`. Finding: raw Kafka value в
артефакте всё ещё, по мнению reviewer-а, строится повторной сериализацией dict
и валидируется против той же сериализации, а не доказывает связь с фактически
опубликованными Kafka bytes. Возвращено worker-у для исправления или
аргументированного отклонения через контракт и тест.
- Issue 07 xhigh narrow code gate: `CHANGES_REQUESTED`. Findings: `raw_topics`
остаётся необязательным, поэтому артефакт без raw bytes может пройти import с
повторной сериализацией; fallback импорта через publisher без `publish_records`
игнорирует сохранённые raw records. Возвращается worker-у как обязательная
доработка.
- Issue 07: worker вернул `DONE_WITH_CONCERNS` после xhigh-находок. Исправлено:
для artifact v1.0 `raw_topics` обязательны; importer требует `publish_records`
и публикует только raw records. Unit: PASS, 146 passed. Chain-level concern:
xhigh code rerun после последней правки ещё не выполнен.
- Issue 07 xhigh code rerun: `APPROVED`. Findings: нет. Unit: PASS, 146
passed. Shell syntax: PASS. `git diff --check`: PASS. Chain-level решение:
открыть коммит-гейт 07.
- Issue 07 commit gate: staged files в границах issue 07, журнал не staged.
Unit: PASS, 146 passed. Shell syntax: PASS. `git diff --cached --check`:
PASS. Commit: `3c38465` (`feat(generator): добавлен артефакт стартовой
истории`).
- Перед issue 11: процессный журнал issue 07 отделён docs-коммитом `d2899e9`.
Дерево чистое. Блокер 11 (`07`) имеет `Status: done`.
- Issue 11 запущен worker-у. Режим ревью: обычный; требуется один code review
через `claude-team-review` после реализации. Условие эскалации: если worker
меняет state-схему, сериализацию или контракт manifest, перевести в гейт.
- Issue 11: worker вернул `BLOCKED` только из-за отсутствия reviewer mechanism.
Unit: PASS, 153 passed. Shell syntax: PASS. `git diff --check`: PASS. Dry-run
`PROFILE=daily-wave ... run_generator.sh backfill`: PASS. Chain-level
решение: добрать code review отдельным reviewer-субагентом координатора.
- Issue 11 code review: `CHANGES_REQUESTED`. Finding: профиль запуска виден в
логах, но не пишется в manifest/артефакт; критерий issue 11 требует, чтобы
выбранный профиль был виден в логах/manifest. Возвращено worker-у как
обязательное исправление.
- Issue 11: worker вернул `DONE` после исправления. `launch_profile` добавлен
в manifest/артефакт, state-схема не менялась. Unit: PASS, 154 passed.
`git diff --check`: PASS. Chain-level решение: повторить code review.
- Issue 11 code rerun: `APPROVED`. Findings: нет. `make generator-test`: PASS,
154 tests. Shell syntax: PASS. `git diff --check`: PASS. Dry-run
`backfill`/`continue`/`reset` с `PROFILE=daily-wave`: PASS. Chain-level
решение: открыть коммит-гейт 11.
- Issue 11 commit gate: Unit: PASS, 154 passed. Shell syntax: PASS.
`git diff --check`: PASS. Dry-run `PROFILE=daily-wave ... run_generator.sh
backfill`: PASS после повторного запуска вне sandbox из-за `snap-confine`
внутри sandbox.
- Issue 11 commit: `4f8f992` (`feat(generator): добавлены профили запуска`).
- Диагностика nested review tools: read-only субагент не видит
`multi_agent_v1.spawn_agent`; `tool_search` по `multi-agent spawn subagent`,
`claude-team-review`, `reviewer` не находит инструменты для запуска
субагентов. Process finding: в текущей среде worker не может сам запускать
reviewer-а; координатор должен запускать reviewer-субагентов сам и возвращать
worker-у только индекс находок.
- Final chain review запущен read-only reviewer-у в режиме `xhigh`.
- Final chain review: `CHANGES_REQUESTED`, low finding — PRD всё ещё держит
07/11 в остатке и объясняет статус 12 отсутствием глаголов. Исправлено:
07/11 перенесены в сделанное, остаток основной ветки начинается с 12.
- Уточнение диагностики nested review tools: первый диагност был read-only
explorer, что не доказывает toolset worker-а. Запущен отдельный диагност с
`agent_type=worker`, без правок, для проверки той же гипотезы в роли worker-а.
- Worker-диагност подтвердил: видит `web`, `image_gen`, `functions`,
`tool_search`, `multi_tool_use`; `tool_search` не находит multi-agent spawn
или `claude-team-review`; `multi_agent_v1.spawn_agent` недоступен. Вывод:
worker не может сам запускать reviewer-субагентов в этой среде; процесс нужно
лечить переносом запуска review-субагентов на координатора.
- 20:44 Новая очередь стартовала: 12 -> 14 -> 09 -> 10 -> 08. Дерево перед
стартом чистое (`git status --short` без вывода). Issue 12 запущен worker-у.
Режим ревью: обычный, один code-review от координатора после реализации.
Условие эскалации: если worker меняет state-схему, сериализацию,
межмодульный контракт manifest или нестабильный Airflow API без проверки
Context7, перевести issue в гейт.
- 20:55 Issue 12: worker вернул `DONE_WITH_CONCERNS`. Unit: PASS, 165 tests;
targeted DAG/control tests: PASS, 11 tests; `py_compile`: PASS;
`docker compose config --quiet`: PASS. Context7 Airflow 2.10.5: `Param`
enum/title/description и `TriggerDagRunOperator(wait_for_completion=True)`.
Concern: живой проход через Airflow UI не выполнен; готовая команда стенда
дана worker-ом. Chain-level решение: открыть обычный code-review.
- 20:55 Issue 12 code review запущен координатором: свежий observer-субагент,
рабочий уровень, без правок репозитория и среды.
- 20:59 Issue 12 code review: `CHANGES_REQUESTED`. Finding:
`startup_history_artifact.py:239` — backfill/import на непустом Kafka data
topic падает без подсказки `make clean`. Возвращено worker-у дословно.
- 21:00 Issue 12: worker вернул `DONE_WITH_CONCERNS` после ревью-находки.
Исправлено: `airflow_control.py:102` оборачивает ошибку непустых Kafka
data-топиков подсказкой `make clean`; добавлен регрессионный тест. Targeted
tests: PASS, 12 tests; unit: PASS, 166 tests; `py_compile`: PASS.
- 21:00 Process update: перечитан `references/effort-levels.md` после правки
пользователем. Для Codex CLI обычный worker на обычном issue = экономный
`medium`; любое code review и worker на гейтовом issue = рабочий `high`;
узкий спор/финальное chain review = усиленный `xhigh`. Дальнейшие запуски —
с явным уровнем по таблице.
- 21:00 Issue 12 code rerun запущен координатором: свежий observer-субагент,
рабочий уровень `high`, без правок репозитория и среды.
- 21:04 Issue 12 code rerun: `APPROVED`. Targeted tests: PASS; generator
tests: PASS. Chain-level решение: открыть коммит-гейт 12.
- 21:06 Issue 12 commit gate: staged files в границах issue 12, журнал не
staged. Unit: PASS, 166 tests; `docker compose config --quiet`: PASS;
`py_compile`: PASS; `git diff --cached --check`: PASS. Commit: `dd4af82`
(`feat(airflow): добавлен пульт управления генератором`).
- 21:07 Перед issue 14: процессный журнал issue 12 отделён docs-коммитом
`200bb82`. Дерево чистое. Блокеров нет; issue 12 имеет `Status: done`.
Issue 14 запущен worker-у. Режим ревью: обычный; worker — `medium`, code
review после реализации — `high`. Условие эскалации: изменение частоты
сохранения state, state-схемы или контракта восстановления переводит issue в
гейт.
- 21:12 Issue 14: worker вернул `DONE_WITH_CONCERNS`. `daily-wave` переведён
на `speed=60`, `tick=1`; `ci` не изменён; успешные live-тик логи приглушены
до DEBUG; state-сохранение не менялось. `make generator-test`: PASS, 167
tests. Concern: стендовая backfill/import/live-проверка и дашборд не
запускались.
- 21:12 Issue 14 code review запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:16 Issue 14 code review: `APPROVED`. Findings: нет. Reviewer checks:
profile values PASS, DAG enum from `PROFILES` PASS, peak budget below cap
PASS. `pytest` без `--with pytest` у reviewer-а не стартовал.
- 21:17 Issue 14 commit gate: staged files в границах issue 14, журнал не
staged. `make generator-test`: PASS, 167 tests; `py_compile`: PASS;
`git diff --cached --check`: PASS. Commit: `9d1bcc4`
(`feat(generator): ускорен учебный профиль daily-wave`).
- 21:18 Перед issue 09: процессный журнал issue 14 отделён docs-коммитом
`0cbfe9b`. Дерево чистое. Блокеров нет. Issue 09 запущен worker-у. Режим
ревью: гейт; worker reasoning_effort=`high`; после реализации требуются два
ревью-субагента от координатора: task review `high` и code review `high`.
- 21:33 Issue 09: worker вернул `DONE_WITH_CONCERNS`. State поднят до v3,
`base_click_id` сериализуется и валидируется; неизвестный донор — ошибка;
добавлены unit-тесты восстановления фактуры и повторяемая стыковая проверка
`CHECK_LIVE_SEAM=1`. Unit: PASS, 171 tests; `bash -n` seam script: PASS;
`git diff --check`: PASS. Concern: стендовая ClickHouse-проверка 2d
backfill + live не запускалась.
- 21:33 Issue 09 task review запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:33 Issue 09 code review запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:38 Issue 09 task review: `CHANGES_REQUESTED`. Finding:
`scripts/check_generated_analytics.sh:315` — device/os/geo проверяются через
схлопнутый `dds.click`, а критерий требует ODS-уровень; нужен `uniqExact` по
`ods.device_by_click` и `ods.geo_by_click` для переходящих `click_id`.
- 21:38 Issue 09 code review: `CHANGES_REQUESTED`. Findings:
`runtime.py:133` — микросекунды offset считаются через float и теряют 1 мкс,
тест скрывает `event_timestamp`; `Makefile:48` — обычный
`make generated-history-check` не включает seam-блок и может быть ложнозелёным.
Индексы обоих ревью возвращены worker-у дословно.
- 21:41 Issue 09: worker вернул `DONE_WITH_CONCERNS` после гейт-находок.
Исправлено: целочисленный offset микросекунд, тест снова сравнивает
`event_timestamp`, seam check auto включается после live-строк, ODS-проверка
device/os/geo добавлена, `make generated-history-check` использует auto.
Unit: PASS, 172 tests; `bash -n`: PASS; `git diff --check`: PASS.
- 21:41 Issue 09 task rerun запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:41 Issue 09 code rerun запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:45 Issue 09 code rerun: `APPROVED`. Findings: нет. Checks: timestamp
offset без float PASS; тест исключает только `event_id` PASS; seam auto и
ODS checks PASS; unknown donor error PASS; state v3 validation PASS.
- 21:45 Issue 09 task rerun: `APPROVED`. Findings: нет. Checks: all fields
except `event_id` PASS; browser/source + ODS device/os/geo seam check PASS;
`make generated-history-check` auto seam PASS; state v3 validation PASS.
Chain-level решение: открыть коммит-гейт 09.
- 21:46 Issue 09 commit gate: staged files в границах issue 09, журнал не
staged. Unit: PASS, 172 tests; `bash -n scripts/check_generated_analytics.sh`:
PASS; `git diff --cached --check`: PASS. Commit: `e2d0684`
(`fix(generator): сохранена фактура визита при восстановлении`).
- 21:47 Перед issue 10: процессный журнал issue 09 отделён docs-коммитом
`0f435d7`. Дерево чистое. Блокеров нет. Issue 10 запущен worker-у. Режим
ревью: обычный; worker reasoning_effort=`medium`; code review после
реализации — `high`. Требование: проверить Superset-визуализации через
Context7 и зафиксировать выбор.
- 21:52 Issue 10: worker вернул `DONE_WITH_CONCERNS`. `world_map` заменён на
`echarts_timeseries_bar` top-N стран; экспорт Superset, docs и урок 06
обновлены; контрактный тест добавлен. Context7 `/apache/superset`: `world_map`
legacy, ECharts bar поддерживает legend/tooltip/axis labels. Tests: PASS
dashboard config; `py_compile`: PASS; `jq`: PASS. Concern: скриншоты не
сняты, стенд остановлен.
- 21:52 Issue 10 code review запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 21:55 Issue 10 code review: `CHANGES_REQUESTED`. Findings: Superset export
`position_json` и `charts[].slice_name` не согласованы с `create_dashboard.py`;
тест проверяет только гео-чарт и может быть ложнозелёным; `previous_slice_names`
может оставить дубль старого и нового гео-чарта. Индекс возвращён worker-у.
- 21:58 Issue 10: worker вернул `DONE_WITH_CONCERNS` после ревью-находок.
Исправлено: export `position_json` и chart list синхронизированы с кодом,
добавлены тесты export/layout, rename удаляет старый дубль. Targeted tests:
PASS; `py_compile`: PASS; `jq`: PASS. Concern: визуальная приёмка не
выполнена, Superset не запущен.
- 21:58 Issue 10 code rerun запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 22:01 Issue 10 code rerun: `CHANGES_REQUESTED`. Finding:
`tests/test_superset_dashboard_config.py:1` — регрессионный тест лежит в
untracked `tests/`, рядом с `__pycache__`; тест должен попасть в изменение,
сгенерированный кэш — нет. Остальные проверки reviewer-а: export/layout,
chart list, rename dedupe, ECharts tooltip/legend/units, docs и неизменность
гео-фактуры — PASS. Индекс возвращён worker-у.
- 22:02 Issue 10: worker вернул `DONE_WITH_CONCERNS` после узкой находки.
`tests/__pycache__/` удалён; `git status --short --untracked-files=all tests`
показывает только `tests/test_superset_dashboard_config.py`. Targeted tests:
PASS; `py_compile`: PASS; `jq`: PASS. Concern: визуальная приёмка не
выполнена.
- 22:02 Issue 10 narrow code rerun запущен координатором: свежий
observer-субагент, reasoning_effort=`high`, без правок репозитория и среды.
- 22:05 Issue 10 narrow code rerun: `APPROVED`. Findings: нет. Checks:
статус `tests/` PASS; chart list/layout test content PASS; direct test
functions PASS. Chain-level решение: открыть коммит-гейт 10.
- 22:06 Issue 10 commit gate: staged files в границах issue 10, журнал не
staged. Dashboard config tests: PASS, 5 tests; `py_compile`: PASS; `jq`:
PASS; `git diff --cached --check`: PASS. Commit: `0c80e24`
(`fix(superset): заменена нечитаемая гео-карта`).
- 22:06 Перед issue 08: процессный журнал issue 10 отделён docs-коммитом
`de938ed`. Дерево чистое. Предпосылки 07, 12, 14, 09 выполнены. Issue 08
запущен worker-у. Режим ревью: обычный; worker reasoning_effort=`medium`;
code review после реализации — `high`. HITL-проход по урокам через пульт
ожидается как concern, если не выполнен фактически.
- 22:14 Issue 08: worker вернул `DONE_WITH_CONCERNS`. Курс и TEST_PLAN
переведены с архивного сида на стартовую историю; `data/*.jsonl` оставлены
как кладовка значений генератора; старые маркеры `make data`/`kafka_load`/
`LIMIT=`/фиксированные сид-ожидания не найдены. `git diff --check`: PASS.
Concern: нужен ручной проход уроков 00-06 через UI и оценка Superset/Grafana.
- 22:14 Issue 08 code review запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 22:17 Issue 08 code review: `CHANGES_REQUESTED`. Findings:
`docs/course/LESSON_STANDARD.md:24` — стандарт уроков всё ещё ведёт через
старый `make clean/up/ddl/data/transform` путь; `lessons/06_superset_bi.md:76`
говорит, что `generated-history-analytics` создаёт dashboard, но урок ниже
отдельно запускает `make superset-dashboard`. Индекс возвращён worker-у.
- 22:19 Issue 08: worker вернул `DONE` после ревью-находок. Исправлено:
старый путь отката в lesson standard заменён на
`make generated-history-analytics && make up`; урок 06 уточняет роль
`superset-init` и ручной `superset-dashboard`. Поиск старых маркеров: PASS;
`git diff --check`: PASS.
- 22:19 Issue 08 code rerun запущен координатором: свежий observer-субагент,
reasoning_effort=`high`, без правок репозитория и среды.
- 22:21 Issue 08 code rerun: `APPROVED`. Findings: нет. Checks: старые
команды и архивный сид как основной путь отсутствуют PASS; Superset запуск
согласован PASS; `data/*.jsonl` как кладовка значений PASS; TEST_PLAN и HITL
PASS. Chain-level решение: открыть коммит-гейт 08.
- 22:22 Issue 08 commit gate: staged files в границах issue 08, журнал не
staged. Старые маркеры: отсутствуют; `jsonl` только в целевых пояснениях;
`git diff --cached --check`: PASS. Commit: `d76c036`
(`docs(course): переведены уроки на стартовую историю`).
- 22:23 Финальное chain review запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды;
цель — проверить всю цепочку 12 -> 14 -> 09 -> 10 -> 08 против PRD и issue.
- 22:31 Финальное chain review: `CHANGES_REQUESTED`. Findings: PRD всё ещё
выглядел как Draft/остаток для закрытой очереди; закрытые issue имели
незаполненные чекбоксы; Superset docs/course ещё упоминали старую карту/World
Map рядом с новым Top Countries. Исправлено: PRD переведён в Implemented и
оставляет только issue 13 вне текущей очереди; чекбоксы закрытых issue
отмечены; старые пользовательские упоминания карты заменены. Reviewer
repro-поиски: PASS.
- 22:31 Финальное chain review rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:35 Финальное chain review rerun: `CHANGES_REQUESTED`. Finding:
`docs/SUPERSET_DASHBOARD.md:70` — рядом с Top Countries оставался старый
маркер `legacy world map`. Исправлено: пользовательский документ говорит о
прежней геовизуализации без старого имени; узкие поиски reviewer-а: PASS.
- 22:35 Финальное chain review second rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:39 Финальное chain review second rerun: `CHANGES_REQUESTED`. Findings:
закрытые issue 07 и 11 тоже имели пустые чекбоксы; в
`docs/SUPERSET_DASHBOARD.md` оставался маркер `world_map`. Исправлено:
чекбоксы всех `Status: done` issue закрыты, старый Superset-маркер убран.
Reviewer repro-поиски: PASS.
- 22:39 Финальное chain review third rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:42 Финальное chain review third rerun: `CHANGES_REQUESTED`. Finding:
`superset/create_dashboard.py:152` — в комментарии остался старый маркер
`Legacy world_map`. Исправлено нейтральным описанием прежней
геовизуализации. Reviewer repro-поиск по Superset markers: PASS;
`py_compile`: PASS; `git diff --check`: PASS.
- 22:42 Финальное chain review fourth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:54 Финальное chain review fourth rerun: `CHANGES_REQUESTED`. Findings:
`docs/OPERATIONS.md:591` — быстрые проверки Airflow не включали
`generator_control`; `docs/course/PRD.md:99` — режим курса всё ещё сводился к
`make up`. Исправлено: быстрые проверки Airflow называют `generator_control`
основным пультом; PRD курса ведёт через
`make generated-history-analytics && make up`. Минимальные проверки: PASS.
- 22:54 Финальное chain review fifth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 22:59 Финальное chain review fifth rerun: `CHANGES_REQUESTED`. Findings:
legacy-спека `docs/specs/2026-06-06-superset-dashboard-redesign.md` всё ещё
содержала старые Superset-маркеры `world_map`. Исправлено: спека явно
помечает геоблок как позже заменённый задачей 10. Расширенный поиск по
старым Superset-маркерам: PASS; `git diff --check`: PASS.
- 22:59 Финальное chain review sixth rerun запущено координатором: свежий
observer-субагент, reasoning_effort=`xhigh`, без правок репозитория и среды.
- 23:03 Финальное chain review sixth rerun: `CHANGES_REQUESTED`. Findings:
`docs/ARCHITECTURE.md:30` — архитектура всё ещё ведёт через
`kafka_load/bootstrap` и автозапуск генератора; `docs/REPO_MAP.md:7` — карта
репозитория не упоминает `generator_control_dag.py` в ручном/учебном пути;
`CONTEXT.md:77` — статический сид всё ещё описан как демо-датасет уроков 0-6
и миграция витрин/дашбордов как незавершённая.
- 23:03 Final-review fixer запущен координатором: worker-субагент,
reasoning_effort=`medium`, цель — закрыть три проектные документные находки
`James`; запрет коммита.
- 23:06 Final-review fixer вернул `DONE`. Исправлено: `docs/ARCHITECTURE.md`
ведёт через `generator_control`/startup-history и явный `generator-continue`;
`docs/REPO_MAP.md` упоминает `generator_control_dag.py`; `CONTEXT.md`
описывает `data/*.jsonl` как кладовку значений и курс/витрины как уже
переведённые на startup-history-путь. Fixer checks: PASS.
- 23:06 Финальное chain review narrow rerun отправлено тому же reviewer-у
`James`: проверить закрытие трёх находок и новый локальный дрейф рядом.
- 23:08 Финальное chain review narrow rerun: `APPROVED`. Checks:
`docs/ARCHITECTURE.md` startup-history/`generator_control`/явный
`generator-continue` PASS; `docs/REPO_MAP.md` Airflow path PASS;
`CONTEXT.md` кладовка `jsonl` и завершённая миграция курса PASS; старые
drift-маркеры рядом отсутствуют PASS. Chain-level решение: финальный гейт
закрыт, собрать handoff.
## 2026-07-05: coordinator-loop 15-18
- 19:59 Старт цепочки 15 -> 16 -> 17 -> 18. `git status --short`: clean.
Issue 13 исключён из текущей цепочки: `needs-triage`, заблокирован 15 и 17.
Issue 15 запущен worker-у. Режим ревью: гейт; worker
reasoning_effort=`high`; task review `high`, code review `high`.
- 20:09 Issue 15: worker вернул `DONE`. Индекс: state v2 теперь даёт
миграционный отказ при `GEN_STATE_RESET=false`; `down`/`clean` охватывают
compose-профиль `live-generator`; общая host/DAG-проверка чистого стенда
подключена к backfill/import/continue; docs описывают миграционный отказ.
Checks: `make generator-test` PASS; `docker compose config --quiet` PASS;
`py_compile` PASS; `bash -n` PASS; точечный `uv run pytest ...` FAIL
из-за отсутствия `pytest` в uv-окружении.
- 20:10 Issue 15 task review запущен координатором: свежий observer-субагент
`Ptolemy`, reasoning_effort=`high`, без правок репозитория и среды.
- 20:10 Issue 15 code review запущен координатором: свежий observer-субагент
`Linnaeus`, reasoning_effort=`high`, без правок репозитория и среды.
- 20:13 Issue 15 code review `Linnaeus`: `CHANGES_REQUESTED`. Findings:
host backfill/import guard не проверяет уже живой live-generator;
`generated-history-analytics` при clean start делает `down` без профиля
`live-generator`.
- 20:13 Issue 15 task review `Ptolemy`: `CHANGES_REQUESTED`. Finding:
`generator-continue` не защищён на грязном стенде без совместимого state.
Индексы ревью возвращены worker-у дословно.
- 20:17 Issue 15: worker вернул `DONE` после находок ревью. Исправлено:
отдельный guard для `continue`; host metrics check перед backfill/import;
`generated-history-analytics` clean start использует профиль `live-generator`.
Checks: `make generator-test` PASS; `docker compose config --quiet` PASS;
`py_compile` PASS; `bash -n` PASS. Отклонений и споров нет.
- 20:17 Issue 15 task narrow rerun отправлен `Ptolemy`: проверить закрытие
находки по `generator-continue`.
- 20:17 Issue 15 code narrow rerun отправлен `Linnaeus`: проверить закрытие
находок по host live-generator guard и `generated-history-analytics` clean.
- 20:19 Issue 15 task narrow rerun `Ptolemy`: `APPROVED`. Findings: нет.
- 20:19 Issue 15 code narrow rerun `Linnaeus`: `APPROVED`. Findings: нет.
Chain-level решение: открыть коммит-гейт 15.
- 20:20 Issue 15 commit gate: `make test` и `make lint` отсутствуют в
Makefile; фактический гейт: `make generator-test` PASS (181 tests);
`docker compose config --quiet` PASS; `py_compile` PASS; `bash -n` PASS;
`git diff --check` PASS. Diff в границах issue 15; журнал не staged.
- 20:20 Issue 15 рабочий коммит: `080d5ae`
(`fix(generator): исключено смешение миров при запуске`).
- 20:22 Перед issue 16: дерево clean; dependency issue 15 закрыта. Issue 16
запущен worker-у. Режим ревью: обычный; worker reasoning_effort=`medium`;
code review после реализации `high`. Условие эскалации: широкая переделка
DAG-зависимостей вместо предпроверки паузы переводит issue в гейт.
- 20:28 Issue 16: worker вернул `DONE_WITH_CONCERNS`. Индекс: README/course
называют `uv` и пререквизиты; quick start добавляет `make ddl`;
`make up` пересобирает Airflow; `generator_control` проверяет pause
`etl_pipeline` перед `TriggerDagRunOperator`; решение Airflow 2.10.5
проверено через Context7 и зафиксировано рядом с выбором API; runbook
предупреждает про дыру `daily-wave` x60. Checks: red phase PASS; generator
tests PASS (186); DAG contract PASS (16); compose config PASS. Concern:
полный clean-stand проход не выполнен.
- 20:28 Issue 16 code review запущен координатором: свежий observer-субагент
`Laplace`, reasoning_effort=`high`, без правок репозитория и среды.
- 20:30 Issue 16 code review `Laplace`: `CHANGES_REQUESTED`. Finding:
pause-check `etl_pipeline` стоит после `run_backfill`, поэтому отказ
происходит уже после записи истории и повторный backfill упирается в
clean-stand guard. Индекс возвращён worker-у дословно.
- 20:33 Обновлённый `coordinator-loop` перечитан по просьбе пользователя.
Процессный вывод по тестам уточнён: шумные команды коммит-гейта дальше
идут через файл-лог, в контекст координатора попадает только сводка.
- 20:33 Issue 16: worker вернул `DONE_WITH_CONCERNS` после code review.
Исправлено: paused-check вынесен перед `precheck`/`run` для backfill/import;
контрактный тест закрепляет порядок branch -> paused-check -> precheck ->
run -> trigger ETL. Checks: DAG contract PASS; generator tests PASS (186);
compose config PASS. Concern: полный Airflow UI clean-stand сценарий не
выполнен.
- 20:33 Issue 16 code narrow rerun отправлен `Laplace`: проверить закрытие
high-находки по порядку pause-check и отсутствие нового рядом.
- 20:34 Issue 16 code narrow rerun `Laplace`: `APPROVED`. Findings: нет.
Chain-level решение: открыть коммит-гейт 16.
- 20:38 Issue 16 commit gate: `uv run ... pytest` не дошёл до pytest из-за
`snap-confine`/`cap_dac_override` в окружении координатора; log analyst
подтвердил ошибку запуска, не тестов. `make generator-test` через Docker:
PASS (186 tests) после запуска вне sandbox. `docker compose config --quiet`
PASS; `py_compile` PASS; `git diff --check` PASS. Clean-stand probe:
`make clean` PASS, `make up` FAIL на `superset-init`; log analyst finding:
`superset-init` падает с `NoSuchTableError: dm.v_events_enriched` до шага
`make ddl`. Finding возвращён worker-у как failed commit gate.
- 20:42 Issue 16: worker вернул `DONE_WITH_CONCERNS` после failed commit
gate. Исправлено: `make up` поднимает базовый набор без `superset`/
`superset-init`, quick start доходит до `make ddl`; README/OPERATIONS
фиксируют, что Superset-дэшборд собирается позже, когда DM готов; контрактный
тест ловит порядок. Checks worker-а: DAG contract PASS; compose config PASS;
`py_compile` PASS; `git diff --check` PASS; `make generator-test` PASS
(186); `make up` PASS; `make ddl` PASS. Concern: базовые контейнеры
остались запущены после проверки.
- 20:42 Issue 16 code narrow rerun отправлен `Laplace`: проверить новый
контракт `make up` без раннего `superset-init` и соседний quick start drift.
- 20:44 Issue 16 code narrow rerun `Laplace`: `APPROVED`. Findings: нет.
Chain-level решение: повторить коммит-гейт 16 после commit-gate фикса.
- 20:46 Issue 16 final commit gate: `make generator-test` PASS (186 tests);
`docker compose config --quiet` PASS; `py_compile` PASS; `git diff --check`
PASS; `make clean` PASS; `make up` PASS; `make ddl` PASS. Diff в границах
issue 16; журнал не staged. Стенд после проверки поднят в базовом состоянии
с применённым DDL.
- 20:46 Issue 16 рабочий коммит: `27947ce`
(`fix(airflow): исправлен запуск пульта на свежем стенде`).
- 20:47 Перед issue 17: дерево clean; issue 15 и 16 закрыты. Issue 17
запущен worker-у. Режим ревью: обычный; worker reasoning_effort=`medium`;
code review после реализации `high`.
- 20:54 Issue 17: worker вернул `DONE_WITH_CONCERNS`. Индекс:
`check_generated_analytics.sh` берёт `model_t0/model_t_end/profile` из
фактического Kafka manifest, ждёт live-строки перед seam-check и требует
ненулевые ODS device/geo; `generated-history-check` пробрасывает PROFILE;
добавлен CLI `kafka-manifest-summary`; Superset tests сравнивают полный
существенный `params`; top countries сортируется по `Events, pcs`; sync
удаляет дубли только в целевом датасете; деструктивность sync задокументирована.
Checks: generated analytics contract PASS; Superset config PASS; startup
history artifact PASS; `py_compile` PASS; `bash -n` PASS; `git diff --check`
PASS. Concern: полный `PROFILE=daily-wave CHECK_LIVE_SEAM=1 make
generated-history-check` не выполнен.
- 20:55 Issue 17 code review запущен координатором: свежий observer-субагент
`Bacon`, reasoning_effort=`high`, без правок репозитория и среды.
- 20:58 Issue 17 code review `Bacon`: `CHANGES_REQUESTED`. Findings:
Superset sync всё ещё может перезаписать/удалить одноимённый чарт вне
целевого dashboard при том же датасете; `CHECK_LIVE_SEAM=auto` может зелёно
пропустить попытку live без строк после стыка. Индекс возвращён worker-у
дословно.
- 21:02 Issue 17: worker вернул `DONE_WITH_CONCERNS` после code review.
Исправлено: Superset sync/delete ограничены чартами целевого dashboard;
обычный `generated-history-check` требует live-строки, а backfill-only путь
явно отключает seam-check. Checks: generated analytics + Superset tests PASS;
startup history artifact PASS; `bash -n` PASS; `py_compile` PASS;
`git diff --check` PASS. Отклонений и споров нет; полный runtime-прогон
ещё не выполнен.
- 21:02 Issue 17 code narrow rerun отправлен `Bacon`: проверить закрытие
находок по Superset sync ownership и live seam default.
- 21:03 Issue 17 code narrow rerun `Bacon`: `APPROVED`. Findings: нет.
Chain-level решение: открыть коммит-гейт 17.
- 21:19 Issue 17 commit gate partial: локальный `uv` не дошёл до pytest из-за
`snap-confine`/`cap_dac_override`; Docker target tests PASS (12);
`make generator-test` PASS (186); `py_compile` PASS; `bash -n` PASS;
`git diff --check` PASS. Runtime prep `PROFILE=daily-wave make
generated-history-analytics` был остановлен после долгого выполнения:
generator-run и ClickHouse были активны, но gate не завершался за разумное
время; после SIGINT running services только `clickhouse`, `kafka`. Finding
возвращён worker-у: нужен точный bounded runtime-путь или изменение проверки.
- 21:27 Issue 17: worker вернул `DONE` после runtime gate finding.
Добавлен bounded `make generated-history-runtime-check`: короткий daily-wave
backfill 1h, live через Compose env, ожидание STG growth до 25s, второй batch
и `CHECK_LIVE_SEAM=1 REQUIRE_SUPERSET=0`. Контрактный тест и OPERATIONS
обновлены. Checks worker-а: Docker target tests PASS (13); `bash -n` PASS;
`py_compile` PASS; `git diff --check` PASS; `make
generated-history-runtime-check` PASS. Ключевые факты runtime:
profile=`daily-wave`, `model_t_end=2026-01-01T01:00:00+00:00`,
STG 10256 -> 10975, crossing visits 19, ODS device/geo 19/19. Concern:
runtime gate чистит volumes и оставляет `clickhouse`, `kafka` running.
- 21:28 Issue 17 code narrow rerun отправлен `Bacon`: проверить bounded
runtime gate и отсутствие нового рядом с уже одобренными исправлениями.
- 21:29 Issue 17 code narrow rerun `Bacon`: `APPROVED`. Findings: нет.
Chain-level решение: повторить коммит-гейт 17 с bounded runtime gate.
- 21:32 Issue 17 final commit gate: Docker target tests PASS (13);
`make generator-test` PASS (186); `py_compile` PASS; `bash -n` PASS;
`git diff --check` PASS; `make generated-history-runtime-check` PASS.
Runtime facts: STG 10256 -> 10862, `manifest_launch_profile=daily-wave`,
`manifest_model_t_end=2026-01-01T01:00:00+00:00`, crossing visits 19,
ODS device/geo 19/19. Diff в границах issue 17; журнал не staged. Runtime
gate оставил `clickhouse`, `kafka` running.
- 21:32 Issue 17 рабочий коммит: `00c97eb`
(`test(generator): усилены проверки startup-history и Superset`).
- 21:33 Перед issue 18: дерево clean; dependencies 15, 16, 17 закрыты. Issue
18 запущен worker-у. Режим ревью: обычный; worker reasoning_effort=`medium`;
code review/docs review после реализации `high`. Примечание: runtime gate 17
оставил `clickhouse`, `kafka` running; worker должен сам очистить или
подготовить стенд перед проверкой курса.
- 21:51 Issue 18: worker вернул `DONE_WITH_CONCERNS`. Индекс: урок 00
объясняет служебные топики генератора; урок 01 переводит перезаливку
`kafka_msg_ts` на `CLEAN_START=0`; урок 05 согласован с no-live default и
consumer lag; course README добавляет HITL-пункт чистого маршрута 00/01/05;
OPERATIONS ведёт STG-путь через startup-history, `make data` архивный.
Checks worker-а: `make generated-history-analytics` PASS на чистом стенде;
`make up` PASS; SQL-проверка `kafka_msg_ts` + `CLEAN_START=0` PASS
(16054/16054); восстановление схемы PASS; Kafka topics PASS; Prometheus
targets PASS (`generator=0` no-live); `system.kafka_consumers` PASS;
`git diff --check` PASS. Concern: стенд после проверки не свежий из-за
намеренного `CLEAN_START=0`.
- 21:52 Issue 18 code/docs review запущен координатором: свежий
observer-субагент `Hume`, reasoning_effort=`high`, без правок репозитория и
среды.
- 21:53 Issue 18 code/docs review `Hume`: `APPROVED` с medium finding:
урок 05 говорит о трёх Prometheus scrape-блоках, но в `configs/prometheus.yml`
их четыре, включая `generator`; находка возвращена worker-у для короткого
триажа перед коммитом.
- 21:54 Issue 18: worker вернул `DONE` после medium finding. Исправлено:
урок 05 говорит о четырёх scrape-блоках и связывает `generator:9109` с
ожидаемым `DOWN` без live. Checks: `rg` PASS; `git diff --check` по уроку
PASS.
- 21:55 Issue 18 docs narrow rerun отправлен `Hume`: проверить закрытие
medium-находки по Prometheus scrape-блокам.
- 21:55 Issue 18 docs narrow rerun `Hume`: `APPROVED`. Findings: нет.
Chain-level решение: открыть коммит-гейт 18.
- 22:00 Issue 18 commit gate: doc `rg` PASS; `git diff --check` PASS;
`make generated-history-analytics` PASS (manifest profile `ci`,
T_end=`2026-01-01T06:00:00+00:00`, 16054 events); `make up` PASS. Diff в
границах issue 18; журнал не staged.
- 22:00 Issue 18 рабочий коммит: `77e4fb6`
(`docs(course): согласованы уроки со startup-history`).
- 22:02 Перед финальным review: добавлен follow-up issue 19 про единые
`make test`/`make lint`; PRD обновлён: 15-18 закрыты, открытыми остаются
13 (`needs-triage`) и 19 (`ready-for-agent`).
- 22:04 Финальное chain review запущено координатором: свежий
observer-субагент `Gibbs`, reasoning_effort=`xhigh`, без правок репозитория
и среды; цель — проверить цепочку 15-18 и PRD/трекер против замысла.
- 22:08 Финальное chain review `Gibbs`: `CHANGES_REQUESTED`. Finding:
docs still recommend plain `make generated-history-check` after backfill-only
`make generated-history-analytics`, but issue 17 changed default check to
require live seam. Affected docs: OPERATIONS, README, startup-history runbook.
Fixer-субагент запускается для проектных документов.
- 22:10 Final-review fixer `Peirce`: исправил README, OPERATIONS и
startup-history runbook: backfill/import-only пути используют
`CHECK_LIVE_SEAM=0`, live-проверка вынесена в
`make generated-history-runtime-check` или описана после `generator-continue`
+ `make transform`. Checks: targeted `rg` PASS; `git diff --check` PASS.
- 22:10 Финальное chain review narrow rerun отправлено `Gibbs`: проверить
закрытие finding по `generated-history-check` docs drift и новый локальный
drift рядом.
- 22:11 Финальное chain review narrow rerun `Gibbs`: `APPROVED`. Findings:
нет. Chain-level решение: финальный review закрыт, собрать handoff.
- 22:12 Handoff для кросс-линейного ревью собран:
`.scratch/handoffs/20260705-2212-generator-cross-review-fixes.md`.
@@ -1,236 +0,0 @@
# Находки ручной 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 ~3060 МБ.
- **Следствие про профиль:** на `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 дней).
@@ -1,65 +0,0 @@
Status: done
# Контракт модельного времени и стартовой истории
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Зафиксировать рабочий контракт для реализации модельного времени и стартовой
истории. Это HITL-срез: до кода нужно решить внешний интерфейс, формат
стартовой истории и правила восстановления, чтобы worker-и не угадывали
поведение через тесты.
Контракт должен остаться коротким и понятным: что задаёт `T0`, как задаётся ×K,
что такое `T_end`, как выглядит стартовая история, какие метки времени
модельные, а какие операционные.
## Acceptance criteria
- [x] Выбраны имена и формат настроек для `T0`, скорости ×K и режима промотки
прошлого.
- [x] Описан живой ход часов: модельное время идёт фиксированным шагом
`tick_seconds * K`; измеренный настенный интервал между тиками не входит в
текущий контракт.
- [x] Разделены уровни повторяемости: точная повторяемость промотки прошлого и
точная повторяемость живого режима при тех же настройках и числе успешных
тиков.
- [x] Описано, как после сбоя вычисляется модельная точка возобновления при ×K:
из сохранённой модельной метки, сохранённой настенной метки и скорости.
- [x] Описано, что короткий и долгий простой считаются по модельному времени:
при большом ×K короткий настенный простой может стать долгим модельным.
- [x] Описан манифест стартовой истории: минимум `GEN_SEED`, `T0`, `T_end`,
настройки генерации, версия state и контрольные числа.
- [x] Описана граница `T_end`: где заканчивается прошлое и с какой метки
начинается живое продолжение, без дублей и дыр.
- [x] Разделены модельные метки событий и настенные операционные метки сервиса.
- [x] Зафиксирован часовой пояс модельных часов для дневного коэффициента.
- [x] Решено, как координатор будет получать повторяемую ClickHouse-проверку:
через очистку данных или идемпотентный прогон.
- [x] Если выбран чистый прогон, перечислены поверхности сброса: таблицы
ClickHouse, Kafka-топики данных и состояние генератора (`generator_state` или
явный сброс состояния при старте).
- [x] Контракт записан в durable-документ: обновление `PRD.md`, короткий
design note в `.scratch/generator-model-time-startup-history/` или уточнение
спеки. Сам issue 01 только ссылается на источник истины.
## Решение
Контракт записан в durable-документ:
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`, раздел
«Рабочий контракт реализации».
PRD ссылается на этот раздел в сквозных инвариантах. Для следующих задач важная
граница: стартовая история покрывает `[T0, T_end)`, а живое продолжение стартует
из слепка на `T_end`.
Дополнительную матрицу «фиксированный шаг / измеренная настенная дельта» не
вводим: ADR-0005 не требовала такого публичного выбора, а текущей учебной
цепочке нужен один повторяемый живой ход часов.
## Blocked by
None - can start immediately
@@ -1,51 +0,0 @@
Status: done
# Модельное время до ClickHouse
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Сделать минимальный живой поток, где генератор стартует от `T0`, пишет
`event_timestamp` по модельному времени и доводит события до ClickHouse
обычным путём стенда. Срез должен доказать не внутренний класс часов, а
наблюдаемое поведение: данные в ClickHouse начинаются от модельной точки и
повторяются при тех же настройках.
## Acceptance criteria
- [x] При фиксированных `GEN_SEED` и `T0` первый короткий прогон пишет события с
модельными `event_timestamp`, начинающимися около `T0`.
- [x] Повторный чистый прогон с теми же настройками даёт те же контрольные
числа в ClickHouse по правилу повторяемости из контракта задачи 1.
- [x] Повторный чистый прогон явно сбрасывает или обходит старое состояние
генератора, чтобы сервис не продолжил прошлый запуск из `generator_state`.
- [x] Расчёт дневного коэффициента в этом срезе больше не зависит от реального
часа запуска процесса.
- [x] Локальные тесты проверяют поведение через публичный интерфейс генератора
или сервиса, без привязки к внутреннему устройству часов.
- [x] Есть команда или короткая инструкция для координатора: поднять стенд,
прогнать поток, выполнить SQL-проверку в ClickHouse.
- [x] Документация запуска не утверждает, что `event_timestamp` равен
настенному времени.
## Решение
Живой сервис получает модельную точку из `GEN_MODEL_T0`, считает дневной
коэффициент по модельному времени и передаёт эту же точку в тиковый поток. После
успешного тика модельная точка сдвигается на
`GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`.
Проверка ClickHouse выполнена двумя чистыми прогонами с `GEN_STATE_RESET=true`.
Оба раза получены одинаковые контрольные числа: 5 событий, диапазон
`event_ts = 2026-01-01 10:00:00.000000`, 5 уникальных `event_id` и 5 уникальных
`click_id`.
Риски по восстановлению state v2 оставлены для задачи 04: текущий срез
доказывает чистый live-старт, а не возобновление после сбоя.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md`
@@ -1,86 +0,0 @@
Status: done
# ×K и дневной коэффициент по модельному времени
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Добавить ускоренный живой ход модельных часов. При ×K за один реальный тик
должна проходить большая модельная длительность, а событийный бюджет должен
считаться по этой модельной длительности. Дневной коэффициент считается по
модельному часу.
Результат должен быть виден в ClickHouse: модельные метки уходят вперёд быстрее
реального времени, а объём событий соответствует пройденному модельному
интервалу.
## Acceptance criteria
- [x] На ×1 поведение остаётся совместимым с обычным живым режимом.
- [x] На ×K модельные `event_timestamp` за короткий реальный прогон покрывают
примерно `K` раз большую модельную длительность.
- [x] Событийный бюджет считается по модельной длительности тика, а не по
реальному времени сна процесса.
- [x] Дневной коэффициент меняется при переходе модельного времени через
дневные/ночные часы в зафиксированном часовом поясе, независимо от реального
часа запуска.
- [x] ClickHouse-проверка показывает повторяемые контрольные числа при тех же
`GEN_SEED`, `T0`, скорости и настройках.
- [x] Worker даёт промежуточный статус, если статистический прогон или стендовая
проверка занимает заметное время.
- [x] В `generator/README.md` или `docs/OPERATIONS.md` кратко описано, что ×K
ускоряет именно модельное время стенда.
## Решение
`calculate_events_count()` теперь считает λ тика по модельной длительности:
`GEN_TICK_SECONDS * GEN_MODEL_TIME_SPEED`. При `GEN_MODEL_TIME_SPEED=1`
формула остаётся прежней.
Для больших λ старый Knuth-алгоритм заменён нормальным приближением, чтобы
ускорение ×K не упиралось в underflow `exp(-λ)` и событийный бюджет продолжал
расти вместе с модельной длительностью.
Добавлены проверки:
- средний бюджет ×10 примерно в 10 раз больше бюджета ×1;
- большой λ не залипает на старом underflow-ограничении;
- `hour_factor()` берёт час из заданного `GEN_MODEL_TIMEZONE`;
- сервисный live-тик при ×10 сдвигает `event_timestamp` с `10:00` на `10:10`;
- старый live-сценарий ×1 остаётся зелёным.
Документация в `generator/README.md` и `docs/OPERATIONS.md` уточняет, что ×K
ускоряет модельную длительность тика и событийный бюджет.
## Проверка
- `make generator-test``120 passed`.
- ClickHouse, быстрый прогон ×60 с `GEN_STATE_RESET=true`,
`GEN_MODEL_T0=2026-01-01T05:58:00+00:00`, `GEN_TICK_SECONDS=1`: за короткий
реальный прогон `event_ts` покрыл `2026-01-01 05:58:00`
`2026-01-01 06:09:00`; после 06:00 UTC поминутные числа выросли с `40/42` до
примерно `58..63`, что показывает смену ночного коэффициента.
- ClickHouse, повторяемость: два чистых запуска с одинаковыми
`GEN_SEED=4242`, `GEN_MODEL_T0=2026-01-01T10:00:00+00:00`,
`GEN_MODEL_TIME_SPEED=60`, `GEN_TICK_SECONDS=60`,
`GEN_STATE_RESET=true` дали одинаковые контрольные числа:
`events=73`, `unique_events=73`, `unique_clicks=73`,
`min_event_ts=max_event_ts=2026-01-01 10:00:00`.
## Риски и границы
- State v2 resume, backfill, manifest и startup history не менялись. Известные
риски по ним остаются задачами 04/05.
- Сервис всё ещё пишет операционные метки state и истории пачек по настенному
времени; это соответствует текущему контракту issue 03.
- Нормальное приближение для больших λ не является точным Poisson, но для
событийного бюджета учебного демо достаточно сохраняет масштаб, среднее и
дисперсию. Точную статистическую выборку можно выделить в отдельную задачу,
если она станет учебной целью.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/02-model-time-to-clickhouse.md`
@@ -1,114 +0,0 @@
Status: done
# Восстановление state v2 от модельной точки
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Привести восстановление state v2 к модельному времени. Один и тот же слепок
должен уметь восстанавливаться в двух разных случаях: после сбоя, где время
действительно прошло, и при старте из стартовой истории, где продолжение идёт
от `T_end` без искусственного разрыва.
Срез должен доказать поведение не только локальными тестами состояния, но и
данными в ClickHouse: активные визиты продолжаются или закрываются по правилам
модельного времени.
## Acceptance criteria
- [x] State сохраняет достаточно данных, чтобы после сбоя вычислить модельную
точку возобновления при ×K.
- [x] Восстановление после короткого модельного сбоя продолжает активные визиты
и досылает созревшие события с исходными модельными метками.
- [x] Восстановление после долгого модельного сбоя закрывает сильно просроченные
активные визиты без досылки остатка.
- [x] Восстановление из стартовой истории использует `T_end` как точку
возобновления и не обрывает активные визиты из-за настенного простоя.
- [x] Визит, переживший восстановление, остаётся однородным: события до и после
восстановления не меняют контекст визита и не создают второй путь генерации
внутри одного `click_id`.
- [x] Повреждённый или несовместимый state не валит сервис: генератор стартует
с чистого листа и пишет предупреждение.
- [x] ClickHouse-проверка подтверждает, что на стыке восстановления нет дублей
событий и нет разрыва `click_id` внутри продолжающегося визита.
- [x] После реализации выполнено саморевью worker-а и отдельное reviewer-ревью,
потому что задача меняет state/serialization и сервисное восстановление.
- [x] Если исправление после ревью меняет формат state или способ восстановления,
выполнен повторный reviewer-круг.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/03-model-speed-and-day-factor.md`
- Review gate из `PRD.md`: сквозной инвариант времени после задачи 3
## Решение
- State v2 теперь хранит `model_timestamp`, `wall_timestamp`,
`model_time_speed`, `model_timezone`, `model_t0` и `gen_seed`.
- Старый v2 без этих полей считается несовместимым: `from_dict_safe` возвращает
`None`, сервис пишет предупреждение и стартует с чистого состояния.
- Live-восстановление считает точку продолжения по формуле
`model_timestamp + max(0, wall_now_utc - wall_timestamp) * model_time_speed`.
- Перед live-восстановлением сервис сверяет state с текущим `Config` по
`gen_seed`, `model_t0`, `model_timezone` и `model_time_speed`. Несовпадение
считается несовместимым state: сервис пишет warning и стартует с чистого
состояния.
- Восстановление из стартовой истории вынесено в минимальный метод
`GeneratorService.restore_from_startup_history(state, model_t_end)`: он берёт
переданный `T_end` напрямую и не применяет wall-дельту.
- Сохранение state после успешного тика пишет модельную точку следующего тика,
а не настенное время. Это убирает повторный старт с уже обработанной модельной
точки.
## Проверки
- Точечный прогон после проверки reviewer-а:
`docker run --rm -v ... generator:test pytest tests/test_state.py::TestGeneratorState tests/test_state.py::TestGeneratorStateValidation::test_from_dict_safe_returns_state_on_valid tests/test_state.py::TestGeneratorStateValidation::test_from_dict_safe_returns_none_on_invalid_gen_seed tests/test_service.py::TestGeneratorServiceStateV2 -v`
— 15 passed.
- `make generator-test` — 126 passed.
- `uv run pytest ...` и `uv run python -m pytest ...` не запускались до тестов:
в текущем uv-окружении нет `pytest`.
- `make lint` недоступен: в `Makefile` нет такой цели.
## ClickHouse-проверка
Выполнены два чистых стендовых сценария. Оба начинались со сброса ClickHouse,
Kafka и state генератора.
1. Короткое восстановление, `GEN_MODEL_TIME_SPEED=1`.
Первый запуск с `GEN_STATE_RESET=true` записал один тик, второй запуск с
`GEN_STATE_RESET=false` восстановился из state и продолжил активные визиты.
Проверка ClickHouse:
- `dds.event`: `rows=332`, `uniq_events=332`, `duplicate_events=0`.
- `continued_from_first_tick=67` для визитов, начатых на
`2026-01-01 10:00:00`.
- Для этих `click_id` нет смены `user_domain_id`, `device_type`, `os_name`,
`geo_country`, `geo_region_name` и `geo_timezone`.
2. Долгое восстановление, `GEN_MODEL_TIME_SPEED=3600`.
Первый запуск с `GEN_STATE_RESET=true` записал один тик на
`2026-01-01 10:00:00`, второй запуск с `GEN_STATE_RESET=false`
восстановился на модельной точке `2026-01-03T22:51:51.552000+00:00`.
Проверка ClickHouse:
- `dds.event`: `rows=200`, `uniq_events=200`, `duplicate_events=0`.
- диапазон `event_ts`: от `2026-01-01 10:00:00.000000` до
`2026-01-03 22:51:51.552000`.
- `continued_old_clicks=0`: старые `click_id` с первого тика не продолжились
после долгой модельной паузы.
## Риски
- Старые записи state v2 без модельной связки больше не восстанавливаются. Это
намеренно: без этих полей нельзя корректно посчитать точку продолжения.
- Полноценный манифест и backfill не реализованы: добавлен только минимальный
интерфейс восстановления от переданного `T_end`.
- `restore_from_startup_history` пока принимает state и `T_end` напрямую, без
проверки manifest. Это риск issue 05, где появится полноценная стартовая
история и запрет смешивания артефактов.
- `GeneratorState.__post_init__` оставляет in-memory совместимость для прямого
создания state в тестах и коде. JSON/Kafka путь остаётся строгим: отсутствие
новых полей не маскируется и ведёт к fresh start.
- После reviewer-fix формат state снова изменился (`gen_seed`). Повторный
reviewer-круг выполнен, новых блокирующих находок нет.
@@ -1,218 +0,0 @@
Status: done
# Стартовая история до ClickHouse
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Сделать промотку прошлого: генератор быстро проходит от `T0` до `T_end`, создаёт
события за этот отрезок, сохраняет слепок состояния и манифест стартовой
истории. Эту историю нужно загрузить в ClickHouse и доказать, что живой поток
может продолжить её с `T_end`.
Это главный срез стартовой истории. Он должен проверять не только факт наличия
данных, но и форму данных: пирамиду, возвраты, длину визита, воронку и стык
между прошлым и живым продолжением.
## Acceptance criteria
- [x] Промотка прошлого создаёт события за `[T0, T_end)`, слепок состояния и
манифест с контрольными данными.
- [x] При одинаковых `GEN_SEED`, `T0`, `T_end` и настройках артефакт промотки
прошлого повторяем точно; проверки в ClickHouse следуют правилам допуска из
контракта задачи 1.
- [x] История загружается в ClickHouse штатной или явно описанной командой.
- [x] SQL-проверка показывает здоровую пирамиду: пользователей меньше, чем
визитов, визитов меньше, чем событий.
- [x] SQL-проверка показывает возвраты: у части пользователей больше одного
визита.
- [x] SQL-проверка длины визита проверяет форму, а не только среднее: долю
коротких визитов, медиану и долю срезов о потолок.
- [x] SQL-проверка воронки `/home -> товары -> /cart -> /payment ->
/confirmation` монотонно убывает, а доля дошедших до `/confirmation` в
согласованном коридоре.
- [x] Живое продолжение после `T_end` не создаёт дублей на границе и не выглядит
как независимый второй мир.
- [x] Визиты, которые переходят через `T_end`, остаются однородными: контекст
визита не меняется на стыке стартовой истории и живого продолжения.
- [x] Подготовлены данные, команды и SQL-проверки, достаточные для внешнего
review gate по распределениям и двум путям генерации из `PRD.md`.
- [x] Worker даёт промежуточный статус, если промотка, загрузка в ClickHouse или
распределительные проверки занимают заметное время.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/04-state-v2-model-resume.md`
## Фактический прогон worker-а
Дата проверки: 2026-06-14.
Настройки стенда:
- `GEN_SEED=4242`
- `GEN_MODEL_T0=2026-01-01T00:00:00+00:00`
- `GEN_MODEL_T_END=2026-01-01T06:00:00+00:00`
- `GEN_MODEL_TIMEZONE=UTC`
- `GEN_MODEL_TIME_SPEED=1`
- `GEN_TICK_SECONDS=60`
- `GEN_LAMBDA_BASE_PER_MIN=120`
- `GEN_JITTER_PCT=0`
- `GEN_MIN_EVENTS_PER_TICK=1`
- `GEN_MAX_EVENTS_PER_TICK=1000`
Команды проверки:
```bash
make clean
docker compose up -d clickhouse kafka
make ddl
docker compose build generator
docker compose run --rm --no-deps \
-e GEN_RUN_MODE=backfill \
-e GEN_STATE_RESET=true \
-e GEN_SEED=4242 \
-e GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
-e GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
-e GEN_MODEL_TIMEZONE=UTC \
-e GEN_MODEL_TIME_SPEED=1 \
-e GEN_TICK_SECONDS=60 \
-e GEN_LAMBDA_BASE_PER_MIN=120 \
-e GEN_JITTER_PCT=0 \
-e GEN_MIN_EVENTS_PER_TICK=1 \
-e GEN_MAX_EVENTS_PER_TICK=1000 \
generator
sleep 10
bash scripts/run_batch.sh
```
Повторяемость проверена двумя чистыми прогонами с полным сбросом
ClickHouse/Kafka/state через `make clean`. В обоих прогонах manifest и ClickHouse
дали одинаковые контрольные числа:
- manifest `browser_events.checksum_sha256`:
`3b1946bd7fd3d669e2461fcbae88d8aed6980bb5f126779fb4e7480ce15a3269`
- manifest `location_events.checksum_sha256`:
`370816c8d0b0311592282b9b75372e18862db9ef4984f7032885e935f9436522`
- manifest `device_events.checksum_sha256`:
`37e157ac7d5a3203c1c27b2a11abc1b9cb9fc7f74f79c77645faa09412e08b33`
- manifest `geo_events.checksum_sha256`:
`ec0e36e4613e05b9a5e72490c7565cf7e5f767d39a7a58b383e56e2eb191fc20`
- ClickHouse digest:
`CC1A73E65E897D1F1FC982CFA4237A07`
Backfill в ClickHouse:
- `events=31825`
- `unique_events=31825`
- `visits=3020`
- `users=1048`
- `min_event_ts=2026-01-01 00:00:00.000000`
- `max_event_ts=2026-01-01 05:59:59.521599`
- `pyramid_ok=1`
- `half_open_ok=1`
Проверка возвратов:
- `users=1048`
- `returning_users=708`
- `returning_share=0.6755725190839694`
- `max_visits_per_user=11`
Форма длины визита:
- `visits=3020`
- `short_visit_share=0.16490066225165562`
- `median_events_per_visit=8`
- `avg_events_per_visit=10.538079470198676`
- `capped_visit_share=0.0619205298013245`
- `median_duration_sec=202`
- `p95_duration_sec=823`
- `max_events_per_visit=30`
Воронка:
- строгая упорядоченная:
`home=2743`, `products=1626`, `cart=995`, `payment=673`,
`confirmation=394`, `monotonic_ok=1`,
`ordered_confirmation_share=0.14363835216915785`;
- калибровочная по наличию страницы в визите:
`home=2743`, `products=2706`, `cart=2003`, `payment=1368`,
`confirmation=824`, `monotonic_ok=1`,
`confirmation_share_all_visits=0.2728476821192053`,
`confirmation_share_from_home=0.3004010207801677`.
Live-продолжение:
```bash
docker compose run -d --name issue05-live --no-deps \
-e GEN_RUN_MODE=live \
-e GEN_STATE_RESET=false \
-e GEN_SEED=4242 \
-e GEN_MODEL_T0=2026-01-01T00:00:00+00:00 \
-e GEN_MODEL_T_END=2026-01-01T06:00:00+00:00 \
-e GEN_MODEL_TIMEZONE=UTC \
-e GEN_MODEL_TIME_SPEED=1 \
-e GEN_TICK_SECONDS=60 \
-e GEN_LAMBDA_BASE_PER_MIN=120 \
-e GEN_JITTER_PCT=0 \
-e GEN_MIN_EVENTS_PER_TICK=1 \
-e GEN_MAX_EVENTS_PER_TICK=1000 \
generator
```
Лог live-запуска подтвердил восстановление из стартовой истории:
`model_time=2026-01-01T06:00:00+00:00`, затем прошли тики `361`-`364`.
После live и повторного `bash scripts/run_batch.sh`:
- `users=1079`
- `visits=3069`
- `events=32145`
- `min_event_ts=2026-01-01 00:00:00.000000`
- `max_event_ts=2026-01-01 06:03:00.000000`
- `history_events=31825`
- `live_events=320`
- `duplicate_events=0`
- `boundary_events=13`
`boundary_events=13` — это live-события ровно на `T_end`; они не входили в
backfill `[T0, T_end)`.
Визиты через `T_end`:
- `crossing_visits=36`
- `visits_with_both_sides=36`
- `homogeneous_visits=36`
- `context_ok=1`
- `min_before_events=2`
- `min_after_events=1`
- `max_after_events=10`
## Риски для review gate
- Manifest, state и события связаны метаданными (`GEN_SEED`, `T0`, `T_end`,
настройки генерации, `last_batch_id`) и checksum событий по топикам. Полной
криптографической связки `state + events + manifest` пока нет.
- Для воронки есть две SQL-формы. Строгая ordered-форма проверяет порядок
`/home -> товары -> /cart -> /payment -> /confirmation`. Калибровочная
contains-форма проверяет, была ли страница в визите. Для review gate
`confirmation_share` сравнивается с коридором мат-модели по contains-форме.
## Review gate
- Саморевью worker-а нашло блокирующий риск: backfill не должен сохранять
state/manifest при частичной публикации. Исправлено fail-fast поведением и
тестом.
- Первый reviewer gate нашёл два блокера: startup-history state мог остаться без
manifest и восстановиться как live-state, а startup-history restore не сверял
поля самого state. Оба пункта исправлены до коммита.
- Повторный reviewer gate: `gate pass`, новых блокирующих находок нет.
- Проверки после исправлений: `make generator-test` — 134 passed.
- Остаточный риск процесса: reviewer был со свежим контекстом, но той же
родословной, а не внешней моделью.
@@ -1,227 +0,0 @@
Status: done
# Стартовая история как источник аналитики
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Перевести штатный путь стенда на стартовую историю как источник аналитики.
Чистый стенд должен получать данные из генерации: стартовая история загружается,
STG→ODS→DDS→DM строится на ней, а Superset работает с этими витринами. Архивный
статический сид остаётся только временной кладовкой фактуры для генератора, а не
источником аналитического контура.
Срез не требует финальной ручной оценки красоты дашбордов, но должен дать
техническое доказательство: витрины и датасеты не пустые, контрольные числа
берутся из генерации.
Границы среза: штатный путь стенда и документы запуска. Переписывание уроков,
новые панели и улучшение формы дашбордов остаются follow-up, если по ходу не
окажутся маленькой обязательной правкой для запуска.
## Acceptance criteria
- [x] Штатная команда запуска чистого стенда создаёт или загружает стартовую
историю и прогоняет её до DM-витрин.
- [x] `kafka_load_dag` или заменяющий его путь больше не использует архивный
`data/*.jsonl` как источник аналитики.
- [x] ClickHouse-проверки из задачи 5 доступны как повторяемая команда для
координатора или CI.
- [x] Основные DM-витрины, на которых стоят дашборды, непустые и показывают
данные генерации.
- [x] Superset-датасеты и дашборды технически открываются на данных генерации;
ручная оценка формы графиков остаётся финальной HITL-приёмкой.
- [x] `README.md`, `docs/OPERATIONS.md` и `generator/README.md` больше не
описывают архивный сид как основной источник аналитики.
- [x] Если уроки или дашборды требуют нетривиальной переделки, создан follow-up
issue вместо расширения этой задачи.
- [x] Если учебные материалы ещё описывают архивный сид как основной источник,
создан отдельный follow-up на миграцию уроков по ADR-0006.
- [x] Описан повторный чистый прогон: какие данные очищаются и какие команды
выполняются, чтобы координатор мог надёжно перепроверить результат.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md`
- Review gate из `PRD.md`: распределения и два пути генерации после задачи 5
## Фактический прогон worker-а
Дата проверки: 2026-06-14.
Штатная команда:
```bash
make generated-history-analytics
```
Команда выполняет чистый прогон: `docker compose down -v --remove-orphans`,
запуск ClickHouse и Kafka, DDL, `GEN_RUN_MODE=backfill`, batch
STG -> ODS -> DDS -> DM, инициализацию Superset и итоговую проверку
`make generated-history-check`.
Быстрая команда issue 06 проверяет путь backfill -> DM -> Superset. Стык
backfill/live, отсутствие дублей на границе и однородность визитов через
`T_end` проверены в issue 05 и не входят в быстрый штатный check issue 06.
Быстрый проверочный профиль по умолчанию:
- `GEN_SEED=4242`
- `GEN_MODEL_T0=2026-01-01T00:00:00+00:00`
- `GEN_MODEL_T_END=2026-01-01T06:00:00+00:00`
- `GEN_MODEL_TIMEZONE=UTC`
- `GEN_MODEL_TIME_SPEED=1`
- `GEN_TICK_SECONDS=60`
- `GEN_LAMBDA_BASE_PER_MIN=60`
- `GEN_JITTER_PCT=0`
- `GEN_MIN_EVENTS_PER_TICK=1`
- `GEN_MAX_EVENTS_PER_TICK=1000`
Суточный профиль остаётся доступен явно:
```bash
GEN_MODEL_T_END=2026-01-02T00:00:00+00:00 make generated-history-analytics
```
Backfill завершился:
- `events=16054`
- `visits=1516`
- `users=445`
Batch STG -> ODS -> DDS -> DM:
- STG, суммарно по 4 потокам: `64216`
- `ods.browser_event=16054`
- `ods.location_event=16054`
- `ods.device_by_click=1516`
- `ods.geo_by_click=1516`
- `dds.event=16054`
- `dds.click=1516`
- `dds.event_without_click=0`
- `dm.v_events_enriched` в `dm.dq_summary`: `16054`
Повторная команда проверки:
```bash
make generated-history-check
```
Результат ClickHouse:
- `events=16054`
- `visits=1516`
- `users=445`
- `min_event_ts=2026-01-01 00:00:00.000000`
- `max_event_ts=2026-01-01 05:59:59.353407`
- `digest=B7E183DD1835E6593CC4B33C7F5B2817`
Возвраты:
- `users=445`
- `returning_users=351`
- `returning_share=0.7887640449438202`
- `max_visits_per_user=8`
Форма длины визита:
- `visits=1516`
- `short_visit_share=0.17678100263852242`
- `median_events_per_visit=8`
- `avg_events_per_visit=10.589709762532982`
- `capped_visit_share=0.06398416886543536`
- `median_duration_sec=204`
- `p95_duration_sec=835`
- `max_events_per_visit=30`
Ordered funnel:
- `home=1387`
- `products=849`
- `cart=511`
- `payment=332`
- `confirmation=182`
- `monotonic_ok=1`
- `confirmation_share=0.13121845710165825`
Contains funnel:
- `home=1387`
- `products=1254`
- `cart=880`
- `payment=568`
- `confirmation=324`
- `monotonic_ok=1`
- `confirmation_share=0.2335976928622927`
Основные DM-витрины:
- `dm.v_events_enriched=16054`
- `dm.v_daily_traffic=797`
- `dm.v_top_pages_daily=6`
- `dm.v_utm_effectiveness=33`
- `dm.v_session_overview=1516`
- `dm.dq_summary=20`
Superset technical check:
- `superset_datasets=6`
- `superset_dashboards=1`
- `superset_dashboard_charts=10`
- `dashboard_url=http://localhost:8088/superset/dashboard/ecommerce-analytics/`
- `login_api_status=200`
- `dashboard_api_status=200`
- `dashboard_title=🛒 E-commerce Analytics Dashboard`
- зарегистрированная Superset database `clickhouse_dwh` читает
`dm.v_events_enriched` через SQLAlchemy engine:
`superset_clickhouse_events=16054`.
Проверки:
- `bash -n scripts/run_generated_history_analytics.sh` — ok.
- `bash -n scripts/check_generated_analytics.sh` — ok.
- `make generated-history-analytics` — ok.
- `make generated-history-check` — ok.
- `make generator-test` — 134 passed.
Документы запуска обновлены:
- `README.md`
- `docs/OPERATIONS.md`
- `generator/README.md`
- `docs/REPO_MAP.md`
- `docs/SUPERSET_DASHBOARD.md`
Follow-up:
- `.scratch/generator-model-time-startup-history/issues/08-migrate-course-from-archive-seed.md`
— миграция учебных материалов с архивного сида на генерацию.
## Риски и что не проверено
- Ручная оценка формы dashboard глазами не выполнялась: это финальная HITL-приёмка.
- Суточный backfill не прогонялся до конца в этом срезе: он доступен через env,
но для координатора выбран быстрый 6-часовой профиль.
- `kafka_load_dag.py` физически оставлен как архивный ручной путь; штатные
документы больше не ведут через него как основной источник аналитики.
- `scripts/check_generated_analytics.sh` рассчитан на штатный UTC-профиль
(`GEN_MODEL_T0/T_END` с `+00:00`). Для произвольного timezone offset в этих
переменных нужна отдельная нормализация границ.
- Проверка `no_2022_rows` доказывает отсутствие архивного сида внутри быстрого
чистого модельного диапазона. Она не доказывает отсутствие старых строк на
грязном стенде вне этого диапазона; штатная команда закрывает это через
`docker compose down -v`.
## Review gate
- Саморевью worker-а нашло слабые доказательства по проверкам issue 05 и Superset:
добавлены возвраты, форма длины визита, ordered/contains funnel, Superset login
API, dashboard API и чтение ClickHouse через Superset database.
- Независимый reviewer gate: `gate pass`, блокирующих находок нет.
- Minor-находка reviewer-а по старой подсказке `make data` в `scripts/run_batch.sh`
исправлена до коммита.
- Проверки после исправлений: `make generated-history-check` — ok,
`bash -n scripts/*.sh` — ok.
@@ -1,117 +0,0 @@
Status: done
# Портативный артефакт стартовой истории и runbook по стенду
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Сейчас стартовая история персистится только в Kafka compact-топиках
(`generator_state`, `generator_startup_history_manifest`) и в ClickHouse. Чистая
пересборка стенда (`make generated-history-analytics` делает `down -v`) каждый раз
**заново генерирует** backfill. Портативного файла-артефакта, который можно
сгенерировать один раз и быстро восстановить на чистом стенде без запуска
генератора, нет.
Из-за этого неудобно: мгновенно сбросить стенд, держать длинную стартовую
историю (2+ суток, чтобы суточная волна повторялась на графике) без повторной
генерации. Сейчас «дёшево» только live-возобновление из слепка и перезапуск без
`down -v`; полный сброс требует регенерации.
Этот пункт работает на главную цель: если стенд поднимается одной командой и есть
короткий runbook, генератор остаётся скрытой инфраструктурой и менти не нужно
знать его устройство. Развилка курса «урок про генератор vs скрытая
инфраструктура» уже закрыта в пользу скрытой инфраструктуры
(`docs/course/PRD.md` §7, 2026-07-04) — эта задача обеспечивает решению опору.
## What to build
- **Экспорт** стартовой истории в портативный файл-артефакт: события (в формате
сообщений топиков) плюс слепок состояния плюс манифест — один связный набор,
чтобы не смешать `GEN_SEED`, `T0`, `T_end` и настройки генерации.
- **Импорт** (решение 2026-07-04): воспроизвести события артефакта в Kafka-топики
и вернуть слепок с манифестом в служебные compact-топики. **Напрямую в
ClickHouse импорт не пишет ничего**: стенд наполняется штатным путём
(Kafka engine + MV -> STG, батч-ETL -> витрины). Так не появляется обходного
пути данных, каждый импорт заодно прогоняет весь пайплайн, а менти видит,
как пустой стенд наполняется изучаемыми механизмами. Live продолжает с `T_end`.
- **Громкий отказ при несовместимом state** (решение 2026-07-04, пересмотр
правила спеки). Различать два случая. Нет состояния или оно повреждено ->
чистый старт с предупреждением (как сейчас, оставить). Состояние есть и
читается, но настройки несовместимы при `GEN_STATE_RESET=false` (оператор
намерен продолжить) -> жёсткое падение с указанием разошедшихся полей
(`seed`/`T0`/`timezone`/`speed`) и подсказкой выставить `GEN_STATE_RESET=true`,
если новый мир нужен осознанно. Текущее тихое поведение — `service.py:217-220`.
Правка идёт вместе с обновлением
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`.
- **Runbook «как пользоваться стендом на генерации»**: как сгенерировать,
сохранить, восстановить, выбрать длительность стартовой истории; что дёшево
(live-возобновление, перезапуск без чистки), а что требует регенерации.
- Новые команды экспорта/импорта делать в стиле глаголов (явное действие одной
командой), а не новыми комбинациями env-переменных.
## Acceptance criteria
- [x] Есть команда экспорта: стартовая история -> портативный файл-артефакт
(события + слепок + манифест) одним связным набором.
- [x] Есть команда импорта: на чистом стенде артефакт воспроизводится в Kafka
(события + служебные compact-топики) **без запуска генерации**; напрямую в
ClickHouse импорт не пишет. После штатного ETL контрольные числа в ClickHouse
совпадают с манифестом и исходной генерацией.
- [x] После импорта live продолжает с `T_end`: без дублей на границе и без
смешения миров.
- [x] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по
манифесту (`GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state).
- [x] Громкий отказ: живое читаемое состояние + несовместимые настройки при
намерении продолжить -> падение с перечислением разошедшихся полей и
подсказкой; нет состояния или повреждено -> чистый старт с предупреждением
(как сейчас). Спека обновлена в этом же изменении.
- [x] Runbook описывает генерацию один раз, дешёвое восстановление, выбор
длительности и то, что переживает перезапуск, а что требует регенерации.
- [x] Runbook — про **использование**, устройство генератора в нём не
объясняется; за конструкцией он отсылает к `generator/README.md` и
`docs/specs/`.
- [x] Документы запуска (`README.md`, `docs/OPERATIONS.md`,
`generator/README.md`) ссылаются на runbook.
## Notes
- Опирается на спеку `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`,
разделы «Манифест стартовой истории» и «Повторяемая проверка в ClickHouse».
- Спека уже упоминала будущий runbook «проверка генератора на стенде» — этот
issue его и закрывает, расширяя до полного цикла «генерация — сохранение —
восстановление».
- Откуда экспорту брать события — решить при реализации и зафиксировать в
спеке/runbook. Кандидаты: писать файл артефакта прямо при backfill (вторая
копия рядом с публикацией в Kafka), вычитать топики событий (учесть retention)
или выгрузить из STG (следить за точностью формата сообщений). Критерий
выбора: артефакт должен байт в байт воспроизводить сообщения топиков.
- Рекомендуемый режим ревью по coordinator-loop: **гейт** (state, сериализация,
формат данных, правка спеки — всё из порогов риска).
- Связано с `08-migrate-course-from-archive-seed.md`: миграция уроков идёт после
этой задачи и будет ссылаться на runbook отсюда (номера отражают порядок,
переставлены 2026-07-04).
## Идеи интерфейса — решения (2026-07-04)
Бывший раздел «на будущее, не решено» разобран с пользователем. Судьба идей:
- **Громкий отказ при несовместимом state** — включён в эту задачу
(см. What to build и критерии).
- **Глаголы / длительность / профили** — отдельная задача
`11-generator-launch-verbs-and-profiles.md`, делать **после этой и до
DAG-пульта**: чистый интерфейс «под капотом» делает DAG тонкой обёрткой с
простыми и понятными параметрами.
- **Airflow-DAG как пульт генератора** — приоритет поднят (пользователь,
2026-07-04): это будущий основной человеческий интерфейс стенда — «слишком
сложно» лечится формой в веб-UI, а не только runbook'ом. Отдельная задача
`12-generator-control-dag.md`, делать после задачи 11.
- **Доливка прошлого кусочком** — отдельная задача
`13-backfill-top-up-from-snapshot.md`, без приоритета.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md`
- `.scratch/generator-model-time-startup-history/issues/06-generated-history-as-analytics-source.md`
@@ -1,90 +0,0 @@
Status: done
# Миграция учебных материалов с архивного сида на генерацию
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
После перевода штатного аналитического контура на стартовую историю генератора
часть учебных материалов всё ещё описывает `data/*.jsonl` как основной источник
данных стенда. Это нельзя править внутри issue 06: потребуется пройти уроки и
сохранить понятный учебный путь.
Развилка «отдельный урок про генератор vs скрытая инфраструктура» закрыта
(2026-07-04, `docs/course/PRD.md` §7): **отдельного урока не будет, генератор —
скрытая инфраструктура**. Устройство генератора — это разработка бэкенда и
математика, не инженерия данных; в цели курса не попадает. Уроки адаптируем,
не вводя марковские цепи и сложный Python в путь менти.
## What to build
Обновить курс и связанные материалы (тест-план) так, чтобы основной путь был:
```text
startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset
```
Архивный `data/*.jsonl` оставить только как временную фактуру генератора.
Генератор в уроках подаётся как готовый источник данных стенда («откуда берутся
данные»), без погружения в его устройство.
## Acceptance criteria
- [x] `docs/course/` больше не ведёт ученика через `make data` или `kafka_load`
как основной путь получения аналитических данных.
- [x] Уроки явно объясняют, что `data/*.jsonl` пока остаётся кладовкой значений
для генератора, а не источником аналитического контура.
- [x] Уроки не вводят устройство генератора (марковская модель, внутренний
Python) в путь менти: генератор упоминается только как готовый источник данных.
- [x] Тест-план согласован с новым штатным путём запуска.
- [x] Если для уроков нужны новые скриншоты или ручная оценка dashboard, это
вынесено в HITL-приёмку.
## Notes
Реальный объём больше, чем кажется: `make data` вплетён во **все** уроки 0005,
а не только в урок 06 (проверка 2026-07-04, `grep -rn "make data" docs/course/`):
- `docs/course/README.md` — быстрый старт через `LIMIT=50 make data`;
- `lessons/00_kafka_intro.md``make data` играет роль трекера, на нём построен
весь разбор партиций и offset'ов;
- `lessons/0105``LIMIT=50 make data` в запуске, «полном сбросе» и таблицах
проверок;
- `docs/TEST_PLAN.md` — сценарии на старом пути.
Тонкое место — урок 00: там ручная заливка используется как учебный приём
(наглядно видно сообщения в топике). Решить при миграции, чем её заменить, не
потеряв наглядность; критерий приёмки запрещает `make data` как **основной путь
аналитики**, а не как локальный демонстрационный приём внутри урока.
Схема потока в `docs/course/PRD.md` §1 уже обновлена (поправка 2026-07-04) — PRD
дополнительно править не нужно.
Демо-материалов в скоупе нет: демо-употребление стенда устарело (демо выросло в
отдельный проект), шпаргалки удалены из репозитория 2026-07-04 (см. поправку в
шапке `docs/course/PRD.md`).
Рекомендуемый режим ревью по coordinator-loop: обычный (документы и уроки, кода
генератора и state задача не трогает); человеческая приёмка — проход по урокам
через пульт (HITL).
## Blocked by
- Жёстких блокеров нет: штатный путь (`make generated-history-analytics`) уже
существует, уроки можно вести через него.
- Мягкая зависимость от
`07-startup-history-portable-artifact-and-usage-docs.md`: runbook из issue 07
изменит команды запуска и восстановления стенда, а уроки будут на него
ссылаться. Делать эту задачу после него, иначе уроки придётся править дважды.
- Мягкая зависимость от `12-generator-control-dag.md` (решение 2026-07-04):
человеческая приёмка миграции — это проход по урокам 00–05, и вести её удобнее
через DAG-пульт, а не через консоль. Поэтому эта задача идёт последней в
очереди — после 12. Порядок всей очереди — в PRD, раздел «Задачи».
- Мягкая зависимость от `14-fast-teaching-profile.md` и
`09-seam-browser-fixture-not-preserved.md` (дополнено 2026-07-04): обе меняют
мир — 14 переводит профиль `daily-wave` на быстрый темп, 09 меняет схему
state. Артефакт стартовой истории для уроков должен родиться **после** них,
иначе он протухнет по антисмешиванию и проход по урокам придётся повторять.
@@ -1,159 +0,0 @@
Status: done
# Браузерная фактура не сохраняется на стыке восстановления визита
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Что нашли
Визит, переживший стык backfill->live (активен на `T_end` и продолжается живьём),
меняет браузерную фактуру в live-продолжении (фактура — конкретные значения полей
события: какой `browser_name`, user agent, язык стоят в строках). Внутри одного `click_id` человек как
будто «пересел» с одного браузера на другой посреди сессии.
Эмпирика. Чистый стенд, 2 суток backfill + 6 live-тиков за `T_end`,
`GEN_SEED=4242`, `GEN_MODEL_T0=2026-01-01T00:00:00+00:00`,
`GEN_MODEL_T_END=2026-01-03T00:00:00+00:00`, `speed=1`:
- `crossing_visits=22`; `browser_name` отличается у `17/22`, `browser_language`
у `22/22`; каждая сторона внутренне постоянна (`22/22`).
- Чисто-исторические визиты (>=3 событий, целиком до `T_end`): браузер постоянен
у `0/14556`.
- device / os / geo на стыке НЕ расходятся: `0/22` конфликтов на уровне ODS.
- `duplicate_events=0`: дублей на границе нет.
- Пример `click_id`: в истории `Chrome / or_IN`, в live `Firefox / el_CY`.
## Почему это дефект
Нарушает заявленный критерий задач 04 и 05: «визит, переживший восстановление,
остаётся однородным; контекст визита не меняется на стыке и не создаёт второй путь
генерации внутри одного `click_id`». Браузер — часть контекста визита.
Критерий был помечен выполненным, но проверялся неполно: саморевью и ревьюер
смотрели только поля `dds.click` (user / device / geo), которые по одной строке на
`click_id` и потому однородны структурно. Per-event браузерные поля в `dds.event` не
проверялись. Это ровно слепая зона «двух путей генерации», под которую PRD требовал
внешний review gate другой родословной после задачи 5 — а он был выполнен той же
родословной, и дефект прошёл.
## Причина (подтверждена чтением кода, 2026-07-04)
Первоначальная гипотеза «рассинхрон `event_index`» не подтвердилась: восстановление
перебирает визит с нуля, индекс события выровнен правильно (точнее, берётся
`event_index % len` — по модулю длины списка, см. `runtime.py:332`). Разошёлся сам
**источник** фактуры:
- При рождении визита (`generation.py`, `generate_batch`) браузерные строки берутся
у **случайного** click_id из словаря — «донора» (`rng.choice(visit_candidates)`,
затем `browser_by_click_id[base_click_id]`). Кто донор — нигде не сохраняется,
он живёт только в памяти процесса.
- При восстановлении из state (`runtime.py`, `_compact_visit_batch`) фактура
пересобирается из **другого** click_id — того, к которому привязан пользователь
(`browser_by_click_id[user.seed_click_id]`).
Донор и seed — разные click_id, отсюда разные браузеры. Внутри одного click_id
сида браузер практически постоянен (реальный клик одного человека), поэтому
«каждая сторона внутренне постоянна». `browser_language` разошёлся у 22/22
(случайные click_id почти всегда с разными языками), `browser_name`у 17/22
(популярные браузеры иногда совпадают случайно). device и geo не расходятся,
потому что оба пути берут их из `UserProfile`.
Расхождение шире, чем браузер (уточнение по ревью 2026-07-04). Шаблон location
ищется по event_id браузерной строки (`location_by_event_id[...]` в обоих путях),
то есть тоже зависит от того, чей click_id взят источником. Из state
восстанавливаются только `page_url` / `page_url_path`; остальные per-event поля
location — `referer_url`, `referer_medium`, `utm_*` — приходят от источника и на
стыке расходятся так же, как браузер. Эмпирика выше их просто не замеряла.
Тот же `restore_state` -> `_visit_from_state` -> `_compact_visit_batch` работает и
при восстановлении после сбоя (задача 04) — дефект касается и этого пути, не
только стыка backfill->live. Стык и рестарт на уровне генератора — один и тот же
код, различие только в отбрасывании просроченных визитов.
## Масштаб и важность
- Затрагивает только визиты, активные ровно на `T_end` (здесь 22 из 17397 визитов,
~0.13%). Чистый backfill и чистый live — без дефекта.
- Тот же механизм (фактура пересобирается при restore) вероятно затрагивает и
восстановление после сбоя (задача 04), не только backfill->live; проверка задачи 04
его не ловила, потому что смотрела только `dds.click`.
- Для учебного демо урон низкий, но это реальное нарушение стыковой однородности и
сигнал, что браузерная фактура не входит в восстановимое состояние визита.
## Решение по направлению фикса (пользователь, 2026-07-04)
**Хранить донора в state.** В сериализацию визита (`_visit_to_state`) добавить поле
с click_id донора фактуры (`base_click_id`), рядом с `page_url_paths` /
`timestamps`. При восстановлении `_compact_visit_batch` берёт строки из
`browser_by_click_id[base_click_id]` — по уже существующему абсолютному индексу
восстановленная часть точно продолжает выпущенную.
Рассмотренные альтернативы (отклонены):
- Брать браузер от `seed_click_id` пользователя уже при рождении визита — schema
state не меняется и семантика красивее («один человек — один браузер»), но
меняется весь выпуск генератора: контрольные суммы, распределение браузеров,
артефакты пришлось бы перегенерировать. Это уже не багфикс, а смена поведения;
если захочется — отдельная задача.
- Сериализовать браузерные строки визита целиком — надёжно, но раздувает state и
дублирует словарь. Перебор: донор + абсолютный индекс дают тот же результат.
Детали для исполнителя:
- В основной ветке рождения донор покрывает визит целиком: кандидаты фильтруются
по `len(browser_events) >= len(visit_path)` (`generation.py:170`), так что
индекс не выйдет за список строк донора и `% len` становится безобидным
холостым ходом (оставить или убрать — на усмотрение при фиксе).
- **Запасная ветка рождения** (`generation.py:182-185`): если кандидатов нет,
берётся одна случайная браузерная строка и повторяется на весь визит — тогда
«восстановить по донору и индексу» даст другие строки. С реальным сидом ветка,
судя по фильтру, не срабатывает — проверить при фиксе и либо превратить её в
громкую ошибку, либо сериализовать так, чтобы восстановление её воспроизводило.
Молча оставить как есть нельзя.
- Неизвестный `base_click_id` при восстановлении — ошибка (по образцу проверки
`seed_click_id` в `_user_from_state`). Сейчас в `_compact_visit_batch` два
тихих fallback'а (`.get(...)` со словарём целиком для браузера и
`location_events[0]` для location) — для донора их заменить на ошибку, не
копировать.
- **Известное расхождение вне скоупа**: event_id при рождении — случайный uuid4
(`generation.py`, `_new_uuid`), при восстановлении — детерминированный uuid5 от
`click_id:index` (`runtime.py:140-141`). Фикс это не трогает: коллизий нет,
дублей не создаёт. Тест поэтому сравнивает события по всем полям, **кроме
`event_id`** — иначе он падает не из-за нашего бага.
- Расширение схемы state — поднять `STATE_VERSION` (`state.py`) и валидацию
нового поля. Старые state становятся несовместимы: поведение при этом — по
действующему правилу (сейчас — предупреждение и чистый старт; громкий отказ
при намерении продолжить делает задача 07, здесь его не реализовывать).
## Acceptance criteria
- [x] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state
посреди визита, восстановить и сверить оставшиеся события с продолжением без
рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и
location: referer, utm — не только browser_name; см. «Расхождение шире» выше).
Это один код восстановления для стыка backfill->live и crash-recovery
(задача 04) — одного теста на него достаточно.
- [x] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что
нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык
однородны в `dds.event` по браузерным полям (`browser_name`,
`browser_language`) **и** полям источника перехода (referer, utm) — 0
расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как
повторяемый, а не разовый.
- [x] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов
device / os / geo на уровне ODS по-прежнему 0.
- [x] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый
state обрабатывается по действующему правилу (см. детали выше).
## Notes
- Рекомендуемый режим ревью по coordinator-loop: **гейт** (state, сериализация —
из порогов риска). Ровно эта слепая зона уже пропустила дефект один раз:
критерий задач 04/05 проверяли только по `dds.click`.
- Задача 13 (доливка) идёт после этого фикса — restore-механизм должен быть
исправлен до того, как на него навешивать доливку.
## Blocked by
- Нет. Это bug по результатам независимой проверки задач 05 и 06.
@@ -1,89 +0,0 @@
Status: done
# Гео-карта на дашборде нечитаема
## Parent
`.scratch/generator-model-time-startup-history/PRD.md` (раздел «Финальная ручная
приёмка»: если панелей не хватает для просмотра — отдельная задача на панель).
## Что нашли
На финальной ручной приёмке (шаг 2 спеки) виджет «🌍 Geography Map» показывает
что-то, но прочитать нельзя:
- нет всплывающих подсказок (tooltip) — навёл на страну, значения не видно;
- нет легенды и подписи шкалы — непонятно, что кодирует цвет и в каких единицах;
- цветовая шкала неочевидна — подсвечено мало стран, остальное пусто.
График рисуется, но непонятно, что именно он показывает.
## Две разные причины (не путать)
1. **Читаемость визуализации (эта задача).** Сделать так, чтобы у графика были
tooltip, легенда и подпись метрики/единиц и понятная цветовая шкала — либо
настройкой текущего чарта, либо заменой карты (choropleth — карта, где страна
закрашена по значению) на более читаемую форму, например топ-N стран
столбцами, потому что распределение сильно разрежено. Важно: сейчас это
legacy-виз `world_map` (`superset/create_dashboard.py:149-164`), и у него,
судя по параметрам, легенда и tooltip вообще не настраиваются — скорее всего,
основной путь именно смена типа визуализации, а не подкрутка текущего.
Возможности виз-типов уточнить по актуальной документации Superset через
Context7 (правило AGENTS.md) и зафиксировать выбор.
2. **Перекос гео-данных (отдельно).** Сама гео-фактура (значения стран в
событиях) берётся из статического сида (`geo_by_click_id`), и на приёмке
распределение стран оказалось сильно перекошенным — закрашено мало стран.
Опора на чужой сид — осознанно временная (ADR-0006: своя гео-фактура —
отдельный будущий шаг); лечится это генерацией гео, а не настройкой чарта.
Здесь не решаем, только отмечаем как смежную причину.
## Acceptance criteria
- [x] У гео-графика есть tooltip со значением по стране (на выбранном типе
визуализации — карте или замене).
- [x] Есть легенда и подпись: какая метрика и в каких единицах кодируется
(для текущей метрики `COUNT(*)` честный ответ — «событий, штук»).
- [x] Выбранный тип визуализации читаем на текущем (разреженном) распределении;
выбор типа (оставить карту или заменить) зафиксирован с коротким «почему».
- [x] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры
из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки
чарта.
- [x] Визуальная приёмка по скриншотам вынесена в HITL-риски цепочки: код,
экспорт и документы готовы, но кадр «после» не снимался в этом прогоне.
## Как принимать (дописано 2026-07-04)
Проверка визуальная, через скриншоты playwright-cli (так уже делали в спеке
редизайна дашборда `docs/specs/2026-06-06-superset-dashboard-redesign.md`):
- Скриншоты «до» и «после»: кадр виджета целиком, плюс кадр с наведением курсора
на закрашенную страну / столбец (виден tooltip).
- «Читаемо» значит: по кадру виджета целиком (без обращения к SQL и коду чарта)
можно ответить на три вопроса — какая метрика показана, в каких единицах, у
какой страны значение больше. Если по кадру ответить нельзя — не принято.
- Tooltip — по отдельному кадру с наведением: видно название страны и значение
метрики. Кадр с tooltip в headless-прогоне может стабильно не ловиться
(tooltip следует за курсором); тогда допустим другой способ показать tooltip
(например, короткая запись экрана) — согласовать с человеком, молча не
ослаблять.
Состояние стенда: стенд не трогали с 2026-06-14 и он мог умереть. Это нормально:
полная пересборка (`make generated-history-analytics`) заново генерирует backfill.
По умолчанию она даёт **6 часов** модельной истории
(`GEN_MODEL_T_END` в `scripts/run_generated_history_analytics.sh`) — для приёмки
читаемости этого достаточно. Если нужен вид как на исходной находке (2 суток),
переопределить `GEN_MODEL_T_END` на `T0`+2 суток при запуске; профиль «2 суток
одной командой» — это ещё не сделанная задача 11, здесь её не делать.
Портативный артефакт (задача 07) для этой задачи не нужен и не блокирует её.
## Notes
- Всплыло на просмотре дашборда на данных генерации (2 суток backfill).
- Смежные документы: `superset/create_dashboard.py` (определение чарта),
`docs/SUPERSET_DASHBOARD.md` — при смене типа визуализации обновить в этом же
изменении.
- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и
подсказки у других виджетов), но базово задача — про гео-карту. Если проход
делается, он не должен раздувать задачу: заметил — запиши отдельным issue.
- Рекомендуемый режим ревью по coordinator-loop: обычный (риска для state и
данных нет, правка ограничена определением чарта и документацией).
@@ -1,58 +0,0 @@
Status: done
# Глаголы, длительность и профили запуска генератора
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Запуск генератора недружелюбный: поведение собирается из ~10 связанных
env-переменных, а смысл запуска — из их комбинации (`GEN_RUN_MODE` +
`GEN_STATE_RESET`). Конец истории задаётся абсолютной меткой `GEN_MODEL_T_END`,
которую оператор считает в уме от `T0`.
Задача — дать чистый интерфейс «под капотом» перед DAG-пультом
(`12-generator-control-dag.md`): если команды простые и говорящие, DAG
становится тонкой обёрткой с понятными параметрами, а не переводчиком
формы в матрицу переменных (решение пользователя, 2026-07-04).
## What to build
- **Глаголы вместо матрицы флагов:** явные команды `backfill` / `continue` /
`reset` (make-цели или аргументы запуска), а не комбинация
`GEN_RUN_MODE` + `GEN_STATE_RESET`. Старые переменные могут остаться как
низкоуровневый механизм, но документированный путь — глаголы.
- **Длительность вместо абсолютной метки:** задавать конец истории как
длительность от `T0` (например, `2d`), расчёт `T_end` — внутри.
- **Именованные профили** вместо повторения блока переменных: минимум два —
быстрый проверочный (6 часов, текущий дефолт CI) и «с суточной волной»
(2+ суток).
## Acceptance criteria
- [x] Стартовую историю на 2 суток можно получить одной командой с глаголом и
длительностью/профилем, без ручного расчёта `T_end` и без выставления
`GEN_RUN_MODE`/`GEN_STATE_RESET` вручную.
- [x] Глаголы не меняют семантику режимов: за `backfill`/`continue`/`reset`
стоит тот же контракт модельного времени и state, что в спеке
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`.
- [x] Профили покрывают быстрый проверочный прогон и прогон с суточной волной;
выбранный профиль виден в логах/манифесте.
- [x] Runbook (из задачи 07) и документы запуска переведены на глаголы и
профили; старый способ через переменные упомянут как низкоуровневый.
- [x] Существующие тесты генератора проходят; поведение по умолчанию
(CI-профиль) не изменилось.
## Notes
- Это интерфейсный слой над существующей механикой: генерацию, state и
антисмешивание не менять.
- Вместе с глаголами не забыть команды экспорта/импорта из задачи 07 — они
уже в стиле глаголов, привести всё к одному стилю.
## Blocked by
- `07-startup-history-portable-artifact-and-usage-docs.md` — runbook и команды
экспорта/импорта появляются там; эта задача переводит их на единый стиль.
@@ -1,169 +0,0 @@
Status: done
# Airflow-DAG — пульт управления генератором
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Даже с runbook и глаголами управление генератором остаётся консольным. Идея
(пользователь, 2026-06-14; приоритет поднят 2026-07-04): параметризованный DAG
в Airflow как основной человеческий интерфейс стенда — форма в веб-UI, где
выбираются операция, профиль, длительность. Плюсы: валидация настроек против
манифеста ещё до запуска, наглядный статус, понятные ошибки.
Учебный бонус: такой DAG сам по себе учебный материал — живой пример
параметризованной оркестрации (тема урока 4), что ближе к цели курса, чем
устройство генератора.
## Решения (дооформление 2026-07-04)
- **Без Docker-доступа из Airflow.** Docker socket в контейнеры Airflow не
прокидываем. Конечные операции — это обычный Python-код генератора, которому
нужны только настройки и Kafka; таски DAG выполняют его сами, в worker'е
Airflow. Урок из проекта `airflow-greenplum-solution` (пользователь): DAG
говорит с управляемой системой по сетевому протоколу, жизненный цикл
контейнеров — только хост/Makefile.
- **Границы пульта.** `make up`, `make clean` (полный сброс) и старт/стоп
live-сервиса — консоль; операции `continue` в пульте нет намеренно.
Backfill и import работают только на чистом стенде (пустые топики данных);
грязный стенд лечится `make clean` с консоли — пульт при отказе прямо
подсказывает это.
- **Генератор не автостартует.** Сейчас `make up` поднимает live-генератор
вместе со стендом (`docker-compose.yml`, `restart: unless-stopped`) — с
пультом это неверно: любой backfill был бы сразу отбит предпроверкой, а
выключить live из UI нельзя. Live становится явным действием
(`make generator-continue`); механизм — например, compose-профиль для
сервиса `generator`, выбрать при реализации и поправить make-цели.
- **Три операции, а не четыре.** `backfill` | `import` | `check`. Отдельного
`export` нет: артефакт в коде пишется только по ходу backfill
(`StartupHistoryArtifactBuilder` внутри `_run_backfill`), «выгрузки задним
числом» не существует — поэтому у backfill есть поле `artifact_path`
(«сохранить артефакт», пусто — не сохранять).
- **Один DAG** с параметром «операция», а не несколько DAG по операциям.
- **После backfill/import DAG сам запускает ETL** (`TriggerDagRunOperator` на
существующий ETL-DAG) и **дожидается его завершения** перед check:
`wait_for_completion` в Airflow 2.10.5 по умолчанию `False` (сверено по
Context7, ревью Codex 2026-07-04) — простой trigger запустил бы check
раньше конца ETL. Менти видит цепочку «мир → пайплайн → витрины».
## What to build
- **Код генератора доступен таскам Airflow.** Варианты: смонтировать
`generator/src` томом (как уже смонтирован `./sql`) и добавить в
`PYTHONPATH`, либо ставить пакет в `Dockerfile.airflow`. Монтировать нужно
во все Airflow-сервисы (scheduler, webserver, worker): список профилей в
форме читается из `PROFILES` при разборе DAG. Критерий выбора — правка
генератора не должна требовать лишних пересборок.
- **Выравнивание зависимостей:** версии сейчас расходятся — kafka-python
2.0.5 у генератора против 2.0.6 в `airflow/requirements.txt`; привести к
одной. `prometheus-client` в образе Airflow нет — либо добавить, либо (лучше)
точка входа backfill не поднимает HTTP-сервер метрик (`service.py:65-66`
вызывается безусловно — понадобится небольшая склейка; это единственное
место, где честно появляется новый код, зафиксировать его в PR).
- **DAG `generator_control`** в `airflow/dags/`: `schedule=None`,
`max_active_runs=1`, форма запуска на `Param`:
- `operation`: `backfill` | `import` | `check` (выпадающий список);
- `profile`: из `PROFILES` (`launch.py`), список брать динамически;
- `duration`: строка вида `6h`/`2d`, пусто — из профиля;
- переопределения мира для backfill (минимум скорость и seed) — через
механизм `overrides` в `build_launch_env`;
- `artifact_path`: для backfill — куда сохранить артефакт (пусто — не
сохранять), для import — что импортировать; дефолт в общем томе `./data`.
- **Ветвление по операции**`BranchPythonOperator`, в стиле существующих
DAG стенда.
- **Каждая операция начинается с валидации, падение — до любых записей:**
- backfill и import: топики данных пусты (переиспользовать
`KafkaTopicInspector.assert_data_topics_empty`) **и STG-таблицы
ClickHouse пусты** — батч-ETL пересобирает ODS из всего STG
(`sql/ods/20_stg_to_ods.sql`), поэтому пустых топиков мало: старый мир в
STG смешался бы с новым. При отказе в логе — подсказка про `make clean`;
- import дополнительно: артефакт читается, манифест совместим с выбранными
настройками (готовые проверки `startup_history_artifact.py`), в лог —
перечень разошедшихся полей;
- вспомогательно: проверка «live не работает» по метрикам
`generator:9109` внутри сети compose (см. Notes об ограничениях).
- **Тела операций — вызовы существующего кода:** `build_launch_env` + прогон
генерации до `T_end` (backfill), функции `startup_history_artifact`
(import), сверка контрольных чисел с манифестом (check). Источник манифеста
для check — compact-топик (он есть и после backfill, и после import);
артефакт — запасной вариант.
- `retries=0` у содержательных тасков: оператор должен сразу видеть ошибку
(проверенный приём из greenplum-проекта).
- **Документация:** в runbook `docs/runbooks/startup-history.md` — раздел
«Пульт в Airflow» как основной путь и явные границы пульта (что остаётся
консолью и почему, включая «continue в пульте нет намеренно»); тонкость
нестандартного мира: `make generator-continue` для мира с переопределёнными
настройками требует тех же настроек, громкий отказ подскажет разошедшиеся
поля. Ссылки из `docs/OPERATIONS.md` и `README.md`; правки compose и
Makefile описать в том же PR (правило репозитория).
## Acceptance criteria
- [x] После `make up` (генератор не автостартует) стартовую историю выбранного
профиля/длительности можно создать из веб-UI Airflow, не открывая консоль и
не выставляя переменных окружения; следом ETL запускается из той же цепочки
и `check` зелёный.
- [x] Импорт артефакта из веб-UI: несовместимый артефакт отклоняется **до**
записи в топики, в логе таска — перечень разошедшихся полей.
- [x] Backfill/import на непустом стенде отклоняются предпроверкой с
подсказкой про `make clean`; миры не смешиваются.
- [x] Артефакт, сохранённый при backfill из веб-UI, пригоден для консольного
импорта (формат один и тот же), и наоборот.
- [x] Операция check сверяет контрольные числа ClickHouse с манифестом и
падает при расхождении.
- [x] Docker недоступен из контейнеров Airflow (socket не монтируется) — это
граница решения, а не упущение.
- [x] Консольные глаголы работают как раньше; `scripts/*` и DAG сходятся в
одном `launch.py`, дублирования логики запуска нет.
- [x] `make up` больше не запускает live; `make generator-continue` запускает
его явно; существующие сценарии (`generated-history-analytics`, CI) не
сломаны.
- [x] Runbook и `docs/OPERATIONS.md` описывают пульт как основной путь и его
границы.
- [x] DAG остаётся читаемым менти: витрина параметризованной оркестрации,
сложность живёт в коде генератора.
## Notes
- Перед реализацией сверить API формы (`Param`, enum, описания полей) и
`TriggerDagRunOperator` для Airflow 2.10 через MCP Context7 — правило
репозитория.
- `Config` генератора читает env процесса при создании: в таске собирать
окружение через `build_launch_env` и применять к процессу таска (executor —
LocalExecutor, таск живёт в своём процессе). Не забыть `GEN_DATA_DIR`:
`build_launch_env` его не задаёт, дефолт `/data`, а в контейнерах Airflow
данные смонтированы в `/opt/airflow/data` — без явной установки `Config()`
упадёт, словари `*.jsonl` не найдутся.
- Права на файлы: артефакт из worker'а пишется uid'ом Airflow (50000); в
контейнере генератора `./data` смонтирован **read-only**, консольные
скрипты монтируют каталог артефактов отдельно. Чтение работает везде,
перезапись чужого файла — не всегда; одну строку об этом — в runbook.
- Проверка `generator:9109` — вспомогательная эвристика, не защита: ошибка
DNS означает «сервис не поднят» (не ошибку таска); при крашлупе после
громкого отказа порт мигает (HTTP-сервер стартует до валидации state);
эфемерные контейнеры `docker compose run` под этим именем не видны. Основная
защита от смешивания — пустота топиков данных.
- Backfill — блокирующий таск на минуты; при `max_active_runs=1` для ручного
стенда это нормально.
- Грабли greenplum-проекта, применимые здесь: DAG создаются на паузе — в
инструкции приёмки не забыть unpause перед trigger; автоматические прогоны
гонять через REST API, а не CLI внутри контейнера.
- Возможное развитие (не в скоупе): операция «очистить данные» из пульта
(прецедент удаления/пересоздания топиков из DAG уже есть в
`airflow/dags/utils/kafka_helpers.py`) — сняла бы требование `make clean`
перед новым миром.
- Артефакты `daily-wave`, сделанные до задачи 14, протухнут после неё
(антисмешивание отработает громко) — при пересечении работ это ожидаемо.
- Рекомендуемый режим ревью по coordinator-loop: обычный — state и
сериализацию задача не трогает, код генератора переиспользуется как есть.
## Зависимости (проверенные предпосылки)
- `07-startup-history-portable-artifact-and-usage-docs.md` — сделана.
- `11-generator-launch-verbs-and-profiles.md` — сделана.
- Мягкая связь с `14-fast-teaching-profile.md`: быстрый профиль появится в
выпадашке сам, если список профилей брать из `PROFILES` динамически;
блокером не является.
@@ -1,208 +0,0 @@
Status: done
# Глагол next-day: собрать следующий модельный день
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Рамка пересмотрена 2026-07-07 (обсуждение с пользователем). Было: «удлинить
готовую историю» (2 суток -> 7) — этот мотив закрыл портативный артефакт
(задача 07), приоритет был низкий. Стало: **режим кормления стенда порциями**.
Ценность не в длине истории, а в дискретности подачи:
- **Урок.** Менти триггерит «следующий день» -> в Kafka падает батч за новый
модельный день -> прогон `etl_pipeline` -> витрины и дашборды сдвинулись.
Полный цикл DWH виден за один шаг; повторяемо и управляемо, в отличие от
непрерывного live.
- **Имитация жизни.** Тот же механизм по расписанию — стенд «живёт».
В этой задаче делаем глагол готовым к расписанию (идемпотентность,
громкие отказы), но само включение расписания — отдельная задача
(решение по скоупу 2026-07-12, см. «Решения по ревью постановки»).
- Удлинение истории остаётся как частный случай (несколько next-day подряд).
## Интерфейс
Новый глагол `next-day` в существующем DAG `generator_control`
(`airflow/dags/generator_control_dag.py`), не отдельный DAG: менти уже знает,
где ручки генератора. Pause-check `etl_pipeline` переиспользуется
(`generator_control_dag.py:153-164` — подтверждено ревью). Guard чистого
стенда **не** переиспользуется — у next-day своя предпроверка границы
(см. решение 3 ниже).
## Развилка реализации — решена (пользователь, 2026-07-07)
**Выбран вариант 2: генерация следующего дня от слепка `T_end`.**
Обоснование. Диагноз задачи 20 показал, что генератор на стыке ничего не
ломает — красный гейт был гонкой в самой проверке. Оба варианта одинаково
требуют цепочку границ и расширение проверок, а вариант 1 сверх того требует
новый режим среза в импорте («всё или ничего» сейчас). Вариант 2 не дороже,
честнее (жизнь стенда не ограничена длиной артефакта) и переиспользует
закалённый restore-механизм (задача 09: донор в state, громкие ошибки,
детерминированная запасная ветка).
Отклонено: вариант 1 (резать готовый артефакт на дневные порции) — не даёт
неограниченной жизни и требует своей новой механики импорта.
## Решения по ревью постановки (2026-07-12, два слепых ревью)
Оба ревью (Codex и Claude, свежие сессии) вернули CHANGES_REQUIRED.
Все находки внесены решениями ниже; полные индексы — в обменном каталоге
сессии (в репо не хранятся).
1. **next-day — новый ограниченный режим запуска, а не переиспользование
существующего глагола.** Сейчас `backfill` — свежая генерация от `T0`
с чистым завершением, `continue` — бесконечный live
(`generator/src/clickstream_generator/launch.py:96-128`,
`service.py:96-128`). next-day = восстановление мира из state
+ генерация ровно одного дня с самозавершением + сдвиг манифеста.
Это основная работа задачи, «переиспользуется» только restore-механизм.
2. **Один модельный день = ровно 24 часа, полуоткрытый диапазон
`[T_end, T_end + 24h)` в модельном времени (UTC).** Календарные сутки и
`GEN_MODEL_TIMEZONE` в границах не участвуют; литералы времени в
проверках — по образцу `clickhouse_datetime_literal` (урок задачи 20).
3. **Предпроверка границы вместо guard'а чистого стенда.**
`assert_stand_clean` в DAG требует пустые Kafka и STG
(`airflow/dags/lib/airflow_control.py:99-109`) — next-day по построению
доливает в непустой стенд. Своя предпроверка: манифест существует,
state согласован с `T_end` манифеста, live-генератор остановлен.
Образец «непустого» режима — `assert_stand_clean.sh continue`
(в Python-версии guard'а такого режима нет — добавить именно для
next-day, `clean`-режим не менять).
4. **Идемпотентность — через явный параметр.** У DAG-запуска next-day есть
необязательный параметр `expected_t_end`: если задан и не совпадает с
`T_end` манифеста — громкий отказ, в тексте обе границы (ожидаемая и
фактическая). Повторный триггер «того же дня» с прежним `expected_t_end`
после успешного прогона ловится этим же сравнением. Без параметра —
генерация от текущей границы (режим «жизни»); от двойного параллельного
запуска защищает `max_active_runs=1`.
5. **Точка фиксации — публикация манифеста, автоотката нет.** Порядок
публикации дня: топики данных -> state -> манифест последним. Сбой до
манифеста оставляет «хвост» за границей — его ловит проверка цепочки
(непарные счётчики), восстановление — переимпорт артефакта
(задокументировать в OPERATIONS.md). Отклонено: инкрементальный откат
через `rollback_import_topics` — он удаляет топики целиком
(`startup_history_artifact.py:250-270`) и для доливки непригоден.
6. **Цепочка границ живёт внутри единственной записи манифеста.**
Манифест остаётся одной записью compact-топика с ключом `default`
(`kafka_io.py:205-274`); добавляется необязательное поле `boundaries`
накопительный список границ `[T0, d1, ..., T_end]`. Старый манифест без
поля читается как `[T0, T_end]`. Итоговые счётчики и контрольные суммы
остаются накопительными за всю историю. Формат артефакта меняется только
этим необязательным полем — без новой версии и без изменения импорта.
7. **Проверка цепочки — новый батчевый режим, live-гейт не трогать.**
Текущий seam-гейт (`scripts/check_generated_analytics.sh:334-531`)
привязан к одной границе и живому генератору — литерально «расширить»
его нельзя. Новый режим (отдельная make-цель): по каждой границе из
`boundaries` — непарные счётчики и однородность per-event полей у
визитов, переживших границу (класс проверки задачи 09). Существующий
сценарий `make generated-history-runtime-check` остаётся зелёным как был.
8. **Расписание — вне этой задачи.** `schedule=None` у DAG не меняется;
задача гарантирует только готовность глагола к расписанию (пункты 4–5).
Включение «жизни» по расписанию — отдельная задача-продолжение (в ней же
решить: как расписание задаёт `operation=next-day`, ведь параметр DAG по
умолчанию — `backfill`, и планового запуска с дефолтом быть не должно).
9. **Исправлены ссылки:** запасная ветка рождения визита —
`generation.py:196-209` (строка 194 — основная ветка); донор
сериализуется в `runtime.py:178-226`.
## Acceptance criteria
- [x] Глагол `next-day` в `generator_control`: батч ровно за
`[T_end, T_end + 24h)` от `T_end` манифеста; после прогона манифест
содержит новую границу в `boundaries` и новый `T_end`.
- [x] Предпроверка границы: на пустом стенде (нет манифеста) next-day
громко падает; при работающем live-генераторе — громко падает;
`clean`-guard и существующие глаголы `backfill|import|check` не изменены.
- [x] Идемпотентность: запуск с `expected_t_end`, не равным `T_end`
манифеста, — громкий отказ с обеими границами в тексте; повторный запуск
того же дня после успеха — тот же отказ, дублей и второго мира нет.
- [x] Проверка цепочки (новая make-цель): проходит по всем границам из
`boundaries`; после N прогонов next-day в цепочке N новых границ;
непарные счётчики нулевые, per-event поля визитов через каждую границу
однородны (браузер, referer, utm).
- [x] Учебный цикл целиком и повторяемо: next-day -> прогон `etl_pipeline`
-> в DM-витринах появились строки ровно за новый день (счётчики до/после,
прирост только в диапазоне нового дня); проверено на двух next-day подряд.
- [x] Регрессий нет: тесты генератора и контракты корня зелёные
(`make test`), сценарий `make generated-history-runtime-check`
задачи 20 остаётся зелёным.
- [x] Документация: OPERATIONS.md — глагол, предпроверка, поведение при
сбое до фиксации манифеста (восстановление переимпортом артефакта).
## Границы (что не трогать)
- Поведение существующих глаголов `backfill`, `import`, `check` и
`clean`-guard'а.
- Формат артефакта — кроме необязательного поля `boundaries` (решение 6);
механику импорта не менять.
- Live seam-гейт задачи 20 (`generated-history-runtime-check`) — не
редактировать, только прогонять как регрессию.
## Blocked by
- Нет. Последний блокер снят 2026-07-12: `20-flaky-runtime-seam-check.md`
закрыта, гейт стыка стабилен (3 подряд зелёных прогона, красный сценарий
ловится).
- Исторические блокеры закрыты: `15-world-boundary-after-cross-review.md`
(граница миров) и `17-trusted-checks-startup-history-superset.md`
(доверенные проверки) — done.
Исторический контекст: `09-seam-browser-fixture-not-preserved.md` — шов
рождается при любом прохождении мира через сериализованный state (рестарт,
сбой, доливка — один код восстановления); фикс 09 положил донора фактуры в
state и закрыл тихие fallback'и. Прежняя редакция этой задачи называлась
«Доливка стартовой истории кусочком от слепка». Проверенные 2026-07-07
факты (import «всё или ничего», донор задачи 09) подтверждены обоими ревью
2026-07-12 по коду ветки.
## Решения по слепым ревью реализации (2026-07-12)
### Полная перечитка Kafka и конечный срок хранения — отклонено в задаче 13
Накопительные `visits` и `users` требуют точного объединения идентификаторов,
а текущая контрольная сумма — SHA-256 последовательности событий. Из одних
итоговых чисел и готовой контрольной суммы нельзя точно добавить новый день. Для
инкрементального расчёта пришлось бы хранить множества идентификаторов и новое
состояние hash в manifest. Это изменило бы формат артефакта сверх разрешённого
поля `boundaries` и нарушило бы решение 6.
Поэтому в задаче 13 остаётся точная пересборка по доступной истории Kafka.
Её проверяет
`test_next_day_publishes_24h_then_state_then_cumulative_manifest`: два дня,
точные накопительные счётчики и суммы. Риск принят только для ручного учебного
цикла. Ограничение по времени, памяти и сроку хранения записано в OPERATIONS.md.
Задача про расписание из решения 8 до включения обязана выбрать бессрочное
хранение data-топиков или новый согласованный формат накопительного состояния.
### Device/geo min/max timestamps после пересборки — отклонено
Эти два диапазона являются диагностическими полями статистики топика и не
участвуют в `compare_clickhouse_stats_to_manifest`, проверке цепочки или
критериях приёмки. Контракт задачи требует накопительные строки и контрольные
суммы; они пересчитываются точно и проверяются тестом двух `next-day`.
Выравнивание
device/geo min/max с потактовым backfill потребовало бы сохранять связь каждой
повторной строки с исходным browser-событием, которой в этих сообщениях нет.
Менять формат ради неиспользуемых диагностических полей в задаче 13 не следует.
### Нулевой стык — допустим с явным статусом
Диагноз текущего стенда подтвердил естественный ночной провал, а не потерю
визитов при восстановлении. На границе `2026-01-02T01:00:00+00:00` DDS и STG
дали `crossing_visits=0`. За предыдущий час завершилось 27 визитов, за
следующий началось 24. Последний визит закончился в `00:56:57.766327`, новый
начался в `01:00:00`; пауза составила 183 секунды. State на границе содержал
`active_visits=0`, поэтому restore не мог отбросить активный визит. Для
контроля: на предыдущей границе state содержал один активный визит, и DDS/STG
нашли один переходящий визит.
Поэтому нулевой стык проходит с явным статусом «однородность неприменима».
Пустая порция, хвост за последней границей и непарные строки по-прежнему дают
отказ. Остаточный слепой участок принят: однородность нельзя проверить без
переходящего визита. Restore закреплён тестом двух последовательных `next-day`
и детерминизмом задачи 09, а live-гейт задачи 20 отдельно проверяет стык, где
переходящий визит гарантирован сценарием.
@@ -1,94 +0,0 @@
Status: done
# Быстрый учебный профиль: суточная волна за минуты занятия
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Мир по умолчанию живёт со скоростью настенных часов (`speed=1`): чтобы
наблюдать суточную волну вживую, стенд должен непрерывно работать сутки.
Столько стенды включёнными не живут, и люди не вытерпят (пользователь,
2026-07-04). Стартовая история закрывает только половину проблемы: она даёт
закономерности **в статике**, но правый край графика ползёт 1:1 — на занятии
live выглядит мёртвой картинкой.
Решение пользователя (2026-07-04): учебному миру нужна скорость 60 — модельный
час за настенную минуту, полная суточная волна проживается за ~24 минуты
занятия.
## What to build
- Перевести профиль `daily-wave` (`launch.py`) на `GEN_MODEL_TIME_SPEED=60`
при `GEN_TICK_SECONDS=1`. Модельный тик остаётся прежним: 1 настенная
секунда × 60 = 60 модельных секунд, поэтому фактура потока (крупность пачек,
бюджет тика до 72 событий в пике: 60/мин × 1.2) не отличается от
сегодняшней, и колпак `GEN_MAX_EVENTS_PER_TICK` трогать не нужно.
- Наивный вариант «speed=60 при тике 60 с» отвергнут при разборе
(2026-07-04): модельный тик стал бы часом; первые события всех визитов тика
получают один таймстемп — старт тика (`runtime.py:433`,
`generation.py:198`) — история слиплась бы в почасовые гребёнки, а пиковый
тик (4320 событий) упёрся бы в колпак 1000 и срезал бы верхушку волны.
- **Оценить цену тика в 1 Гц.** Сейчас каждый live-тик пишет state в
compact-топик, запись в историю пачек и несколько INFO-строк
(`service.py:566, 584-585, 591-600`): на тике 1 с это 86 400 сохранений и
~полмиллиона строк лога за настенные сутки против 1 440 сегодня. Пер-тиковые
INFO приглушить до DEBUG (или логировать раз в N тиков); частоту сохранения
state менять только с оглядкой на спеку — она входит в контракт
возобновления после сбоя.
- Профиль `ci` не трогать: это дефолт CI, его мир и контрольные суммы должны
остаться прежними.
- Обновить документы, где живут предположения о старом `daily-wave`:
runbook `docs/runbooks/startup-history.md` (таблица профилей; продолжение
мира — `PROFILE=daily-wave make generator-continue`; старые артефакты
профиля становятся несовместимыми — антисмешивание отработает громко, это
ожидаемо), `docs/OPERATIONS.md` (smoke-последовательность со `sleep 130`
ожидание двух минутных тиков — на тике 1 с теряет смысл), `README.md`,
`generator/README.md`.
- Спеку `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`
дополнить: выбранная пара tick/speed и почему именно она.
## Acceptance criteria
- [x] После backfill/импорта `daily-wave` и `PROFILE=daily-wave make
generator-continue` модельное время идёт ~в 60 раз быстрее настенного; на
дашборде суточная волна проживается за ~24 настенные минуты.
- [x] Пиковые тики не упираются в `GEN_MAX_EVENTS_PER_TICK` — волна не
срезана.
- [x] Стык «история → live» бесшовный: без дублей и дыр, антисмешивание
работает как раньше.
- [x] Многочасовой live на тике 1 с не заливает журналы и compact-топики:
пер-тиковые INFO приглушены или их объём обоснован в PR; объём
state-записей (86 400/сутки) измерен и обоснован — либо частота сохранения
state осознанно изменена вместе с правкой спеки (контракт возобновления).
- [x] Профиль `ci`, его контрольные суммы и поведение по умолчанию не
изменились; существующие тесты генератора проходят.
- [x] Runbook, `docs/OPERATIONS.md`, README'и и спека обновлены.
## Notes
- Нагрузка по событиям: ~42–72 события в секунду (ночь–пик) — незаметно для
Kafka и ClickHouse; backfill не дорожает (число тиков на 2 суток то же, что
сейчас: модельный шаг тика не изменился).
- Если генерация тика займёт больше 1 настенной секунды, тики пойдут реже и
фактическая скорость окажется чуть ниже 60 (`service.py:623-627`) — это
допустимо, точная скорость не обещается.
- Несовместимость со старыми мирами ловится по-разному: импорт артефакта и
continue от стартовой истории сверяют полный набор настроек (включая
`tick_seconds` в манифесте), а обычный live-continue — только
seed/T0/timezone/speed (`service.py:213-230`); для этой правки хватает и
его — speed изменился.
- Альтернатива «третий профиль fast-live» отвергнута: у медленного
`daily-wave` не остаётся сценария использования, а лишний профиль усложняет
выпадашку пульта (задача 12).
- Рекомендуемый режим ревью по coordinator-loop: обычный (значения профиля и
логирование). Исключение: если по ходу решат менять частоту сохранения
state — это уже контракт возобновления, эскалировать в **гейт**.
## Blocked by
Нет: профили из задачи 11 сделаны. Мягкие связи: задача 12 (профиль появится
в форме пульта автоматически) и задача 08 (артефакт курса рождается из этого
профиля — эту задачу лучше сделать до него).
@@ -1,46 +0,0 @@
Status: done
# Граница миров после кросс-ревью
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Закрыть семейство находок кросс-линейного ревью про «смешение миров»: после
обновления кода, сброса стенда или консольного запуска старое состояние и старый
live-генератор не должны тихо писать события в новый мир данных.
Источник: `.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`,
разделы `Issue 12/14` и `Issue 09`.
Связанные старые issue: `09-seam-browser-fixture-not-preserved.md`,
`12-generator-control-dag.md`, частично `14-fast-teaching-profile.md`.
## Acceptance criteria
- [x] Известная старая версия state при `GEN_STATE_RESET=false` даёт громкий
отказ с понятным сообщением, а не тихий чистый старт поверх существующей
истории.
- [x] Сброс и остановка стенда убирают ручной live-генератор из compose-профиля;
он не переживает `down`/`clean` и не воскресает после перезагрузки хоста.
- [x] Консольные пути backfill/import/continue используют ту же идею
предпроверки чистого стенда, что Airflow-пульт, но реализованы способом,
пригодным для запуска с хоста.
- [x] Документация объясняет миграционный случай: почему старый state может
теперь дать громкий отказ и что делать оператору или менти.
- [x] Есть проверка, которая доказывает каждый из трёх путей смешения миров:
старая версия state, compose-профиль live-генератора, консольный запуск на
грязном стенде.
## Blocked by
Нет — можно начинать сразу.
## Notes
- Рекомендуемый режим `coordinator-loop`: гейт. Worker `high`, ревью задачи
`high`, ревью кода `high`.
- Не чинить здесь ложнозелёные проверки и курс целиком: это отдельные задачи
17 и 18. Здесь только граница миров и доказательства именно этой границы.
@@ -1,50 +0,0 @@
Status: done
# Свежий стенд и Airflow-пульт без догадок
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Сделать так, чтобы человек на свежем клоне мог пройти быстрый старт до
`generator_control` без неявных шагов, сырых ошибок и зависаний Airflow.
Источник: `.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`,
разделы `Issue 12/14` и `Issue 08`.
Связанные старые issue: `12-generator-control-dag.md`,
`14-fast-teaching-profile.md`.
## Acceptance criteria
- [x] Quick start и курс явно называют `uv` и остальные обязательные
пререквизиты до первой команды, которая их требует.
- [x] Быстрый старт на чистом стенде подготавливает DDL до проверок генератора;
отсутствие таблиц не падает сырым `UNKNOWN_TABLE`.
- [x] После обновления репозитория Airflow получает новые зависимости и DAG-и
без ручных догадок: через пересборку, явную команду или ясно описанный шаг.
- [x] `generator_control` не висит навсегда, если зависимый `etl_pipeline`
стоит на паузе: оператор получает понятный отказ или инструкцию до запуска
долгого ожидания.
- [x] Решение по Airflow API, проверенное через Context7 для текущей версии,
зафиксировано в коде или документации кратко и рядом с местом выбора.
- [x] Runbook предупреждает, что долгий простой `daily-wave` на скорости 60
создаёт большую дыру в модельном времени, и советует reset или повторный
импорт стартовой истории.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/15-world-boundary-after-cross-review.md`
## Notes
- Рекомендуемый режим `coordinator-loop`: обычный issue, worker `medium`, ревью
кода `high`. Если правка уйдёт в устройство DAG-зависимостей шире
предпроверки паузы, эскалировать до гейта.
- Формальная зависимость от 15 нужна из-за общего участка предпроверки чистого
стенда: свежий стенд должен опираться на уже выбранный способ отказа и
диагностики грязного мира.
- Не включать сюда учебные тексты уроков 00/01/05, кроме пререквизитов и
quick start: основной проход курса закрывает задача 18.
@@ -1,48 +0,0 @@
Status: done
# Проверки startup-history и Superset без ложного зелёного
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Сделать проверки доверенными: зелёный результат должен означать, что проверен
фактический стык стартовой истории и живого продолжения, а Superset export
соответствует ожидаемому контракту чарта.
Источник: `.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`,
разделы `Issue 09`, `Issue 10` и `Issue 08`.
Связанные старые issue: `09-seam-browser-fixture-not-preserved.md`,
`10-dashboard-geo-map-readability.md`, частично `14-fast-teaching-profile.md`.
## Acceptance criteria
- [x] Проверка startup-history берёт границу стыка из манифеста фактического
прогона, а не из значения окружения по умолчанию.
- [x] ODS-часть проверки не проходит на пустом ODS: отсутствие строк считается
ошибкой проверки, а не успешным результатом.
- [x] Профиль запуска доходит до проверки: сценарий с `daily-wave` проверяет
именно мир `daily-wave`, а не диапазон `ci`.
- [x] Ожидание live-строк доказывает, что живое продолжение реально успело
записать данные; короткое ожидание не должно превращать проверку стыка в
пропущенную проверку.
- [x] Тест Superset сравнивает полный существенный контракт `params` между
конфигурацией и export, а не только небольшой набор полей.
- [x] Сортировка top countries задана явно по числу событий, чтобы порядок не
зависел от случайной формы ответа после pivot.
- [x] Повторная синхронизация Superset не удаляет одноимённые чарты вне целевого
источника данных или dashboard; деструктивность sync задокументирована.
## Blocked by
Нет — можно начинать сразу.
## Notes
- Рекомендуемый режим `coordinator-loop`: обычный issue, worker `medium`,
ревью кода `high`.
- Не смешивать с правками курса: задача 18 должна пользоваться уже доверенными
проверками, но текст уроков не является частью этой задачи.
@@ -1,50 +0,0 @@
Status: done
# Курс проходит по startup-history на чистом стенде
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## What to build
Довести учебный путь до воспроизводимого прохода на чистом стенде: уроки должны
работать с `startup-history`, не опираться на архивный сид и не обещать live-поток
там, где он больше не запускается по умолчанию.
Источник: `.scratch/handoffs/20260705-0002-cross-review-generator-chain.md`,
раздел `Issue 08`.
Связанные старые issue: `08-migrate-course-from-archive-seed.md`, частично
`12-generator-control-dag.md` и `14-fast-teaching-profile.md`.
## Acceptance criteria
- [x] Упражнение урока 01 с добавлением `kafka_msg_ts` воспроизводимо на стенде:
повторная заливка данных не сбрасывает схему, которую только что изменил
менти.
- [x] Урок 00 объясняет служебные топики генератора настолько, чтобы список
топиков не выглядел как набор неожиданных артефактов.
- [x] Уроки 00 и 05 не противоречат друг другу в описании видимости consumer lag;
утверждение проверено на текущем стенде.
- [x] Урок мониторинга согласован с no-live default: либо явно запускает live,
либо не обещает, что алерт отсутствия сообщений будет тихим на здоровом
стенде без live.
- [x] Операционная документация не ведёт учебный путь первым делом к архивному
`make data`; startup-history остаётся основным источником аналитики.
- [x] Проход уроков 00, 01 и 05 на чистом стенде проверен командно или вынесен
в явный HITL-пункт с точной инструкцией, что должен подтвердить человек.
## Blocked by
- `.scratch/generator-model-time-startup-history/issues/15-world-boundary-after-cross-review.md`
- `.scratch/generator-model-time-startup-history/issues/16-fresh-stand-generator-control.md`
- `.scratch/generator-model-time-startup-history/issues/17-trusted-checks-startup-history-superset.md`
## Notes
- Рекомендуемый режим `coordinator-loop`: обычный issue, worker `medium`, ревью
кода и документов `high`.
- Если по ходу окажется, что упражнение 01 требует нового поддержанного режима
перезаливки, не прятать это в тексте урока: оформить как явное решение и
покрыть проверкой.
@@ -1,63 +0,0 @@
Status: done
# Единые make test и make lint для коммит-гейта
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Why
Во время coordinator-loop по задачам 15-18 координатор несколько раз упёрся в
отсутствие общих целей `make test` и `make lint`. Из-за этого коммит-гейт
собирался вручную из частных команд: `make generator-test`, `docker compose
config --quiet`, `py_compile`, `bash -n`, `git diff --check` и отдельных
стендовых проверок.
Для учебного проекта это плохой пример: у менти и агента должен быть один
понятный путь проверки, а не набор догадок.
## What to build
Добавить поддержанные цели `make test` и `make lint`, которые покрывают текущие
реальные проверки репозитория и не печатают лишний подробный вывод по умолчанию.
## Acceptance criteria
- [x] `make test` существует и запускает полный быстрый набор проверок, нужный
перед коммитом: тесты генератора, контрактные тесты верхнего уровня и
ключевые проверки конфигурации.
- [x] `make lint` существует и запускает статические проверки, которые сейчас
выполняются вручную: синтаксис Python, `bash -n`, `docker compose config
--quiet`, `git diff --check` или обоснованно выбранный эквивалент.
- [x] Цели используют `uv` там, где нужен Python вне Docker, и не зависят от
локального `pytest`, случайно установленного на хосте.
- [x] Шумные команды не вываливают полный список тестов при успешном прогоне;
подробный вывод доступен через явный режим или отдельную команду.
- [x] `docs/OPERATIONS.md` или `README.md` кратко объясняет, когда запускать
`make test`, `make lint` и какие более дорогие стендовые проверки остаются
отдельными.
## Blocked by
Нет — можно начинать сразу.
## Notes
- Не включать в `make test` долгие или разрушительные проверки вроде
`make generated-history-runtime-check` без явного решения: они чистят volumes
и управляют стендом.
- Отдельно учесть проблему окружения coordinator-loop 2026-07-05: локальный
`uv run ... pytest` падал до запуска тестов из-за `snap-confine`, поэтому
цель должна либо идти через устойчивый Docker-путь, либо явно диагностировать
такую ошибку.
- По ревью Claude дополнительно подключены корневые контрактные тесты, усилены
ключевые проверки поведения `CHECK_LIVE_SEAM`/профиля/DM-витрин, а также
закрыты подтверждённые обходы через `generator-reset` и безверсионный state.
## Проверка
- `make contract-test` — 16 passed.
- `make generator-test` — 190 passed.
- `make lint` — passed.
- `make test` — 190 generator tests, 16 contract tests, compose config passed.
@@ -1,158 +0,0 @@
Status: done
# Runtime-проверка стыка нестабильна: фактура меняется через раз
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Что нашли
Два подряд прогона `make generated-history-runtime-check` на одном и том же
стенде (2026-07-07, проверка пути менти) дали разный результат:
- Прогон 1 (20:1820:20): **красный**
`Ошибка: per-event фактура меняется на стыке: 8/19`
(у 8 из 19 визитов, переживших границу backfill->live, поменялась
per-event фактура). При этом `duplicate_events=0`, events=2776.
- Прогон 2 (21:5x, та же команда, без изменений кода): **зелёный**
«runtime-проверка startup-history/live seam прошла», конфликтов 0,
events=2815.
Конфигурация проверки: `GEN_LAUNCH_PROFILE=daily-wave`,
`GEN_HISTORY_DURATION=1h`, `GEN_MODEL_T_END=2026-01-01T01:00:00+00:00`
(зашита в `scripts/run_generated_history_runtime_check.sh`).
## Почему это важно
- Это гейт, которому мы доверяем стык backfill->live (задача 17); флаки-гейт
ничего не гарантирует: красный пугает зря, зелёный ничего не доказывает.
- Смена фактуры на стыке — класс дефекта задачи 09, который считается
закрытым (донор фактуры сохранён в state, тихие fallback'и заменены на
ошибки). Либо фикс неполон, либо есть второй источник расхождения.
- Задача 13 (глагол next-day) навешивает на этот же механизм цепочку границ —
ей нужен доверенный, стабильный гейт.
## Диагноз (Codex, 2026-07-07; ключевой факт перепроверен координатором)
**Корень: гонка остановки live-генератора, а не смена фактуры.** Цепочка:
`run_generated_history_runtime_check.sh` останавливает live сразу после первых
новых STG-строк -> генератор публикует batch по четырём топикам
последовательно, без атомарности -> `docker compose stop generator` может
оборвать процесс между топиками -> часть browser-событий остаётся без парных
location/device/geo -> DDS строит `dds.event` через `LEFT JOIN location`,
и такие строки получают `NULL` в referer/utm -> seam-SQL считает это «сменой
фактуры внутри click_id».
Эмпирика: в красном прогоне `browser_raw=2776`, но `location_raw=2736`
40 live-событий без пары; в зелёном `browser_raw=location_raw=2815`, поэтому
проверка прошла (хотя device/geo и там отстали: 2781).
Судьба гипотез:
1. Запасная ветка рождения — **опровергнута**: ветка уже хранит донора
(`generation.py:196-209`, комментарий «Запасная ветка тоже восстановима»)
и громко падает на неполных locations. Перепроверено координатором по коду.
2. Недетерминизм live — **подтверждена частично**: решает не сид, а настенный
момент остановки процесса и какие топики успели дописаться.
3. Проверочный SQL — **опровергнута в формулировке**: поля не «легитимно
различаются», SQL маскирует пропущенный location под «смену фактуры».
Уточнение механизма обрыва (адверсарное ревью постановки, 2026-07-12;
перепроверено координатором по коду):
- Генератор не обрабатывает SIGTERM: ловится только `KeyboardInterrupt`
(`service.py:175`), а `docker compose stop` шлёт именно SIGTERM. В compose
у сервиса generator нет `init:`/`stop_signal:`, python работает PID 1 —
SIGTERM игнорируется, и через grace-период прилетает SIGKILL: жёсткий
обрыв посреди batch, без `finally` и без flush.
- Почему отставание device/geo не валит гейт, а location валит: device/geo
привязаны к `click_id` (одно значение на визит) — при пропуске весь визит
однородно NULL, массив уникальных значений длины 1, проверка проходит;
location привязан к `event_id` — частичный пропуск даёт смешанный массив
(реальное значение + NULL) и «смену фактуры». Догонять device/geo фикс
не обязан — важна граница batch.
## Направление фикса (развилка решена после ревью постановки 2026-07-12)
1. **Основное: корректная обработка SIGTERM в генераторе.** Минимальный
хендлер: по SIGTERM выставить `_running = False`, дать текущему tick'у
дописаться (все четыре топика + flush + запись batch history), затем
штатный `finally`/`stop()`. Runtime-check после `docker compose stop`
дожидается фактического завершения контейнера. Так генератор завершается
только на границе batch — гонка снята по построению, а не вероятностно.
- **Отклонено: «ожидание записи `generator_batch_history` перед stop»
как самостоятельный фикс** — запись history подтверждает только
ПРОШЕДШИЙ batch (`service.py:586-603`, пишется после цикла публикации);
следующий batch к моменту stop уже может быть в полёте, и SIGKILL
оборвёт его так же. Довод — ревью постановки, перепроверен по коду.
- Bounded live mode (генератор сам останавливается по лимиту) — более
тяжёлая альтернатива; в скоуп не входит, возвращаться к ней только
если SIGTERM-хендлера окажется недостаточно (с доводом в задаче).
2. **Дополнительно:** precheck в seam-check на непарные
browser/location/device/geo live-строки — чтобы ошибка называла реальную
причину, а не «фактура поменялась». Precheck не подменяет основную
проверку: настоящая смена фактуры внутри `click_id` обязана падать
как и раньше.
3. **Отклонены любые вероятностные смягчения**, а не только «увеличить
sleep»: settle-sleep перед прогоном, рост `WAIT_LIVE_ROWS`/`LIVE_SECONDS`,
сужение окна `GEN_LIVE_CHECK_MINUTES` и прочие способы снизить
вероятность — гонку они не убирают и фиксом не считаются.
## Принятый остаточный риск (решение координатора, 2026-07-12)
Ревью отметило: `stop_grace_period: 1m` не ограничивает публикацию по
времени — при зависании Kafka дольше минуты SIGKILL всё ещё оборвёт batch.
Риск принят: штатный тик публикуется за секунды (минута — многократный
запас), а ограничивать публикацию таймером значило бы рвать batch уже по
построению. Ключевое отличие от исходного флаки: такой обрыв больше не
маскируется под «смену фактуры» — precheck непарных строк назовёт его
явно, гейт упадёт громко и честно.
## Acceptance criteria
- [x] Причина расхождения 8/19 найдена и названа (код, не догадка) —
см. «Диагноз» выше.
- [x] Генератор корректно завершается по SIGTERM: текущий batch дописывается
во все четыре топика целиком (flush + запись history), потом процесс
выходит; runtime-check дожидается фактической остановки контейнера
(направление фикса, пункт 1). Плюс `stop_grace_period: 1m` в compose.
- [x] Seam-check различает «непарные live-строки» и «смена фактуры»:
precheck называет реальную причину (пункт 2).
- [x] Красный сценарий по-прежнему ловится: контролируемое искажение
`referer_url` у события пересекающего визита в `dds.event` уронило гейт
с прежним сообщением «per-event фактура меняется на стыке: 18/19» при
нулевых счётчиках непарных строк (стендовая приёмка 2026-07-12).
- [x] Стабильность обоснована структурно (SIGTERM ставит флаг, тик
дописывает все четыре топика + flush + history и выходит на границе
batch — тест `test_sigterm_during_publish_finishes_current_batch`);
дымовая проверка поверх довода: 3 подряд зелёных
`make generated-history-runtime-check` (2026-07-12, прогоны 19:25,
19:28, 19:30).
## Находки стендовой приёмки (2026-07-12, исправлены в этой же задаче)
Ревью по чтению кода их поймать не могло — вскрылись только прогонами:
1. **Гейт жил на побочном эффекте бага.** Пересекающие визиты для проверки
стыка появлялись только потому, что генератор игнорировал SIGTERM и
дописывал ~10 секунд данных до SIGKILL. После фикса live-хвост стал
коротким и пересекающих визитов могло не быть вовсе. Фикс: шаг 6
runtime-check ждёт не «любую новую STG-строку», а появления
пересекающего визита в STG (детерминированное предусловие проверки,
то же окно, что у seam-SQL).
2. **Часовой пояс в STG-запросах.** Сырые `event_timestamp` наивные и
означают UTC, сервер ClickHouse — Europe/Moscow: сравнение с границей,
заданной с `+00:00`, уезжало на 3 часа, precheck был зелёным вакуумно
(не видел ни одной live-строки). Фикс: явный `'UTC'` в
`parseDateTime64BestEffort*` с обеих сторон сравнения (4 места),
закреплено контрактными тестами.
## Blocked by
- Нет. Диагноз можно начинать сразу; стенд воспроизводит через раз.
Связано: `09-seam-browser-fixture-not-preserved.md` (класс дефекта и решение
про донора), `13-backfill-top-up-from-snapshot.md` (нуждается в доверенном
гейте на цепочке границ), `17-trusted-checks-startup-history-superset.md`
(появление этого гейта).
@@ -1,73 +0,0 @@
Status: done
# Подготовка ветки к слиянию: .gitignore для артефактов и необязательные поля формы
## Parent
`.scratch/generator-model-time-startup-history/PRD.md` — закрывающая
гигиена перед слиянием `feature/data-generator` в `main`. Находка F1 — из
`.scratch/generator-model-time-startup-history/hitl-findings.md`.
## Что не так
1. **Побочные артефакты backfill не покрыты `.gitignore`.** После HITL в
`data/` лежит `ci_backfill.json` (49 МБ) — риск случайного коммита.
Сиды проекта в `data/` — это `*.jsonl`, они под git и должны остаться.
2. **F1 (BUG):** в форме `generator_control` (Trigger DAG w/ config) все
пять необязательных полей (`duration`, `seed`, `model_time_speed`,
`artifact_path`, `expected_t_end`) показаны обязательными: красная `*`,
браузер не даёт отправить форму с пустым полем. Канонический запуск
«выбрать профиль, остальное пусто» через UI невозможен.
## Причина F1 (проверено)
В `airflow/dags/generator_control_dag.py:230-265` эти `Param(...)`
объявлены с `type="string"`. Шаблон формы Airflow вешает `*` и HTML-атрибут
`required` на каждое поле, у которого в `type` нет `"null"`.
Проверено по Context7 (`/apache/airflow/2.10.5`, 2026-07-19): идиома
необязательного строкового параметра — `Param(None, type=["null", "string"])`;
поле с `"null"` в `type` форма не помечает обязательным.
Чтение параметров в DAG уже терпимо к `None`: везде
`str(_param(...) or "").strip()` (строки 71-161), поэтому смена пустого
значения с `""` на `None` ничего не ломает — но это надо перепроверить
глазами по каждому месту чтения.
## Что сделать
- [x] В `.gitignore` добавить правило `data/*.json` (раздел «Тестовые
данные и артефакты») с комментарием: артефакты генератора; сиды
`data/*.jsonl` остаются под git.
- [x] Пять необязательных `Param` в `airflow/dags/generator_control_dag.py`
перевести на `Param(None, type=["null", "string"])`. Обязательные
(`operation`, `profile`) не трогать. У изменённых Param коротким
комментарием зафиксировать: `"null"` в type = необязательное поле
формы (проверено по Context7, Airflow 2.10.5).
- [x] Убедиться, что каждое место чтения этих параметров в DAG переживает
`None` (сегодня везде `or ""` — сверить полный список).
- [x] В `generator/tests/test_generator_control_dag_contract.py` дополнить
контракт: у пяти необязательных Param в объявлении есть `"null"`,
у `operation`/`profile` — нет.
- [x] `docs/OPERATIONS.md`: сверить описание формы `generator_control`;
если там есть обход «заполняйте все поля» — убрать, поведение
«пусто = из профиля» оставить как есть.
- [x] Дешёвые проверки зелёные: `make test` и `make lint` (нужен docker;
если в песочнице исполнителя недоступен — явно сказать в отчёте,
прогонит оркестратор).
## Границы
- Логику генератора (`generator/src/`) не менять.
- Профили и их состав не трогать (это отдельная задача редизайна).
- Форму DAG не перестраивать (отдельный DAG `next-day` — тоже отдельная
задача, F3).
- `data/ci_backfill.json` не удалять и не коммитить.
- Коммиты не делать.
## Сначала прочитать
1. Этот файл.
2. `.scratch/generator-model-time-startup-history/hitl-findings.md` — F1.
3. `airflow/dags/generator_control_dag.py` — объявления и чтение Param.
4. `generator/tests/test_generator_control_dag_contract.py`.
@@ -1,65 +0,0 @@
# Handoff: спеки генератора готовы — передача на детальный план и реализацию
Дата: 2026-06-10
Ветка: `feature/data-generator`
Жанр: одноразовые леса́ (ADR-0003) — durable-рассуждение в спеках/ADR/CONTEXT, не здесь.
> Место: `.scratch/handoffs/` по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md)
> (перекрывает generic-дефолт скилла «temp dir»: мультимашинность + worktrees).
> Предыдущий handoff (2026-06-09-generator-rework.md) отработал и удалён.
## Где остановились
Дизайн-этап переработки генератора **завершён**. Спека математической модели
написана, прошла **два** адверсариальных ревью свежими агентами (второе ревью
ловило ошибки правок первого — практика себя оправдала), все находки закрыты,
ключевые цифры перепроверены замерами по полному сиду. Кода по-прежнему
не трогали.
Разделение ролей (см. память проекта): дизайн/спеки — Fable, **детальный план
и реализация — Codex 5.5**. Следующая сессия — скорее всего, подготовка задачи
для Кодекса или сама реализация.
## Документы (источники истины, не пересказываю)
- **Мат-модель (главный документ для исполнителя):**
`docs/specs/2026-06-10-generator-math-model.md` — марковская цепочка по
страницам, формула «популяция ↔ интенсивность ↔ пауза», кулдаун, правило
30 минут на рестарт, критерии приёмки.
- **Форма доработки:** `docs/specs/2026-06-09-generator-rework-hierarchical.md`
(Open questions закрыты ссылкой на мат-спеку).
- **Почему генератор, а не реплей:** `docs/adr/0004-steady-stream-synthetic-generator.md`.
- **Профиль сид-датасета (опора калибровки):** `CONTEXT.md`, раздел
«Профиль сид-датасета» — измерено по полным файлам 2026-06-10.
- **Диагноз дефекта старого кода:** `generator/KNOWN_ISSUES.md`.
## Ловушки (одной строкой; детали — в спеках)
- Ранний «факт» **«1..7 событий на визит» был неверен** (срез файла); реально
1..27, медиана 10. Если встретишь «1..7» где-то ещё в доках/коде — это
остатки ошибки, чинить по профилю сида.
- **Формула паузы в мат-спеке обязательна** при смене λ/популяции; кулдаун в
неё не прибавляется (уже учтён балансом). Вторая ревизия исправляла именно
двойной счёт — не откатить случайно.
- **device/geo публикуются на каждое событие** (как в сиде) — это контракт с
ETL, не деталь.
- `event_timestamp` событий — **запланированное** время, не момент отправки
тика (иначе метки прилипают к сетке тиков).
- Модель интенсивности (`_calculate_events_count`) сохраняем; дефолт
`GEN_LAMBDA_BASE_PER_MIN` меняется 200 → 30.
## Следующий шаг
Передать пару спек Кодексу: детальный план реализации → код. Уровень спек
сознательно «решения и инварианты, без алгоритмов» — конкретику исполнитель
достраивает сам. Возможная подготовка: оформить задачу в `.scratch/<feature>/`
по `docs/agents/issue-tracker.md` (скилл `to-issues`, если план дробить).
## Suggested skills (для следующей сессии)
- **`to-issues`** — если решим дробить реализацию на задачи в локальном трекере.
- **`tdd`** — для этапа реализации (pytest-набор `generator/tests/` существует,
но писался под старую модель — пересмотр под новую неизбежен).
- **`adversarial-review`** / **`code-review`** — ревью реализации против
мат-спеки перед вливанием.
- **`conventional-commits`** — коммиты по правилам репозитория.
@@ -1,66 +0,0 @@
# Handoff: генератор разбит на AFK-задачи
Дата: 2026-06-11
Ветка: `feature/data-generator`
Жанр: одноразовые леса по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md).
## Где остановились
После обсуждения с пользователем дизайн переработки steady-stream генератора
переведён из больших спек в локальные задачи для агентов. Код генератора не
меняли.
Создан каталог задач:
- `.scratch/feature-data-generator/issues/01-minimal-connected-visit.md`
- `.scratch/feature-data-generator/issues/02-visit-page-path-and-monotonic-time.md`
- `.scratch/feature-data-generator/issues/03-tick-stream-with-active-visits.md`
- `.scratch/feature-data-generator/issues/04-user-population-and-returns.md`
- `.scratch/feature-data-generator/issues/05-intensity-and-flow-calibration.md`
- `.scratch/feature-data-generator/issues/06-state-v2-and-restart.md`
- `.scratch/feature-data-generator/issues/07-service-integration-and-docs.md`
Все задачи имеют `Status: ready-for-agent`. Разрез сделан как последовательные
вертикальные срезы, а не как слои архитектуры. Решение пользователя: идти по
этому варианту.
## Важный контекст
- Старый генератор считаем слабым прототипом, а не ценным кодом для сохранения.
Сохранять надо внешние контракты, если они полезны: топики Kafka, формат
сообщений для текущего ETL, команды запуска, метрики, идею compact-топика
состояния.
- Отдельную задачу "зафиксировать внешний контракт" не создавали: контракт
встроен в первый и последний срезы.
- Детали модели не дублировать отсюда. Источники истины:
- `docs/specs/2026-06-10-generator-math-model.md`
- `docs/specs/2026-06-09-generator-rework-hierarchical.md`
- `docs/adr/0004-steady-stream-synthetic-generator.md`
- `CONTEXT.md`
- `generator/KNOWN_ISSUES.md`
- Предыдущий handoff `.scratch/handoffs/2026-06-10-generator-spec-to-codex.md`
остаётся полезным как предыстория спек.
## Следующий шаг
Начать с задачи 01 через TDD:
1. написать один красный тест на публичное поведение "минимальный связанный
визит";
2. реализовать минимальное новое генеративное ядро без Kafka;
3. не тащить старую плоскую модель `generate_batch()`;
4. после зелёного теста переходить к следующему поведению, не писать все тесты
заранее.
Пример запуска задачи:
```text
/goal Реализовать .scratch/feature-data-generator/issues/01-minimal-connected-visit.md с использованием /tdd. Соблюдать цикл: один тест на наблюдаемое поведение → минимальная реализация → зелёный тест → остановиться и отчитаться. Не писать все тесты заранее, не реализовывать задачи 02-07.
```
## Suggested skills
- `tdd` — для реализации каждой задачи короткими циклами red-green-refactor.
- `adversarial-review` или `claude-team-review` — после реализации нескольких
срезов, чтобы сверить код с мат-спекой.
- `conventional-commits` — при фиксации следующих изменений.
@@ -1,86 +0,0 @@
# Handoff: ADR про модельное время генератора + отложенное ревью петли
Дата: 2026-06-11
Ветка: `feature/data-generator`
Жанр: одноразовый handoff по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md).
## Зачем этот handoff
Сессия с Fable шла в стиле brainstorm-with-docs про **модельное время
генератора**. Параллельно идёт эксперимент: автономная петля субагентов Кодекс
(координатор + worker + reviewer) доделывает срез генератора (задачи 06→07).
Эти две линии **намеренно разделены** — пересмотр времени не правит то, что петля
строит на реал-тайм-модели. Handoff фиксирует, что уже сделано и что осталось,
чтобы не потерять контекст в новой сессии.
## Что сделано в этой сессии (durable, не дублирую — см. файлы)
- Создан **[ADR-0005](../../docs/adr/0005-generator-model-clock.md)** «Модельные
часы генератора, отвязанные от настенного времени». Все решения там; кратко:
модельное время расцеплено с `now()`; режимы (живой ×1 / ускоренный ×K /
заливка `K → ∞`) — драйверы поверх одного шва; живой стенд масштабируется ×K
(дефолт ×1); правило 30 минут переопределяется в **модельном** времени с
параметрическим origin возобновления; «стартовая история стенда»
(сгенерированное прошлое + заморозка state v2) — **будущее направление, ещё не
строим**.
- Правки **[`CONTEXT.md`](../../CONTEXT.md)** (глоссарий): разведены три значения
слова «сид» (`GEN_SEED` / статический сид / стартовая история стенда) и добавлен
термин «модельное время и масштаб ×K».
**Эти изменения (ADR-0005 + CONTEXT.md) ещё НЕ закоммичены.** Их коммит — выход
брейншторма пользователя, его нельзя мешать с коммитами кода от петли (06/07).
Отдельный docs-коммит.
## Открытые線 для следующей сессии
### 1. Отложенное ревью результата автономной петли (приоритет)
Делать **после** того, как петля закоммитит задачу 07. Это read-only ревью.
Измерительный лист — в памяти проекта
`memory/ralph-loop-experiment-generator.md`. Главное:
- **A (главный индикатор эксперимента):** ссылочная целостность при
`restore_state`. `_validate_v2_payload` проверяет только форму, не ссылки.
Структурно-валидный state v2 с висячей ссылкой визит→пользователь
(`generator/src/clickstream_generator/runtime.py:283`) или пользователь→словарь
(`runtime.py:253/255`) проходит `from_dict_safe` и роняет `restore_state` без
обёртки try→fresh. Прогноз: петля это **пропустит** (слепые зоны внутри одной
линии Кодекс скоррелированы). Проверить гипотезу локально структурой с висячей
ссылкой (как пользователь проверял оценку снимка 4.7 MB и падение на битой
вложенности).
- **B/D (проверено OK ранее):** кулдауны по меткам, не тикам (`runtime.py:104`);
детерминизм компактного снимка (`_stable_event_id`, restore не жжёт ГПСЧ).
Подтвердить, что 06→07 их не сломали.
- **C (риск, вне скоупа 06):** всплеск досылки созревших событий с прошлыми
метками на первом тике после рестарта — всплыл ли хоть как риск в саморевью.
- Верхнеуровневое для 07: границы задачи, честность отметок acceptance, не
смешаны ли 06/07, `git status`.
Цель ревью двойная: (1) корректность 06/07; (2) мета-вывод — **какой класс
дефектов автономная петля систематически не видит без внешнего взгляда другой
модели**.
### 2. Реконсиляция мат-спеки с ADR-0005
После ревью петли. Разделы «Персистентность» и «Воспроизводимость» в
`docs/specs/2026-06-10-generator-math-model.md` написаны от настенных часов;
привести в соответствие с ADR-0005 (модельное время, правило 30 минут в
модельном времени). Не делать сейчас — файл смежен с тем, что петля трогала.
## Фон (память проекта, не дублирую)
- `memory/ralph-loop-experiment-generator.md` — схема Ральф-цикла + измерительный
лист.
- `memory/generator-full-rework.md`, `memory/fable-design-codex-implementation.md`
— направление переработки генератора и разделение ролей Fable(дизайн)/Codex(код).
- `.scratch/handoffs/2026-06-11-subagent-coordinator-experiment.md` — детальная
схема эксперимента с координатором и наблюдения по циклам 05/06.
## Suggested skills
- `conventional-commits` — для отдельного docs-коммита ADR-0005 + `CONTEXT.md`
(не мешать с кодом петли).
- `brainstorm-with-docs` — когда вернёмся к реконсиляции спеки или к проектированию
«стартовой истории стенда» (будущее направление из ADR-0005).
- `handoff` — после ревью петли зафиксировать рефлексию эксперимента (что петля
поймала/пропустила, особенно по находке A).
@@ -1,242 +0,0 @@
# Handoff: эксперимент с координатором субагентов
Дата: 2026-06-11
Ветка: `feature/data-generator`
Жанр: одноразовый handoff по [ADR-0003](../../docs/adr/0003-handoffs-in-scratch.md).
## Зачем нужен этот handoff
Пользователь хочет провести эксперимент: не реализовывать следующие задачи
самому верхнеуровневому агенту, а использовать его как координатора цепочки
субагентов. После эксперимента планируется отдельно отрефлексировать, что
сработало, что не сработало и стоит ли закреплять такой процесс.
Этот документ фиксирует текущее понимание процесса перед запуском, чтобы не
потерять договорённости в новой сессии.
## Текущий рабочий контекст
Активная линия работы — переработка steady-stream генератора кликстрима.
Задачи лежат в `.scratch/feature-data-generator/issues/`.
Уже выполнены и ожидают человеческой приёмки:
- `01-minimal-connected-visit.md`
- `02-visit-page-path-and-monotonic-time.md`
- `02-5-generator-service-cleanup.md`
- `03-tick-stream-with-active-visits.md`
- `04-user-population-and-returns.md`
Оставшаяся последовательность:
1. `05-intensity-and-flow-calibration.md`
2. `06-state-v2-and-restart.md`
3. `07-service-integration-and-docs.md`
Задачи зависимы и, вероятно, меняют близкие файлы генератора, поэтому запускать
их надо строго последовательно, не параллельно.
В рабочем дереве на момент обсуждения была незакоммиченная правка только в
`.scratch/feature-data-generator/issues/06-state-v2-and-restart.md`: добавлен
комментарий, что задачу 06 нельзя начинать до завершения предыдущих срезов,
особенно задачи 05.
## Договорённая схема эксперимента
Верхнеуровневый агент не включает для себя `/goal`. Его роль — координатор и
gatekeeper, а не непосредственный исполнитель.
Для каждой задачи:
1. Координатор запускает нового worker-субагента на ровно один issue.
2. Задание субагенту формулируется в стиле `/goal`:
- реализовать конкретный файл задачи;
- работать по методике `/tdd`;
- не реализовывать следующие задачи;
- не коммитить;
- не откатывать чужие изменения;
- в конце отчитаться по изменённым файлам, тестам, проверкам и рискам.
3. После реализации тот же субагент делает саморевью в отдельной роли:
- остановиться;
- проверить результат против acceptance criteria, спеки и тестов;
- искать ошибки, лишний объём, хрупкость и нарушение учебной читаемости;
- сначала выдать находки с важностью, не исправляя их сразу.
4. Координатор решает, какие замечания достаточно важные.
5. Тот же субагент исправляет только одобренные важные замечания.
6. Координатор проверяет верхнеуровневые вещи:
- границы задачи;
- список изменённых файлов;
- честность отметок acceptance criteria;
- тесты;
- `git status`;
- отсутствие смешивания нескольких задач.
7. Коммит делает координатор, не субагент.
8. Координатор закрывает агента и переходит к следующему issue новым
worker-субагентом.
Для сложных переходов, особенно после задачи 05 перед задачей 06, можно добавить
отдельного reviewer-субагента. Это не обязательный шаг на каждую задачу, а
контрольная мера, если есть риск, что саморевью исполнителя недостаточно.
## Роль координатора
Координатор не должен пытаться заново глубоко реализовывать или полностью
повторять работу субагента. Его польза — в управлении процессом:
- держать порядок задач;
- ограничивать область изменений;
- читать отчёты и принимать решения;
- запускать исправления только по существенным замечаниям;
- делать финальный обзор diff на уровне границ и рисков;
- выполнять проверки и коммитить.
Главный риск верхнеуровневого `/goal`: он может заставить координатора
оптимизировать работу под закрытие большой цели, а не под аккуратное управление
этапами. Поэтому для координатора `/goal` не использовать.
## Почему саморевью тем же субагентом допустимо
Пользователь отмечает, что на практике субагенты хорошо делают “ревью свежим
взглядом” внутри уже накопленного подробного контекста задачи. Это может быть
полезнее, чем полностью внешний поверхностный обзор координатора, которому не
хватит деталей реализации.
Ожидаемая модель:
- субагент глубоко понимает сделанную задачу и проверяет её на несостыковки;
- координатор не доверяет этому слепо, но оценивает адекватность выводов и
границы изменений;
- для особо рискованных мест можно подключить отдельного проверяющего агента.
## Suggested skills
- `tdd` — должен использовать worker-субагент при реализации каждой задачи.
- `conventional-commits` — использовать координатору перед созданием каждого
коммита.
- `claude-team-review` — опционально после задачи 05 или перед задачей 06, если
нужен дополнительный независимый взгляд на модель.
- `handoff` — после эксперимента зафиксировать рефлексию: что получилось, что
не получилось, какие правила стоит оставить.
## Возможная первая команда субагенту
```text
/goal Реализовать .scratch/feature-data-generator/issues/05-intensity-and-flow-calibration.md с использованием /tdd.
Работай только в границах этой задачи. Не реализовывай задачи 06-07.
Не коммить. Не откатывай чужие изменения.
Сначала прочитай сам issue и источники решений, указанные в нём. Работай
вертикальными TDD-срезами: один тест на наблюдаемое поведение -> минимальная
реализация -> зелёная проверка -> следующий тест.
В конце остановись и отчитайся:
- какие файлы изменены;
- какие тесты добавлены или изменены;
- какие проверки запускались и с каким результатом;
- какие acceptance criteria закрыты;
- какие риски или спорные места остались.
```
После этого координатор должен попросить того же субагента выполнить саморевью
результата, не исправляя замечания до отдельного решения координатора.
## Наблюдения во время эксперимента
### Цикл 05: интенсивность и калибровка потока
Стартовая задача оказалась дорогой по обратной связи: это не локальный инвариант,
а отладка статистической модели на длинных симуляциях. Первый worker долго
работал без промежуточного отчёта; координатору пришлось сначала поставить
мягкий статус-чек в очередь, затем прервать агента ради статуса. Это не сломало
работу, но показало: для статистических задач лучше заранее задавать контрольные
точки или ожидать длинный первый цикл.
Саморевью того же worker оказалось полезным. Оно нашло несколько реальных
несостыковок:
- `docker-compose.yml` оставлял старую интенсивность `GEN_LAMBDA_BASE_PER_MIN=200`;
- тест межсессионной паузы был слишком широким;
- в тестовой конфигурации оставались старые числа;
- README мог быть двусмысленным про `GEN_MIN/MAX_EVENTS_PER_TICK`.
Координаторская проверка тоже добавила ценность: после саморевью был найден
конфликт между новым `GEN_LAMBDA_BASE_PER_MIN=30` и старым нижним пределом
`GEN_MIN_EVENTS_PER_TICK=5`. При тике 5 секунд это давало минимум 60 событий в
минуту и ломало критерий интенсивности. Worker исправил это отдельной точечной
итерацией.
Полезная схема цикла:
1. worker реализует задачу;
2. координатор прерывает только если агент слишком долго молчит;
3. worker делает саморевью без правок;
4. координатор выбирает, какие замечания чинить;
5. worker чинит только выбранные пункты;
6. координатор добавляет свой узкий sanity-check по связям между дефолтами,
документацией и критерием задачи;
7. координатор коммитит.
Результат цикла 05:
- коммит `8f1e997 feat(generator): откалиброван поток steady-stream генератора`;
- полный прогон: `uv run --with-requirements generator/requirements.txt pytest generator/tests -q`
— 96 passed;
- `git diff --check` — без замечаний.
Промежуточный вывод: тот же субагент действительно хорошо использует подробный
контекст задачи для саморевью, но координатор всё равно нужен как внешний
проверяющий связей между настройками, обычным запуском и acceptance criteria.
### Reviewer-субагент между задачами
После задачи 06 пользователь предложил добавить отдельного reviewer-субагента
между задачами. Это выглядит особенно полезно на границах вроде 06 -> 07, где
следующая задача будет опираться на уже изменённые состояние, сервисный цикл и
документацию.
Важное ограничение: reviewer не является источником истины. Его находки нужно
рассматривать как гипотезы и классифицировать координатором:
- `чинить до коммита` — реальный дефект или риск закрытия acceptance criteria;
- `записать как риск` — важно знать, но не блокирует текущую задачу;
- `ложная тревога` — reviewer неверно понял код, тест или границы задачи;
- `вне скоупа` — может быть полезно позже, но не относится к текущему issue.
Только находки из первой группы возвращаются worker-агенту на исправление.
Иначе есть риск превратить reviewer-а в источник лишнего объёма и расползания
задачи.
В цикле 06 это правило сразу пригодилось. Саморевью исполнителя и отдельный
reviewer независимо нашли два существенных риска:
- битое state v2 с валидным ГПСЧ могло пройти `from_dict_safe`, а затем уронить
сервис уже в `restore_state`;
- снимок активных визитов сохранялся полными batch-словарями и на верхних
лимитах получался порядка мегабайт, хотя спека говорит про компактное
состояние.
Координатор проверил обе гипотезы локально: `restore_state` действительно падал
на битой вложенной структуре, а оценка JSON-снимка при 200 активных визитах и
популяции 300 дала около 4.7 MB. Эти находки классифицированы как `чинить до
коммита` и возвращены worker-агенту. Низкие замечания про совместимость
`generate_tick_batch` и пересечение сервиса с задачей 07 не стали правками:
первое оказалось ложной тревогой, второе — допустимым пересечением для
восстановления state v2.
После крупной переделки по замечаниям reviewer-а нужен второй круг ревью. В
цикле 06 это подтвердилось: исправление компактного состояния само изменило
дизайн снимка и восстановление активных визитов. Второй reviewer нашёл новый
дефект уже в исправленной версии: формально похожий v2-state с `population=[]`
или строковым `pending_visit_births` проходил первичную загрузку, но затем
оставлял поток без пользователей или ронял следующий тик. Координатор
подтвердил это локальной проверкой и вернул worker-агенту как единственный
обязательный пункт второго круга.
В цикле 07 reviewer оказался полезен уже не для поиска падений, а для силы
доказательства. Worker добавил сервисный тест с мок-публикацией, но внешний
reviewer заметил, что один опубликованный event доказывает четыре топика и
связи, но слабее доказывает невырожденную модель «один визит -> несколько
событий с одним `click_id`». Координатор классифицировал это как `чинить до
коммита`: финальный интеграционный тест должен доказывать именно уход от старой
плоской модели, а не только факт публикации.
@@ -1,76 +0,0 @@
# Handoff: ADR-0006 (источник аналитики) + спека модельного времени
Дата: 2026-06-14
Ветка: `feature/data-generator`
Режим работы: дизайн (brainstorm-with-docs), не реализация кода.
## Что сделано в этой сессии (закоммичено)
Содержание не дублирую — смотри сами файлы:
- **ADR-0006** `docs/adr/0006-generation-as-sole-analytics-source.md` — генерация
становится единственным источником аналитики; статический сид → архивный;
перевод процесса на новый (сгенерированный) сид — цель работы. Коммит `49512b1`.
- **Спека** `docs/specs/2026-06-14-generator-model-time-and-startup-history.md`
модельное время на практике: точка отсчёта `T0`, повторяемость, заливка
прошлого, стартовая история, сохранение состояния, скорость ×K, проверка в два
шага. Коммит `49512b1`.
- **Указатели вперёд** в ADR-0004 и ADR-0005 на ADR-0006 и спеку. Коммит `49512b1`.
- **AGENTS.md** — ужесточено правило про русский язык + добавлено правило про
понятность и краткость документов. Коммит `c4cc01e`.
## Контекст и решения, которых нет в артефактах (важны для продолжения)
- **Роль сида = «кладовка значений» (палитра атрибутов).** Генератор сам придумывает
структуру (иерархию, время, воронку), но фактуру (браузер/гео/устройство/utm)
копирует из строк сида: `generator/src/clickstream_generator/generation.py`
(строки ~202228, `{**base_browser, …}`), индексы в `dictionary.py`. Профиль
популяции хранится ссылкой на сид-сессию (мат-спека, §«Профиль пользователя»).
Поэтому сид нельзя удалить, пока генератор не научится фактуре сам.
- **Сид полностью чист** — проверено: 1000 записей × 4 файла, 0 падений в DQ
(`ods.*_errors`). Значит вывод сида не ломает учебный путь про грязные данные.
- **Двухшаговая проверка** (раздел «Проверка» спеки): шаг 1 — агент на чистом
стенде сверяет числа в ClickHouse (пирамида/воронка/возвраты), детерминированно;
шаг 2 — человек смотрит дашборды (распределение похоже на задуманное + стенд
«дышит» на ×K). Вскрытый пробел: дашборда с распределением сгенерированных
данных нет (Superset на сиде, Grafana показывает пропускную способность).
- **Известные расхождения калибровки** (из прошлого ревью, не в этих артефактах):
медиана длины визита ~8 против сидовых 10, всплеск на длине 2, нет
bounce-визитов длины 1, доля `/confirmation` ~27%. Когда генерация станет
источником, это станет «правдой» стенда — учесть при заполнении коридоров шага 1
и в будущей задаче калибровки/фактуры.
## Очередь (следующие шаги)
1. Выровнять `CONTEXT.md` (раздел «три значения слова сид») под ADR-0006:
статический сид → архивная кладовка значений, цель — полный вывод.
2. Пометить в мат-спеке `docs/specs/2026-06-10-generator-math-model.md` разделы
«Персистентность через рестарты» и «Воспроизводимость» как переописанные в
модельном времени новой спекой (ссылкой, без дублирования).
3. (Опционально) Этап 1 вживую: поднять стенд на текущем генераторе, снять
реальные числа в ClickHouse, заполнить коридоры в разделе «Проверка». Поднимает
docker-стек (меняет состояние) — только с согласия владельца.
4. Черновик спеки **синтеза фактуры** — то, что позволит удалить `data/*.jsonl`.
5. Черновик спеки **перестройки процесса** (загрузка `kafka_load_dag` → новый сид,
Superset/витрины, уроки) — на этапе реализации, по ADR-0006.
6. Реализация механизма (модельные часы/×K/стартовая история) — это код, зона Codex.
## Ограничения (продолжать соблюдать)
- Роль: Claude — ADR/спеки/ревью; Codex — код. Намерение, против которого
сверяем, живёт в ADR/спеках Claude.
- Язык: ясный русский без транслитераций (теперь зафиксировано в `AGENTS.md`).
- Коммиты: только по просьбе; формат conventional-commits; трейлер
`Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`.
- Git строго аддитивно: на ветке может параллельно писать Codex — не делать
amend/rebase/reset чужих коммитов. В `main` — merge `--no-ff`, не тащить
`.scratch/`.
- Context7 для спорных/меняющихся API (особенно Airflow).
- Публичный репозиторий — без персональных данных менти.
## Suggested skills
- **brainstorm-with-docs** — для продолжения дизайна (пункты 1, 4, 5 очереди:
`CONTEXT.md`, спека фактуры, спека перестройки процесса).
- **conventional-commits** — для коммитов.
- **Context7 (MCP)** — при касании API.
@@ -1,16 +0,0 @@
# Handoff: экономия контекста в coordinator-loop (указатель)
Дата: 2026-06-14 MSK
Полная, авторитетная версия этого handoff перенесена **внутрь навыка**, рядом с
handoff про наблюдаемость:
`~/dotfiles/agents/.agents/skills/coordinator-loop/handoffs/2026-06-14-coordinator-loop-context-economy.md`
Почему там: handoff про доработку навыка `coordinator-loop`, и сам навык, и его
handoff-побратим (`2026-06-14-observability-design.md`) живут в dotfiles. Чтобы будущая
сессия по доработке навыка нашла всё в одном месте, авторитетная версия лежит у навыка.
Здесь оставлен только указатель: диагностика, на которой стоит handoff, снята с прогона
именно этого репозитория (фичи `feature-data-generator` и
`generator-model-time-startup-history` под `/coordinator-loop`).
@@ -1,68 +0,0 @@
# Handoff: задачи по модельному времени и стартовой истории
Дата: 2026-06-14
Жанр: одноразовый handoff по ADR-0003.
## Что сделано
Создан рабочий набор артефактов для следующей фазы работ:
- `.scratch/generator-model-time-startup-history/PRD.md`
- `.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md`
- `.scratch/generator-model-time-startup-history/issues/02-model-time-to-clickhouse.md`
- `.scratch/generator-model-time-startup-history/issues/03-model-speed-and-day-factor.md`
- `.scratch/generator-model-time-startup-history/issues/04-state-v2-model-resume.md`
- `.scratch/generator-model-time-startup-history/issues/05-startup-history-backfill-to-clickhouse.md`
- `.scratch/generator-model-time-startup-history/issues/06-generated-history-as-analytics-source.md`
Артефакты режут работу на 6 последовательных issue и 2 review gate. Главная
идея: каждый кодовый срез должен подтверждаться через ClickHouse, а не только
локальными тестами генератора. Финальная проверка дашбордов глазами остаётся в
конце.
## Важные решения
- Координатор не берёт `/goal` на всю цепочку: один worker получает один issue.
- Worker работает через `/tdd`, не коммитит и не реализует следующие задачи.
- После реализации worker делает саморевью без правок.
- Координатор классифицирует находки и коммитит сам.
- После задачи 3 нужен review gate по сквозному инварианту времени.
- После задачи 5 нужен review gate по распределениям и двум путям генерации.
- Для review gate после задачи 5 нужен reviewer другой родословной; без него
координатор останавливает цепочку и отдаёт риск человеку. Это следует из
`docs/research/2026-06-11-subagent-coordinator-experiment.md`.
## Что важно не потерять
- В задаче 1 нужно зафиксировать durable-контракт, а не оставить решение только
в комментарии к issue.
- При ×K восстановление после сбоя нельзя считать простым `datetime.now()`: нужна
сохранённая связка модельного и настенного времени.
- Короткий или долгий простой считается по модельному времени, а не по настенным
минутам.
- Чистый прогон должен сбрасывать не только ClickHouse, но и Kafka-топики данных
и состояние генератора.
- Стартовая история должна быть парным артефактом: события, слепок состояния и
манифест.
- Живой и восстановленный пути не должны менять контекст внутри одного визита;
это известная слепая зона из research.
- Задача 6 специально сужена до штатного пути стенда и документов запуска.
Уроки, новые панели и улучшения дашбордов уходят в follow-up, если окажутся
нетривиальными.
## Suggested skills
- `to-issues` — если понадобится переразбить или опубликовать дополнительные
issue.
- `tdd` — основной режим работы worker-а над каждым кодовым issue.
- `conventional-commits` — перед каждым коммитом координатора.
- `claude-team-review` или другой внешний reviewer — на двух review gate.
- `handoff` — если работа прерывается между issue или после review gate.
## Следующий шаг
Начать с
`.scratch/generator-model-time-startup-history/issues/01-time-and-startup-history-contract.md`.
Это HITL-задача: нужно закрепить интерфейс модельного времени, манифест
стартовой истории, правило границы `T_end` и способ повторяемой проверки в
ClickHouse.
@@ -1,96 +0,0 @@
# Handoff: независимое ревью модельного времени и стартовой истории
Дата: 2026-06-14
Жанр: одноразовый handoff по ADR-0003.
## Контекст
После прогона coordinator-loop по фичам `feature-data-generator` и
`generator-model-time-startup-history` (issues 0106, реализация Codex) проведено
независимое ревью результатов — другой родословной (Claude). Это в том числе
закрыло пропущенный внешний review gate после задачи 5 из PRD: он был выполнен той
же родословной, что и worker, а PRD требовал другую.
## Что проверили независимо (не по отчётам)
- `make generator-test``134 passed` (своим прогоном).
- Код = «Рабочий контракт реализации» спеки буква в букву: настройки
`GEN_MODEL_*`, state v2 (`model_timestamp/wall_timestamp/model_time_speed/…`),
формула live-возобновления, полуоткрытая граница `[T0, T_end)` с
`drain_until(include_boundary=False)`, манифест и антисмешивание.
- Данные в ClickHouse реальные и сходятся с отчётами до цифры: 6 ч → 16 054;
перепрогнали 1 сутки → 93 952; 2 суток → 187 087 событий / 17 397 визитов /
2 765 пользователей.
- Пирамида (users < visits < events), возвраты, монотонность обеих воронок,
отсутствие архивного сида (все строки 2026 года).
- Повторяемость суточной волны на 2 сутках: часовые числа day1≈day2
(отношение в основном 0.9–1.1), форма «ночь–день» повторяется.
- Стык backfill→live (live-доливка от `T_end=2026-01-03`): дублей 0, граница
держится, device/os/гео на стыке однородны.
- Дашборд: визуальная приёмка (шаг 2 спеки) — человеком и через Playwright.
«Rows by Layer» (4 равных столбца = 187 312) корректен: пайплайн проносит
событие 1:1, потерь нет, дедуп at-least-once на чистом backfill не возникает.
## Что нашли
- **issue 09 — реальный дефект стыка.** Браузерная фактура не переживает
восстановление визита: у визитов, активных ровно на `T_end`, в live-продолжении
меняется `browser_name`/`browser_language` (17/22 и 22/22), хотя внутри чистого
backfill браузер постоянен (0/14556). device/гео не задеты. Нарушает заявленный
критерий однородности задач 04/05; проверялось неполно (смотрели только поля
`dds.click`, а не per-event браузер в `dds.event`). Масштаб ~0.13% визитов.
- Гео-карта на дашборде нечитаема (нет легенды/подсказок/понятной шкалы) — issue 10.
- Стартовый сид по умолчанию — 6 часов (быстрый профиль для CI); суточная волна на
нём не видна, для просмотра нужен ≥2-суточный профиль через env.
## Открытые пробелы проверки (НЕ закрыты)
Ревью прошло по корректности кода, форме данных, стыку и дашборду, но НЕ трогало:
1. **×K не гоняли.** Все прогоны на `speed=1`. Ускорение модельного времени,
событийный бюджет по модельной длительности и смена дневного коэффициента при
×K проверены только в коде/по отчёту issue 03.
2. **Live-crash recovery не воспроизводили.** Формула возобновления с настенной
дельтой и закрытие просроченных визитов — только в коде. Гипотеза: дефект
issue 09 бьёт и сюда (тот же restore-механизм), эмпирически не подтверждено.
3. **Распределения не сверяли с коридорами мат-спеки** (`2026-06-10-generator-math-model.md`).
Подтвердили форму (монотонность, возвраты, длина), но не попадание
`confirmation_share` и доли коротких визитов в спроектированные коридоры.
4. **Воспроизводимость сами не перепрогоняли** — идентичность checksum при
повторном чистом прогоне взята из отчётов issue 03/05.
5. **Второй review gate (после задачи 3) не делали** — систематический sweep на
утечку настенных часов в расчёт интенсивности и сохранение состояния.
Помельче: не проверяли обработку повреждённого/старого state (fresh-start с
предупреждением), `browser_user_agent` (тот же механизм, что issue 09), сам процесс
coordinator-loop по правилам PRD.
## Зафиксированные follow-up (все needs-triage, на потом)
- `issues/07-migrate-course-from-archive-seed.md` — миграция уроков.
- `issues/08-startup-history-portable-artifact-and-usage-docs.md` — портативный
артефакт стартовой истории + runbook + идеи интерфейса (глаголы вместо флагов,
длительность/профили, громкий отказ при несовпадении, доливка кусочком,
Airflow-DAG как пульт).
- `issues/09-seam-browser-fixture-not-preserved.md` — дефект браузерной фактуры.
- `issues/10-dashboard-geo-map-readability.md` — читаемость гео-карты.
- `docs/course/PRD.md` §7 — развилка «генератор как скрытая инфраструктура vs
отдельный урок про генератор» (не грузить менти марковскими цепями).
## Состояние стенда
Поднят на 2 сутках генерации (+ несколько live-тиков от seam-проверки): ClickHouse,
Kafka, Superset, postgres-metadata. Дашборд:
`http://localhost:8088/superset/dashboard/ecommerce-analytics/`.
## Suggested skills
- `tdd` — для фикса issue 09 (один поведенческий тест на однородность браузера через
стык, затем минимальная правка).
- `conventional-commits` — перед коммитами.
- `claude-team-review` / внешний reviewer другой родословной — на пробелы 1–5.
## Следующий шаг
Сегодня только фиксация, без правок. Дальше — приоритизировать issue 09 (баг) против
08/10 (удобство/виз) и закрыть пробелы проверки 1–5 (особенно ×K и crash recovery).
@@ -1,91 +0,0 @@
# Handoff: триаж бэклога генератора и целеполагание стенда
Дата: 2026-07-04
Жанр: одноразовый handoff по ADR-0003.
## Рабочая форма (зафиксирована сегодня)
С Claude — обсуждение проекта, планы, проектные документы и независимые ревью.
Исполнение кода — Codex, предпочтительно через `/goal` или `/coordinator-loop`.
Сохранено в памяти Claude (memory), новая сессия подхватит автоматически.
## Что решено и зафиксировано
- **Целеполагание сверено** и записано в `docs/course/PRD.md` §1 (поправка
2026-07-04): три части трека ClickHouse (теория — внешние курсы, лабы —
clickhouse-learning-cluster, этот стенд — интеграции «как в жизни»); иерархия
целей: курс -> стенд-носитель -> генератор как скрытая инфраструктура.
- **Развилка «урок про генератор» закрыта**: урока не будет, генератор — скрытая
инфраструктура (PRD §7).
- **Демо-употребление устарело**: `docs/DEMO_CHEATSHEET_5MIN.md` и
`docs/DEMO_SCRIPT_10_15MIN.md` удалены, демо выросло в отдельный проект.
- **Новая открытая развилка** в PRD §7: кластерная конфигурация ClickHouse
(пользователь склоняется к «всё в одном», решение не принято).
- **Конвенция трекера**: завершённая задача — `Status: done`
(`docs/agents/issue-tracker.md`); задачи 01–06 фичи переведены в `done`
(стояли ошибочные `ready-for-human` от глючившего координатора).
## Бэклог фичи `generator-model-time-startup-history`
Список задач и порядок — в PRD фичи (раздел «Задачи», дополнен сегодня):
- **07** (артефакт + runbook, `ready-for-agent`) — импорт строго через Kafka
(напрямую в ClickHouse не пишет — решение пользователя), громкий отказ при
несовместимом state (пересмотр правила спеки), граница runbook «использование,
не устройство». Режим ревью — гейт.
- **08** (миграция курса, `ready-for-agent`) — после 07; `make data` вплетён во
все уроки 00–05, объём больше, чем кажется; тонкое место — урок 00.
- **11** (глаголы/длительность/профили, `ready-for-agent`) — после 07, до 12.
- **12** (DAG-пульт, `needs-triage`) — дооформить после 07 и 11; приоритет
поднят пользователем: будущий основной человеческий интерфейс стенда.
- **13** (доливка, `needs-triage`) — без приоритета, после фикса 09.
- Перестановка: бывший 07 (миграция) и 08 (артефакт) поменяны местами.
## Следующий шаг
Весь бэклог готов к передаче Codex. Порядок пересмотрен (решение 2026-07-04,
«пульт вперёд»): узкое место — время человека на ручную приёмку, поэтому
основная цепочка теперь 07 -> 11 -> 12 (пульт появляется раньше), а миграция
курса 08 идёт последней — её приёмку (проход по урокам) человек ведёт уже через
пульт. 09 и 10 независимы, можно параллельно; 13 — после фикса 09. Граф и
подробности — в PRD фичи, раздел «Задачи»; там же зафиксировано, что номер
файла — идентификатор, а не порядок.
## Дооформление 09 и 10 — что изменилось против прежнего понимания
Сделано в этот же день, вторым заходом. 09 и 10 переведены в `ready-for-agent`;
детали — в самих issue, здесь только сдвиги в понимании:
- **Гипотеза по 09 не подтвердилась.** Дело не в рассинхроне `event_index`, а в
источнике фактуры: при рождении визита браузерные строки берутся у случайного
«донора» из словаря, при восстановлении — у `seed_click_id` пользователя.
Прежняя развилка «выбор по абсолютному индексу, не трогая схему state»
оказалась нерабочей — restore не знает донора. Решение пользователя: хранить
`base_click_id` донора в state (альтернативы и причины отказа — в issue).
- **Дефект 09 шире браузера** (нашло ревью): per-event поля location (referer,
utm) расходятся на стыке так же — критерии приёмки расширены, иначе частичный
фикс «только браузер» прошёл бы приёмку. Расхождение event_id (uuid4 при
рождении, uuid5 при restore) зафиксировано как известное и вне скоупа.
- **По 10 сняты две мины:** пересборка по умолчанию даёт 6 часов истории, а не
2 суток (профиль «2 суток одной командой» — это ещё не сделанная задача 11);
legacy-виз `world_map`, похоже, вообще не умеет легенду и tooltip — основной
путь, вероятно, смена типа визуализации, а не настройка (проверить через
Context7 при реализации).
- **Процессная заметка:** дооформленные issue прогнаны через тройное ревью
(самопроверка + два свежих агента, адверсарно, с проверкой каждого утверждения
по коду). Улов оправдал затраты — см. пункты выше; для issue с режимом «гейт»
так стоит делать и дальше.
## Не забыть (вне бэклога)
Открытые пробелы независимого ревью от 2026-06-14 (см. handoff
`2026-06-14-generator-model-time-verification-review.md`): ×K не гоняли,
live-crash recovery не воспроизводили, распределения не сверяли с коридорами
мат-спеки, воспроизводимость checksum не перепроверяли.
## Suggested skills
- `conventional-commits` — перед коммитами.
- `tdd` — для фикса issue 09 (тест на однородность браузера через стык, затем
минимальная правка).
- `coordinator-loop` — на цепочки 07 -> 08 и 07 -> 11 -> 12.
@@ -1,185 +0,0 @@
# Handoff: цепочка генератора 12 -> 14 -> 09 -> 10 -> 08
Дата: 2026-07-04.
Жанр: одноразовый handoff по ADR-0003.
Источник: `.scratch/generator-model-time-startup-history/coordinator-journal.md`.
## Статус
Цепочка `12 -> 14 -> 09 -> 10 -> 08` завершена через `coordinator-loop`.
Финальный chain review закрыт `APPROVED`; правки финального review
закоммичены в `f8b419d`.
Все issue текущей очереди имеют `Status: done`; в PRD фичи осталась только
дальнейшая задача 13 (`needs-triage`).
## Коммиты
- `dd4af82``feat(airflow): добавлен пульт управления генератором`.
- `200bb82``docs(issues): закрыта задача 12 по пульту генератора`.
- `9d1bcc4``feat(generator): ускорен учебный профиль daily-wave`.
- `0cbfe9b``docs(issues): закрыта задача 14 по быстрому профилю`.
- `e2d0684``fix(generator): сохранена фактура визита при восстановлении`.
- `0f435d7``docs(issues): закрыта задача 09 по фактуре визита`.
- `0c80e24``fix(superset): заменена нечитаемая гео-карта`.
- `de938ed``docs(issues): закрыта задача 10 по гео-графику`.
- `d76c036``docs(course): переведены уроки на стартовую историю`.
- `3d4e13c``docs(issues): закрыта задача 08 по миграции курса`.
- `f8b419d``docs(generator): закрыты находки финального ревью цепочки`.
## Что построено
- `airflow/dags/generator_control_dag.py` — ручной Airflow-пульт
`backfill/import/check`, без Docker socket, с ожиданием `etl_pipeline`.
- `generator/src/clickstream_generator/airflow_control.py` — общая логика
env, предпроверок чистого стенда и сверки manifest для DAG-пульта.
- `docker-compose.yml` / `Makefile``make up` не автостартует live-генератор;
live запускается явно через `make generator-continue`.
- `generator/src/clickstream_generator/launch.py``daily-wave` переведён на
`speed=60`, `tick=1`; `ci` сохранён.
- `generator/src/clickstream_generator/runtime.py` и `state.py` — state v3
хранит `base_click_id`, восстановление визита берёт ту же per-event фактуру.
- `scripts/check_generated_analytics.sh` — стыковочная проверка browser/source и ODS
device/os/geo включается в режиме `auto` после live-строк.
- `superset/create_dashboard.py` — гео-блок заменён на `Top Countries by Events`
(`echarts_timeseries_bar`) с tooltip, легендой и единицами.
- `docs/course/` и `docs/TEST_PLAN.md` — учебный путь переведён с архивного
сида на `startup-history/backfill -> Kafka -> STG -> ODS -> DDS -> DM -> Superset`.
## Ревью по issue
- Issue 12: обычный режим. Worker `medium` был запущен до уточнения уровней,
проверка кода сначала `medium`, повторный прогон `high`. Первая проверка нашла
отсутствие подсказки `make clean` на грязных Kafka-топиках; повторный прогон
`APPROVED`.
- Issue 14: обычный режим. Worker `medium`, проверка кода `high`; `APPROVED`.
- Issue 09: гейт. Worker `high`; проверка задачи `high` и проверка кода `high`.
Первый круг нашёл ODS-проверку device/os/geo, потерю 1 мкс в timestamp offset
и ложнозелёный `generated-history-check`; второй круг `APPROVED`.
- Issue 10: обычный режим. Worker `medium`, проверка кода `high`.
Проверка нашла рассинхрон Superset export/layout, ложнозелёный тест и риск дубля
при rename; узкие повторные прогоны закрыты, итог `APPROVED`.
- Issue 08: обычный режим. Worker `medium`, проверка кода `high`.
Проверка нашла старый путь отката в стандарте уроков и несогласованность урока 06
вокруг `superset-dashboard`; повторный прогон `APPROVED`.
## Финальное ревью
Финальное ревью цепочки шло на усиленном уровне `xhigh`.
До обновления скилла повторные прогоны запускались свежими сессиями ревьюеров:
`Bernoulli`, `Noether`, `Banach`, `Curie`, `Sartre`, `Bacon`.
Это отклонение от обновлённого процесса зафиксировано как дорогой вариант.
После обновления скилла проектные doc-находки чинил отдельный worker для правок
`Raman` (`medium`), а повторный прогон выполнял тот же ревьюер `James` через
`send_input`.
Закрытые классы находок:
- PRD фичи больше не `Draft` и не держит закрытые 12/09/10/14/08 в остатке.
- Все `Status: done` issue фичи без пустых `[ ]`.
- Superset docs/course/artifacts/code не содержат старые `World Map/world_map`
маркеры рядом с новым `Top Countries`.
- `docs/OPERATIONS.md`, `docs/course/PRD.md`, `docs/ARCHITECTURE.md`,
`docs/REPO_MAP.md` и `CONTEXT.md` согласованы со startup-history-путём и
`generator_control`.
## Отклонения процесса
- Ранние повторные прогоны ревью запускались свежими субагентами, а не продолжением
того же ревьюера. После обновления `coordinator-loop` точечные повторные прогоны
нужно делать через `send_input` тому же ревьюеру.
- До обновления скилла координатор сам правил часть project docs по финальному
ревью. После обновления project docs правил worker для правок; процессные файлы
`.scratch/...` остались зоной координатора.
- Визуальная приёмка Superset и живой проход Airflow UI не выполнялись в этой
цепочке. Это не скрыто: все такие пункты вынесены в HITL/риски.
## Открытые риски
- Нужен кросс-линейный ревью-проход другой родословной по фактическим изменениям
`c672ed0..HEAD`. Особый фокус: issue 12 Airflow-пульт, issue 09 state v3 и
стыковочная проверка, issue 10 Superset export/layout, issue 08 учебный путь.
- Нужна ручная HITL-приёмка:
- Airflow UI: `generator_control` backfill/import/check на чистом стенде.
- Superset: кадр `Top Countries by Events` целиком и кадр с tooltip.
- Курс 00-06: проход через новый startup-history-путь.
- Задача 13 (`backfill-top-up-from-snapshot`) остаётся `needs-triage`; после
фикса issue 09 её можно дооформлять отдельно.
## Быстрые проверки, которые уже проходили
- `uv run --with-requirements generator/requirements.txt pytest generator/tests -q`
— PASS на issue 12 и 09.
- `make generator-test` — PASS на issue 14.
- `uv run --with pytest pytest tests/test_superset_dashboard_config.py` — PASS
на issue 10.
- Поисковые проверки курса на старые `make data`/`kafka_load`/фиксированные
сид-ожидания — PASS на issue 08.
- Финальные `rg`-проверки по PRD, чекбоксам, Superset-маркерам и
startup-history-дрифту — PASS перед `f8b419d`.
## Независимый ревью-проход (2026-07-04, свежий взгляд)
Кто и как: Claude, свежим взглядом по диапазону `c672ed0..HEAD`, без субагентов
(правило проекта: «свежим взглядом» = без субагентов). Читались реальные диффы
всех пяти задач и сверялись с текстами issue; тесты прогонялись заново, а не со
слов handoff.
Вердикт: цепочка выполнена на уровне кода и тестов; правок для приёмки не нужно.
Доказательства (прогонял сам):
- `generator/tests` — 172 passed (в том числе контрактный тест DAG и тесты пульта).
- `tests/test_superset_dashboard_config.py` — 5 passed.
- Грепы: старых маркеров `world_map`/`Geography Map` в коде и доках нет (остались
только законный историч. след в спеке, `previous_slice_names` и тест
переименования); во всех done-issue 0 пустых чекбоксов; `state v2` в отгруженном
коде не осталось.
- `etl_pipeline` действительно принимает `conf` `full_refresh` (`Param(True,
type="boolean")`) — имя параметра из `TriggerDagRunOperator` пульта совпадает.
По задачам:
- Issue 09 (гейт): донор `base_click_id` хранится в state; восстановление громко
падает на неизвестном доноре (тихие `.get`-fallback'и убраны); запасная ветка
рождения сделана восстановимой и провалидирована. Отдельная реальная находка —
µs-фикс `_timestamp_to_state_offset` (целочисленная арифметика вместо
`total_seconds()*1e6`). Тесты бьют прямо в критерии приёмки.
- Issue 12 (пульт): `retries=0`, `is_paused_upon_creation`, join после ветвления
через `NONE_FAILED_MIN_ONE_SUCCESS`, `wait_for_completion=True`, env до
`Config()`, предпроверка пустоты Kafka и STG. Compose-профиль `live-generator`
оставляет `make up` чистым; явные пути включают профиль по имени сервиса.
- Issue 14: `daily-wave` speed 1→60, tick 60→1; `ci` не тронут; пер-тиковые INFO
приглушены до DEBUG.
- Issue 10: `echarts_timeseries_bar` с tooltip, легендой и единицами; дедуп дублей
при переименовании; экспорт-JSON защищён тестами и от рассинхрона с конфигом, и
от дрейфа раскладки.
- Issue 08: доки, старый путь `make data`/`kafka_load` убран как основной, грепы
чисты.
Мелочи (косметика, не блокеры):
- `test_default_version` (`test_state.py`): docstring говорит «версии 2», а ассерт
уже `"3.0"`.
- `build_control_env` для `import` тоже зовёт `build_launch_env("backfill", …)`
безвредно (важен только «мир»-env), но может смутить читателя.
- `tooltipTimeFormat: "smart_date"` на категориальной оси — рудиментарный
параметр, безвреден.
## Открытые риски (обновление 2026-07-04)
- Кросс-линейный проход *другой родословной* (иная модель/агент) не выполнялся:
этот ревью сделан Claude свежим взглядом без субагентов. Для кода и тестов
этого достаточно; если нужна ещё одна родословная — отдельный шаг.
- Ручная приёмка (HITL) остаётся, код к ней честен (пропуски печатаются):
- Airflow UI: `generator_control` backfill/import/check на чистом стенде.
- Superset: кадр `Top Countries by Events` целиком и кадр с tooltip.
- Курс 00-06: проход по новому startup-history-пути.
- Задача 13 (`backfill-top-up-from-snapshot`) — `needs-triage`; предпосылка
(исправный restore из issue 09) выполнена, можно дооформлять.
## Следующий шаг
Независимый ревью кода и тестов пройден. Осталось: ручная HITL-приёмка (Airflow
UI, Superset-кадры, проход курса) и отдельное решение по задаче 13.
@@ -1,160 +0,0 @@
# Кросс-линейное ревью цепочки генератора 12 -> 14 -> 09 -> 10 -> 08
Дата: 2026-07-05 00:02.
Жанр: одноразовый handoff по ADR-0003; отчёт кросс-прохода другой родословной
(Claude) по диапазону `c672ed0..ec815ce`. Бриф — из
`.scratch/handoffs/2026-07-04-generator-chain-review.md`.
Метод: четыре параллельных адверсарных ревьюера с исполняемыми доказательствами
(мутационные тесты на копии, эксперимент с compose-профилем на реальной версии
Compose, зонды на реальном сиде, сверка Airflow API через Context7). Репозиторий
не менялся.
## Вердикт
**CHANGES_REQUESTED.** Счёт: 1 critical, 5 high, 10 medium, 4 low — после
`APPROVED` финального ревью той же линии. Третье подтверждение находки о
родословной: внутренние ревью закрыли локальные дефекты честно, кросс-линия
нашла сквозные сценарии.
## Сквозные семейства (главная ценность прохода)
1. **«Смешение миров» возвращается тремя дорогами.** Инвариант задачи 07
(«чистый стенд, громкий отказ») пробит: (а) state v2 после обновления кода
классифицируется как «повреждённый» → тихий чистый старт нового мира поверх
истории; (б) live-генератор ушёл в compose-профиль, и `make clean`/`down`
его больше **не убивают** — осиротевший генератор старого мира пишет в чистые
топики и переживает перезагрузку (`restart: unless-stopped`); (в) консольные
пути (`make generator-backfill`, `startup-history-import`) остались без
предпроверок, которые пульту сочли обязательными.
2. **Последствия «make up больше не стартует live» не выметены.** Алерт
`Kafka No Messages Produced` горит весь курс на здоровом стенде; уроки 0/5
ожидают поток; `sleep 5` в документированной проверке шва меньше времени
старта генератора — проверка стала пустышкой.
3. **Ложнозелёные проверки.** Superset-тесты пропускают дрейф параметров
(top-15 → 500 строк — 5 тестов зелёные); стыковая проверка сверяет шов там,
куда указал env, а не где он по манифесту; ODS-блок проходит на пустом ODS.
4. **Чистый клон/свежий стенд никто не прошёл.** `uv` не заявлен в
пререквизитах; README-быстрый старт падает без `ddl_init`; пульт вечно висит
на запаused `etl_pipeline`; после `git pull` образ Airflow не пересобирается
(DAG красный); центральное упражнение урока 1 физически невоспроизводимо.
Всё это лежало ровно в вынесенной в риски HITL-приёмке.
## Индекс находок по issue
### Issue 08 (курс) — CHANGES_REQUESTED
- **CRITICAL** `docs/course/lessons/01_kafka_to_clickhouse.md:192-239`
упражнение с `kafka_msg_ts` невоспроизводимо: «перезаливка среза» заменена на
`make generated-history-analytics && make up`, который делает `down -v` и
накатывает эталонный DDL — добавленная менти колонка уничтожается, финальный
SELECT падает; в тексте остался висящий `TRUNCATE` (след нестыковки).
Нужен путь перезаливки без сброса схемы (смена `kafka_group_name` /
`CLEAN_START=0` + повторный backfill) и проверка на стенде.
- **HIGH** `docs/course/README.md:19-34`, `README.md:43-47` — первая команда
курса требует `uv` на хосте; ни пререквизиты, ни quick start его не называют.
- **HIGH** `docs/course/lessons/05_monitoring.md:254,259-262,339` — на штатном
пути (без live) `Kafka No Messages Produced` горит постоянно; урок обещает
«алерты не шумят»; панели описаны для потока, которого нет.
- **MEDIUM** `00_kafka_intro.md:56-65` — служебные топики `generator_state` и
`generator_startup_history_manifest` не объяснены (урок построен на
разглядывании списка топиков).
- **MEDIUM** `00_kafka_intro.md:117-122` vs `05_monitoring.md:156-161`
взаимоисключающие утверждения о видимости consumer lag; проверить на стенде.
- **MEDIUM** `README.md:64-68`, `docs/OPERATIONS.md:175-199`
`make generated-history-check` после `PROFILE=daily-wave` падает: в check
зашит диапазон ci, PROFILE не передаётся.
- **MEDIUM** `docs/OPERATIONS.md:95` — источником STG первым назван архивный
`make data`.
### Issue 12/14 (пульт, профиль) — CHANGES_REQUESTED
- **HIGH** `airflow/dags/generator_control_dag.py:227-235` +
`etl_pipeline_dag.py:263``etl_pipeline` создаётся на паузе; `trigger_etl`
(`wait_for_completion`, без `execution_timeout`) виснет навсегда,
`max_active_runs=1` блокирует повторные запуски. В Airflow 2.10 нет
`fail_when_dag_is_paused` (Context7). Нужна предпроверка «etl_pipeline не на
паузе» + шаг unpause в README/runbook.
- **HIGH** `docker-compose.yml:293-294,334`, Makefile (`down`, `clean`) —
профильный `live-generator` не удаляется `down -v` (доказано экспериментом,
Compose v5.1.3); `restart: unless-stopped` воскрешает его после перезагрузки.
В `down`/`clean`/CLEAN_START нужен `--profile live-generator`.
- **MEDIUM** `README.md:32-40` — быстрый старт без `ddl_init`: precheck падает
сырым `UNKNOWN_TABLE`; добавить шаг и человекочитаемую подсказку.
- **MEDIUM** `airflow/requirements.txt:9-10`, Makefile `up` — после `git pull`
образ Airflow не пересобирается → `ModuleNotFoundError: prometheus_client`,
DAG красный. Нужен `--build` или инструкция.
- **MEDIUM** `docs/OPERATIONS.md:237-241` vs `kafka_io.py:161,253``sleep 5`
меньше времени старта continue (≥10 с): live-строк ноль, `CHECK_LIVE_SEAM=auto`
молча пропускает шов. Было `sleep 130` при тике 60 с.
- **MEDIUM** `airflow_control.py:102-141` vs `run_generator.sh:56-77`,
`import_startup_history_artifact.sh` — предпроверки только в DAG; консольные
пути тихо смешивают миры. Вынести `assert_stand_clean` в общую точку входа.
- **LOW** `service.py:203-215` — простой стенда ×60: ночь = +30 модельных суток
дыры; предупредить в runbook, советовать reset/повторный импорт.
- **LOW** — фиксация Context7-проверки Airflow API только в `.scratch/`, не в
коде/доках (AGENTS.md требует); сами вызовы для 2.10.5 корректны.
### Issue 09 (state v3, фактура) — CHANGES_REQUESTED
- **HIGH** `state.py:236-241``kafka_io.py:181``service.py:100-102`
поднятие STATE_VERSION молча превращает живой стенд во «второй мир»: v2 —
читаемый, несовместимый state, но version-гейт убивает его до семантики,
`from_dict_safe` даёт `None`, сервис тихо стартует новый мир с `model_t0`
поверх истории. Сверка `manifest.state_version != state.version`
(`service.py:246`) недостижима. Спека (строки 198-204, 245-247) требует
громкий отказ; `IncompatibleStateError` уже существует — использовать его при
известной старой версии и `GEN_STATE_RESET=false`; миграционной заметки в
доках нет.
- **MEDIUM** `check_generated_analytics.sh:13,273-289,342,387-393` — проверка
стыка сверяет шов по env-дефолту (`2026-01-01T06:00`), не по манифесту: после
`daily-wave` голый `make generated-history-check` «доказывает» однородность
фальшивого шва посреди backfill. LEFT JOIN `dds.click` структурно пуст; ODS-блок
проходит на пустом ODS. Брать `model_t_end` из манифеста, требовать ненулевые
ODS-строки.
- **MEDIUM** `generation.py:196-209` — предпосылка issue «fallback не
срабатывает» неверна: срабатывает у ~8% визитов (зонд, seed=4242, N=20000);
выпуск не изменился только потому, что у всех доноров сида строки внутри
click_id идентичны — свойство нигде не зафиксировано; при регенерации сида
с вариативностью ~8% визитов поедет. Зафиксировать свойство сида или
ограничить `GEN_MAX_SESSION_EVENTS` длиной максимального донора.
- Ядро фикса крепкое (проверено исполнением): инвариант «рождение ==
восстановление» держится, RNG не потребляется при restore, микросекунды
чинятся честно, форма распределений не сместилась (сверка с `e2d0684^`).
### Issue 10 (Superset) — CHANGES_REQUESTED
- **MEDIUM** `tests/test_superset_dashboard_config.py:64-76` — тесты сверяют
подмножество параметров: мутация «top-15 по убыванию» в экспорте (row_limit
15→500, order_desc→false и др.) проходит все 5 тестов. Нужно полное сравнение
`params` config-vs-export. Классы «возврат world_map» и «рассинхрон layout»
закрыты честно (мутации падают).
- **MEDIUM** issue `10-*:51-68`, `create_dashboard.py:158-185` — приёмка «по
кадру» выполнена декларативно, чарт никем не отрисован; порядок столбцов после
pivot не гарантирован — задать `x_axis_sort: "Events, pcs"`,
`x_axis_sort_asc: false`; снять кадр при HITL.
- **MEDIUM** `create_dashboard.py:505-520` — удаление дублей по глобальному
совпадению имени: повторный прогон молча удалит одноимённый чарт менти;
сузить по `datasource_id`, задокументировать деструктивность.
- **LOW** `create_dashboard.py:176``tooltipTimeFormat` бессмыслен для
категориальной оси; легенда дублирует ось при единственной серии.
- **LOW** `docs/SUPERSET_DASHBOARD.md:205-211` — путь ручного импорта экспорта
(legacy v0) ничем не проверяется.
## Маршрутизация (предложение)
- **Багфикс-цепочка Codex** (кандидат-порядок по риску): (1) семейство
«смешение миров» — 09-HIGH + 12-HIGH-clean + разъезд предпроверок одним
issue; (2) 12-HIGH-паузы + README/ddl_init/build — «свежий стенд» одним
issue; (3) 08-CRITICAL урок 1 + uv + алерт урока 5 — «курс на чистом клоне»;
(4) ложнозелёные проверки (09-MEDIUM-шов, 10-MEDIUM-тесты); (5) остальное.
- **HITL остаётся HITL** (кадры Superset, живой проход Airflow UI, проход по
урокам) — но пункты 08-CRITICAL и 12-HIGH показывают, что часть HITL дешевле
заменить проверками на чистом клоне в CI-манере.
- Отдельно: третий случай подтверждения находки о родословной — кандидат на
датированную поправку в `references/rationale.md` скилла coordinator-loop.
## Что подтверждено как корректное
Стыковка env DAG→генератор; ветвление DAG; отсутствие потребности в Docker
изнутри Airflow; make-цели/имена DAG/DDL-колонки/числа профилей в уроках; ядро
state v3; замена карты в обоих источниках; 172 unit-теста зелёные.
@@ -1,122 +0,0 @@
# Handoff: fixes after generator cross-line review
Дата: 2026-07-05 22:12.
Жанр: handoff по ADR-0003 после coordinator-loop 15 -> 16 -> 17 -> 18.
## Что закрыто
- 15: граница миров после обновления, clean/reset и host-запусков.
Коммиты: `080d5ae`, `108053b`.
- 16: свежий стенд и `generator_control` без неявных ручных шагов.
Коммиты: `27947ce`, `9804d0b`.
- 17: доверенные проверки startup-history и Superset.
Коммиты: `00c97eb`, `6731349`.
- 18: курс 00/01/05 проходит по startup-history на чистом стенде.
Коммиты: `77e4fb6`, `6d64f39`.
- Follow-up 19: отдельная задача на единые `make test` и `make lint`.
Коммит: `2778eae`.
- Финальный review fix: документация больше не советует обычный
`make generated-history-check` после backfill-only пути.
Коммит: `8e1c733`.
- Issue 19 закрыта после внешнего Claude review: единые `make test` и
`make lint`, подключение корневых контрактных тестов и усиление проверок
startup-history.
Коммит: `9b0b063`.
## Ключевые стыки реализации
- `generator/src/clickstream_generator/stand_clean.py` — общий guard чистого
стенда для Airflow и host-путей.
- `scripts/assert_stand_clean.sh` — host-проверка перед backfill/import/continue.
- `airflow/dags/generator_control_dag.py` — pause-check `etl_pipeline` до
мутирующих веток.
- `scripts/check_generated_analytics.sh` — manifest boundary, fail-closed
live seam, непустой ODS и fail-closed проверка DM-витрин.
- `scripts/run_generated_history_runtime_check.sh` — bounded runtime gate для
daily-wave + live seam.
- `scripts/run_generator.sh``reset` теперь проходит clean-stand guard до
запуска live-генератора.
- `Makefile``make test`, `make lint`, `contract-test`; обычный pytest-вывод
тихий, подробность можно включить через `PYTEST_ARGS`.
- `generator/src/clickstream_generator/state.py` — любая версия state не `3.0`
даёт громкий `UnsupportedStateVersionError`; `from_dict_safe` больше не
глотает ошибку старой/неизвестной версии.
- `generator/src/clickstream_generator/airflow_control.py`
`target_dag_trigger_error()` покрывает три ветки pause-check без импорта
Airflow.
- `superset/create_dashboard.py` — sync ограничен чартами целевого dashboard.
- `docs/course/lessons/00_kafka_intro.md`,
`docs/course/lessons/01_kafka_to_clickhouse.md`,
`docs/course/lessons/05_monitoring.md` — курс согласован с startup-history,
no-live default и consumer lag.
## Проверки
- Issue 15: `make generator-test`, `docker compose config --quiet`,
`py_compile`, `bash -n`.
- Issue 16: `make generator-test`, `docker compose config --quiet`,
`py_compile`, `make clean`, `make up`, `make ddl`.
- Issue 17: Docker target tests, `make generator-test`, `py_compile`,
`bash -n`, `make generated-history-runtime-check`.
- Issue 18: doc `rg`, `git diff --check`,
`make generated-history-analytics`, `make up`.
- Issue 19 / commit `9b0b063`: `make contract-test` — 16 passed;
`make generator-test` — 190 passed; `make lint` — passed; `make test`
190 generator tests, 16 contract tests, `docker compose config --quiet`.
- Финальный chain review `Gibbs`: initial `CHANGES_REQUESTED`, narrow rerun
`APPROVED`.
## Происхождение ревью
- 15: gate; task reviewer `Ptolemy` high, code reviewer `Linnaeus` high;
оба rerun `APPROVED`.
- 16: code reviewer `Laplace` high; primary review found paused-DAG order bug,
later reruns `APPROVED`.
- 17: code reviewer `Bacon` high; found Superset ownership and live-seam
defaults; bounded runtime gate rerun `APPROVED`.
- 18: docs/code reviewer `Hume` high; found Prometheus scrape-count drift;
rerun `APPROVED`.
- Финал: chain reviewer `Gibbs` xhigh; found docs drift around
`generated-history-check`; rerun `APPROVED`.
- После `9b0b063`: внешний Claude review подтвердил закрытие всех девяти
находок и дал `APPROVED`. Reviewer не перезапускал Docker-проверки сам, но
сверил код, тесты и документы; фактические прогоны были выполнены
координатором.
## Отклонения процесса
- Локальный `uv run ... pytest` у координатора падал до pytest из-за
`snap-confine` / `cap_dac_override`. Для координаторского gate целевые тесты
запускались через Docker-образ `generator:test`. Это вынесено в issue 19.
- Первичный runtime gate issue 17 через полный `daily-wave` был остановлен:
прогон был активным, но слишком долгим для коммит-гейта. После этого добавлен
bounded `make generated-history-runtime-check`.
- Несколько стендовых проверок чистили Docker volumes. Это ожидаемо для issue
16-18 и зафиксировано в журнале.
## Что вручную проверить дальше
- Чистый ручной путь через Airflow UI: `make up`, `make ddl` или `ddl_init`,
`airflow dags unpause etl_pipeline`, затем `generator_control` backfill.
- Backfill-only путь: `make generated-history-analytics`, затем
`CHECK_LIVE_SEAM=0 make generated-history-check`.
- Стык backfill/live: `make generated-history-runtime-check`.
- Superset: после готового DM открыть dashboard и сверить, что чарты созданы и
`Top Countries by Events` читается.
- Kafka UI: в уроке 00 должны быть понятны четыре data-топика и служебные
`generator_state`, `generator_startup_history_manifest`,
`generator_batch_history`.
## Suggested skills
- `triage` — если продолжать issue 13: он всё ещё `needs-triage`.
- `brainstorm-with-docs` — если перед issue 13 нужно заново согласовать
модель доливки истории от слепка.
- `coordinator-loop` — только после перевода issue 13 в `ready-for-agent`.
- `code-review` — для внешнего прохода по коммитам после ручной проверки.
## Открытые риски
- Ручная HITL-проверка после `9b0b063` ещё не выполнена пользователем.
- Issue 13 остаётся `needs-triage`: доливка истории от слепка должна опираться
на закрытые инварианты 15 и 17.
@@ -1,70 +0,0 @@
# Handoff: проверка пути менти, диагноз флаки-гейта, задача next-day
Дата: 2026-07-07 22:34.
Жанр: handoff по ADR-0003 после сессии «проверка пути менти + триаж 13/20».
## Что закрыто
- Проверка пути менти двумя субагентами (читатель документации на Opus,
прогонщик стенда на Sonnet):
- Документация: workable-with-friction, четыре мелких находки — все
исправлены. Коммит `900e949`.
- Стенд с нуля: clean -> generated-history-analytics -> up -> проверки ->
Superset (чарты с данными) -> Kafka (4 data-топика + служебные) ->
ClickHouse (ODS/DM непустые) — всё прошло, дубликатов ноль.
- Единственное падение: `make generated-history-runtime-check` красный
через раз — заведена задача 20.
- Диагноз задачи 20 (Codex, ключевой факт перепроверен по коду): генератор
фактуру на стыке НЕ ломает; корень — гонка остановки live-генератора в
самой проверке + seam-SQL, маскирующий непарные строки под «смену фактуры».
Детали и направление фикса — в задаче. Коммит `98be722`.
- Задача 13 переписана под глагол `next-day` в `generator_control`;
оба открытых вопроса закрыты; развилка реализации решена пользователем:
**вариант 2 — генерация следующего дня от слепка** (обоснование в задаче).
Коммиты `a3ac4b4`, `98be722`; решение развилки — в коммите с этим handoff.
- Созданы репо-локальные субагенты `.claude/agents/deep-reasoner.md` (Opus)
и `fast-worker.md` (Sonnet); `.gitignore` скрывает локальное в `.claude/`.
Коммит `984d6fc`.
## Состояние трекера
- `20-flaky-runtime-seam-check.md`**ready-for-agent**: диагноз есть,
направление фикса выбрано (управляемая остановка live после полного flush
всех топиков + precheck непарных строк в seam-check). Идти первым.
- `13-backfill-top-up-from-snapshot.md`**ready-for-agent**, blocked by 20.
Вариант реализации решён, критерии в задаче.
## Что дальше
1. Фикс задачи 20 по конвейеру `/claude-subagent-playbook`: постановка в
задаче самодостаточна, свежий адверсарный взгляд на неё до реализации,
реализация Кодексом (прямой `codex exec`, не плагин — см. handoff в
скилле), два слепых ревью.
2. После зелёного стабильного гейта (3+ прогона) — задача 13.
3. Ручная HITL-проверка пути менти пользователем всё ещё не сделана
(Airflow UI, Superset глазами, Kafka UI) — стенд остался в рабочем
состоянии после зелёного runtime-check.
## Проверки этой сессии
- `make generated-history-runtime-check`: красный (8/19), затем зелёный —
эмпирика флаки в задаче 20; полные логи в session-scratchpad (одноразовые,
сводка перенесена в задачу).
- Утверждение диагноза про запасную ветку перепроверено по
`generation.py:194-209`.
- Дерево чистое после каждого коммита; `.claude/settings.local.json`
в git не попал.
## Отклонения процесса
- Прогонщик стенда дважды засыпал без отслеживаемого фонового процесса и
в финале умер, не собрав отчёт, — вердикт собран координатором из логов.
- Диагност через плагин codex-rescue: форвардер отчитался «finished» при
живом фоновом вызове; результат забран напрямую из output-файла задания.
Уроки процесса — в handoff рядом со скиллом claude-subagent-playbook.
## Suggested skills
- `claude-subagent-playbook` — для фикса задачи 20 (одна нетривиальная
задача: постановка готова, нужен конвейер).
- `conventional-commits` — коммиты.
@@ -1,82 +0,0 @@
# Handoff: задача 13 закрыта конвейером, дальше — задача про расписание и HITL
Дата: 2026-07-12 23:59.
Жанр: handoff по ADR-0003 после сессии «задача 13 (глагол next-day) по
/claude-subagent-playbook».
## Что закрыто
- **Задача 13 — done.** Полный конвейер плейбука: двойное слепое ревью
постановки (оба CHANGES_REQUIRED, 9 решений внесены, коммит `540989d`)
-> реализация Codex (`gpt-5.6-sol`, high, тред
`019f57d2-ff7b-70d3-8143-3740324309d9`) -> два слепых ревью кода
(Codex: CHANGES_REQUIRED 3 MAJOR; Claude: APPROVED_WITH_MINORS) ->
триаж в 2 круга + узкий диагноз нулевого стыка -> перепроверки
ALL_CLOSED у обоих -> моя приёмка. Коммит `f5dee26`.
- Суть решений, диагноз нулевого стыка (естественный ночной провал, не
дефект restore) и аргументы отклонений — в самой задаче
`.scratch/generator-model-time-startup-history/issues/13-*.md`, здесь
не дублируются.
- Приёмка своими глазами: `make test` 204+31; `generated-history-chain-check`
зелёный (граница 1: crossing=1, граница 2: crossing=0 с явным статусом,
непарные — нули); регрессия `generated-history-runtime-check` задачи 20
зелёная (19 переходящих визитов). Учебный цикл: DM 322 -> 10 026 ->
19 196 за два next-day.
## Состояние
- Ветка `feature/data-generator`, дерево чистое после `f5dee26`
(+ handoff-коммит следом), запушено в origin.
- Стенд поднят, в мире 3 модельных дня (артефакт + два next-day от
прогонов исполнителя); live-генератор остановлен.
- Модель Codex — `gpt-5.6-sol` (high для реализации/ревью; xhigh не
окупается). Перед каждым фоновым запуском — сообщать пользователю, что
запускается и какой этап.
## Что дальше
1. **Завести задачу про расписание** («имитация жизни»): включение
`next-day` по расписанию. Обязана решить: как расписание задаёт
`operation=next-day` (дефолт DAG — `backfill`, планового запуска с
дефолтом быть не должно) и retention/формат накопительного состояния
манифеста (см. отклонение «полная перечитка Kafka» в задаче 13 —
оно блокирует её по записи в тикете).
2. **Ручная HITL-проверка пути менти** (Airflow UI, Superset глазами,
Kafka UI) — переносится пятый handoff подряд; исполнитель задачи 13
отметил, что Airflow UI руками не гонял (проводка next-day проверена
контрактными тестами и реальными запусками через `run_batch.sh`).
3. **Внести уроки в плейбук**: рефлексия по расходу контекста мастера
проведена в конце сессии 2026-07-12/13 (см. итоговое сообщение) —
согласовать с пользователем и записать в
`~/.claude/skills/claude-subagent-playbook/references/lessons.md`.
Ключевые кандидаты: ограничить раздел confirmations у ревьюеров
(одна строка на пункт); самый большой неконтролируемый расход —
эхо всего файла задачи в контекст мастера, когда исполнитель правит
файл, который мастер недавно писал сам.
## Проверки этой сессии
- `make test`: 204 (генератор) + 31 (контракты корня) — прогнаны мной,
исполнителем и ревьюерами независимо.
- Стенд: chain-check и runtime-check зелёные (лог приёмки одноразовый в
session-scratchpad, сводка — в задаче 13).
- Дерево сверялось с baseline после каждого ревью/перепроверки —
ревьюеры исходники не трогали.
## Отклонения процесса
- Перепроверка Codex принесла новую находку (MAJOR про микросекунды) —
это не нарушение узости, штатный случай из prompt-templates.md;
закрыта вторым кругом триажа тем же тредом исполнителя.
- Требование «громкий отказ на нулевом стыке» из обоих ревью покраснело
на честном стенде; решён диагнозом по данным (ветка «естественный
провал») и сменой семантики на явный статус — класс «артефакт
верификатора», как в задаче 20.
## Suggested skills
- `claude-subagent-playbook` — задача про расписание тем же конвейером
(постановку писать с нуля — этапы 1–2).
- `conventional-commits` — коммиты.
- `research` — если для задачи про расписание понадобится фактура по
Airflow (params при scheduled runs) — через Context7.
@@ -1,103 +0,0 @@
# Handoff: ручной HITL пути менти пройден — вырос в набросок редизайна
Дата: 2026-07-19 21:44.
Жанр: handoff по ADR-0003 после сессии «ручная HITL-проверка пути менти»
(долг переносился пятый handoff подряд — закрыт).
## Что сделано
- **HITL-проверка пути менти пройдена целиком, своими руками через UI.**
Чистый стенд (`make clean` + `make up`) -> Airflow UI: `ddl_init` ->
`generator_control` `backfill` (профиль `ci`) -> ETL -> `make superset-init`
-> дашборд глазами -> `generator_control` `next-day` -> проверка стыка.
Каждый шаг сверен по данным (ClickHouse, manifest, chain-check).
- **Результат мира:** backfill `[00:00, 06:00)` = 16 054 события; после
`next-day` = 110 602 события, визитов 10 228, пользователей 1 726;
`boundaries` = три границы, `model_t_end` = `2026-01-02 06:00`.
`generated-history-chain-check` — EXIT 0 (стык 06:00: 9 переходящих
визитов, непарных 0, однородность подтверждена).
- **9 находок + рамочная модель** — в
`.scratch/generator-model-time-startup-history/hitl-findings.md`.
Половина находок родилась из наблюдений менти. HITL из «проверить, что
не сломалось» перерос в **набросок редизайна пути менти**.
## Ключевые выводы (кратко; подробно — в hitl-findings.md)
- **Рамочная модель менти:** основной режим — `import` артефакта (база),
дальше две ветки от одного состояния: `next-day` (пакетно) и `continue`
(живой поток). Это три разных лабораторных. `next-day` и `continue`
не схлопывать. Подтверждено `launch.py:117-137`.
- **F7 + замер:** опыт на 6h беден; нужен готовый артефакт из файла.
Артефакт 6h = 49 МБ; `xz -9e` -> 1,1 МБ (48x, 13,5 с). На 3 дня ~13 МБ
— кладётся в обычный git. Паттерн-образец: `airflow-greenplum-solution`
(`bookings/seed/demo.sql.xz`).
- **Решение менти:** целевой стартовый мир — **3 дня на `daily-wave`**
(обсуждаемо). На `ci` три дня плоские — волну даёт `daily-wave`.
- **F8:** два профиля путают. Свести к одному учебному (волна, ×60);
`ci` (×1) — служебный для тестов. ×60 оправдан live-лабой (`continue`).
- **F9:** `next-day` растёт O(N²) из-за полной перечитки Kafka для
счётчиков (`service.py:591`). Параллелить не то (ломает детерминизм);
правильно — инкрементальные счётчики (O(N), снимает риск OOM). Это и
есть решение «новый формат состояния manifest», блокирующее расписание.
- **F1, F5 (баги UI):** форма `generator_control` держит необязательные
поля обязательными (`type="string"` без `null`); Airflow и Superset
быстро разлогинивают (ротацию секрета исключил, корень TBD).
## Состояние
- Ветка `feature/data-generator`. После этого handoff в дереве останутся:
`hitl-findings.md` + этот handoff (коммитятся), плюс **не коммитить**
`data/ci_backfill.json` (49 МБ, побочный от backfill; `.gitignore` его
не покрывает — риск, см. ниже).
- Стенд **оставлен поднятым** по просьбе пользователя; в мире 1,25 суток
(профиль `ci`), live-генератор не запущен. Выросший мир можно смотреть
в Superset (dashboard id 1).
## Что дальше
1. **Разложить находки в постановки по плейбуку** (этапы 1-2, двойное
слепое ревью). Ориентировочно ~4 задачи (номера 21+ в `issues/`):
- **Ядро:** эталонный 3-дневный артефакт (`daily-wave`) как база
менти через `import`; формат/сжатие (`xz`, нужен ли `raw_topics`);
место хранения (git после отладки сида). Снимает F7.
- **Профиль:** один учебный профиль, `ci` -> служебный тестовый.
Перед слиянием проверить, не зависит ли live-тест от скорости ×1
(F8).
- **Расписание + retention:** беспараметрный DAG `next-day` (F1/F3),
`schedule`, инкрементальные счётчики manifest (F9/F6). Это одна
связка — их корень общий.
- **Баг-фиксы:** F1 (форма) и F5 (разлогин); F4 (JS-баг Grid) низкий.
2. **Три режима -> свои уроки/лабы** — вход в учебный контент
(`docs/course/`). Под каждую ветку отдельная лаба: `next-day`
(пакетная инкрементальная обработка) и `continue` (потоковый приём);
общая база `import` — их совместное начало. Отдельно от
инфраструктурных задач.
3. **Мелочь по гигиене:** `data/*.json` не покрыт `.gitignore`
побочные артефакты backfill (49 МБ) рискуют попасть в коммит. Решить:
правило в `.gitignore` или чистить артефакты после прогонов.
## Проверки этой сессии
- `ddl_init`: 4 слоя созданы (stg 12 / ods 8 / dds 2 / dm 6 таблиц).
- Форма мира после backfill: пирамида 445 < 1516 < 16 054, интервал
`[00:00, 06:00)` точный.
- После `next-day`: 3 границы в manifest, мир до `2026-01-02 06:00`.
- `generated-history-chain-check`: EXIT 0.
- Длительность `next-day` (метаданные Airflow): задача `run_next_day`
638 с (~10,6 мин); ETL `trigger_etl` 30 с — узкое место в генерации.
- Замер сжатия: `xz -9e` 48x за 13,5 с; gzip -9 17x.
## Отклонения процесса
- Ревью субагентами не запускали: пользователь просил «свежий взгляд»,
что по договорённости = без субагентов. Проверка проведена мной руками.
- Обход бага F1: форму backfill/next-day заполняли значениями профиля
`ci` целиком (пустые необязательные поля UI не пропускает).
## Suggested skills
- `claude-subagent-playbook` — разложить находки в постановки тем же
конвейером (этапы 1-2 с нуля).
- `conventional-commits` — коммиты.
- `research` / Context7 — для задачи про профиль/форму (Airflow Param
`type=["null","string"]`) и про формат артефакта.
@@ -0,0 +1,89 @@
# Handoff: старт редизайна пути менти — спека и пилот GitHub Issues
Дата: 2026-07-19 22:29.
Жанр: handoff по ADR-0003 — точка входа в новую фичу на чистой ветке.
## Контекст
- `feature/data-generator` (146 коммитов) влита в main (`0e312b3`) и
запушена. Перед слиянием закрыта задача 21: баг F1 (необязательные поля
формы `generator_control` требовались как обязательные) починен через
`Param(None, type=["null", "string"])`, `.gitignore` покрыл артефакты
`data/*.json`, доки (`ARCHITECTURE.md`, `REPO_MAP.md`) догнали код.
`make test` (205 + 31) и `make lint` зелёные.
- `.scratch` очищен целиком (решение пользователя 2026-07-19): задачи
01–21, PRD, журналы и старые handoff'ы — одноразовые материалы, всё
долговечное перенесено в `docs/` (ADR 00040006, спеки, runbook
`startup-history`). История доступна в git: см. состояние на `0e312b3`,
ключевой файл находок —
`.scratch/generator-model-time-startup-history/hitl-findings.md@0e312b3`.
## Направление работ: редизайн пути менти
Рамочная модель (из ручного HITL 2026-07-19, подтверждена по
`launch.py`): основной режим менти — **`import` готового артефакта**
(база), от одного восстановленного состояния две ветки-лабы:
**`next-day`** (пакетно добавить сутки) и **`continue`** (живой поток,
оправдывает скорость ×60). `backfill` в целевой модели — инструмент
мейнтейнера для сборки артефакта, не первый шаг менти.
Ориентировочно 4 задачи:
1. **Ядро: эталонный 3-дневный артефакт** на профиле `daily-wave` как
база менти через `import`. Факты: артефакт 6h = 49 МБ raw; `xz -9e`
жмёт 48× (1,1 МБ за 13,5 с); 3 дня ≈ 13 МБ сжатого — кладётся в
обычный git. Паттерн-образец: `airflow-greenplum-solution`
(`bookings/seed/demo.sql.xz`). Открыто: нужен ли артефакту
`raw_topics` или хватит компактного `state`.
2. **Один учебный профиль**: `daily-wave` (волна, ×60) — учебный,
`ci` (×1, плоский) — служебный для тестов; форма не должна
подсовывать `ci` первым. Перед слиянием проверить, не зависит ли
runtime-seam-тест от скорости ×1.
3. **Расписание + инкремент**: беспараметрный DAG `next-day` (удобство
менти и пригодность к `schedule`) + инкрементальные счётчики manifest:
сейчас `next-day` перечитывает всю Kafka-историю с нуля — O(N²) по
дням и рост RAM (риск OOM); замер: день 2 = 638 с, узкое место —
генерация+пересчёт, не ETL (30 с). Параллелить нельзя (детерминизм),
правильный рычаг — накопленное состояние счётчиков в manifest/state.
4. **Баг-фиксы**: F5 — быстрый разлогин Airflow и Superset (ротация
секрета исключена; копать время жизни сессии/cookie); F4 — JS-ошибка
авто-refresh в Airflow Grid (косметика, низкий приоритет).
Отдельная линия (после инфраструктуры): три режима → свои лабы в
`docs/course/` (`import` — общая база; `next-day` — пакетная
инкрементальная обработка; `continue` — потоковый приём).
## Пилот GitHub Issues (решение пользователя 2026-07-19)
Новый процесс для этой фичи:
- Спека редизайна — файлом в репозитории (материал — hitl-findings из
истории git, см. выше).
- Тонкий корневой issue на GitHub со ссылкой на спеку и чекбоксами на
дочерние issues; дочерние — полноценные самодостаточные постановки.
- Triage-метки — пять канонических ролей из `docs/agents/triage-labels.md`
как настоящие метки GitHub.
- В том же заходе поправить контракт: AGENTS.md и
`docs/agents/issue-tracker.md` сейчас говорят «GitHub Issues не
используются» — заменить на правила пилота. Старые задачи не
мигрировать (архив в истории git).
## Что дальше (порядок)
1. Спека редизайна пути менти в `docs/specs/` (из hitl-findings).
2. Обновить контракт трекера (AGENTS.md, `docs/agents/issue-tracker.md`).
3. Корневой issue + 4 дочерних на GitHub, метки.
4. Работа по задачам конвейером `claude-subagent-playbook`.
## Состояние стенда
Стенд поднят: мир 1,25 суток на профиле `ci`, `model_t_end` =
`2026-01-02 06:00`, live-генератор не запущен, дашборд — Superset id 1.
Для новой работы, скорее всего, понадобится чистый прогон (`make clean`).
## Suggested skills
- `claude-subagent-playbook` — конвейер задач.
- `conventional-commits` — коммиты.
- `research` / Context7 — фактура для спеки (формат артефакта, Airflow
`schedule`).