docs(generator): зафиксировано независимое ревью и задачи на потом

- Зачем:
  - после coordinator-loop нужно независимое ревью результатов модельного
    времени и стартовой истории; пропущенный внешний review gate после задачи 5
    закрыт другой родословной.
- Что:
  - добавлен verification-handoff: что проверено независимо, дефект стыка
    (issue 09) и открытые пробелы (×K, crash recovery, коридоры мат-спеки,
    воспроизводимость, review gate задачи 3).
  - заведены issues 08 (портативный артефакт + runbook + идеи интерфейса),
    09 (баг браузерной фактуры на стыке), 10 (читаемость гео-карты).
  - в docs/course/PRD.md §7 — открытый вопрос «генератор как скрытая
    инфраструктура vs отдельный урок».
- Проверка:
  - git show --stat HEAD
  - чтение .scratch/handoffs/2026-06-14-generator-model-time-verification-review.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 23:03:08 +03:00
co-authored by Claude Opus 4.8
parent 86bba06f09
commit d7f02fd700
5 changed files with 321 additions and 0 deletions
@@ -0,0 +1,96 @@
Status: needs-triage
# Портативный артефакт стартовой истории и 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. Портативного файла-артефакта, который можно
сгенерировать один раз и быстро залить на чистый ClickHouse без запуска
генератора, нет.
Из-за этого неудобно: раздать готовое демо, мгновенно сбросить стенд, держать
длинную стартовую историю (2+ суток, чтобы суточная волна повторялась на графике)
без повторной генерации. Сейчас «дёшево» только live-возобновление из слепка и
перезапуск без `down -v`; полный сброс требует регенерации.
Этот пункт работает на главную цель: если стенд поднимается одной командой и есть
короткий runbook, генератор становится скрытой инфраструктурой и менти не нужно
знать его устройство. Связано с открытым вопросом курса про отдельный урок по
генератору (см. `docs/course/PRD.md`, §7).
## What to build
- Экспорт стартовой истории в портативный файл-артефакт: события плюс слепок
состояния плюс манифест — один связный набор, чтобы не смешать `GEN_SEED`,
`T0`, `T_end` и настройки генерации.
- Импорт: залить артефакт на чистый ClickHouse и Kafka без прогона генерации;
live-режим продолжает с `T_end`.
- Runbook «как пользоваться стендом на генерации»: как сгенерировать, сохранить,
восстановить, выбрать длительность стартовой истории; что дёшево
(live-возобновление, перезапуск без чистки), а что требует регенерации.
## Acceptance criteria
- [ ] Есть команда экспорта: backfill -> портативный файл-артефакт
(события + слепок + манифест).
- [ ] Есть команда импорта: артефакт -> чистый ClickHouse без запуска генератора;
контрольные числа манифеста и ClickHouse совпадают с исходной генерацией.
- [ ] Сохранено антисмешивание: импорт отвергает артефакт, несовместимый по
манифесту (`GEN_SEED`, `T0`, `T_end`, настройки генерации, версия state).
- [ ] Runbook описывает генерацию один раз, дешёвое восстановление, выбор
длительности и то, что переживает перезапуск, а что требует регенерации.
- [ ] Документы запуска (`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 его и закрывает, расширяя до полного цикла «генерация — сохранение —
восстановление».
- Связано с `07-migrate-course-from-archive-seed.md`: удобный стенд упрощает выбор
«адаптировать уроки», а не писать тяжёлый урок про генератор.
## Идеи интерфейса (на будущее, не решено)
Запуск сейчас недружелюбный: поведение собирается из ~10 связанных env-переменных,
`T_end` задаётся абсолютной меткой вместо длительности, а несовпадение настроек при
live-продолжении даёт тихий «свежий старт» (warning в лог, общее сообщение, без
указания разошедшегося поля — `service.py:217-220`). Идеи, как сделать удобнее:
- **Глаголы вместо матрицы флагов:** явные `backfill` / `continue` / `reset`, а не
комбинация `GEN_RUN_MODE` + `GEN_STATE_RESET`.
- **Длительность как длительность и профили:** `HISTORY_DURATION=2d` вместо ручного
расчёта `T_end`; именованные профили вместо повторения блока из ~10 переменных.
- **Громкий и адресный отказ при несовпадении (пересмотр решения спеки).** Сейчас
при `GEN_STATE_RESET=false` несовместимый по настройкам state молча ведёт к чистому
старту (`service.py:217-220`) — это сознательный выбор спеки ради устойчивости.
Предложение: различать два случая. Нет состояния или оно повреждено -> чистый старт
с предупреждением (как сейчас, оставить). Состояние есть и читается, но настройки
несовместимы при `GEN_STATE_RESET=false` (оператор намерен продолжить) -> **жёсткое
падение** с указанием разошедшихся полей (`seed`/`T0`/`timezone`/`speed`) и подсказкой
выставить `GEN_STATE_RESET=true`, если новый мир нужен осознанно. Меняет правило
спеки «несовместимо -> чистый старт», поэтому правка идёт вместе с обновлением
`docs/specs/2026-06-14-generator-model-time-and-startup-history.md`.
- **Доливка прошлого кусочком:** backfill, продолжающий слепок от `T_end` (сейчас
backfill всегда стартует с чистого состояния от `T0`, `service.py:178-185`).
- **Airflow DAG как пульт запуска (идея пользователя, 2026-06-14):** обернуть операции
генератора в параметризованный DAG (params: режим, `T0`, длительность, скорость,
seed) — UI, валидация настроек против манифеста до запуска, повторные попытки,
наглядность. Хорошо ложится на ограниченный backfill/доливку (конечная задача);
непрерывный live — это долгоживущий сервис compose, DAG его скорее стартует/останавливает,
чем держит внутри таска. Бонус: такой DAG сам по себе учебный (тема урока 4 —
оркестрация Airflow), что ближе к цели курса, чем устройство генератора.
## 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`
@@ -0,0 +1,73 @@
Status: needs-triage
# Браузерная фактура не сохраняется на стыке восстановления визита
## Parent
`.scratch/generator-model-time-startup-history/PRD.md`
## Что нашли
Визит, переживший стык backfill->live (активен на `T_end` и продолжается живьём),
меняет браузерную фактуру в live-продолжении. Внутри одного `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 — а он был выполнен той же
родословной, и дефект прошёл.
## Причина (гипотеза по коду)
Конкретные браузерные строки визита (`ActiveVisit.batch`) в state не сериализуются:
хранятся `seed_click_id`, `page_url_paths`, `timestamps`, `next_index` (`state.py`).
При восстановлении live-путь пересобирает фактуру
(`runtime.py._compact_visit_batch`: `browser_by_click_id[seed_click_id]`, индекс
`event_index % len`). device и geo переживают восстановление, потому что привязаны к
`UserProfile` (`user.device`, `user.geo`) и не зависят от индекса события; браузер
зависит от выравнивания `event_index` между уже выпущенной частью и восстановленной —
вероятен рассинхрон индекса. Точную точку уточнить при фиксе.
## Масштаб и важность
- Затрагивает только визиты, активные ровно на `T_end` (здесь 22 из 17397 визитов,
~0.13%). Чистый backfill и чистый live — без дефекта.
- Тот же механизм (фактура пересобирается при restore) вероятно затрагивает и
восстановление после сбоя (задача 04), не только backfill->live; проверка задачи 04
его не ловила, потому что смотрела только `dds.click`.
- Для учебного демо урон низкий, но это реальное нарушение стыковой однородности и
сигнал, что браузерная фактура не входит в восстановимое состояние визита.
## Направление фикса
- Либо сериализовать браузерную фактуру визита в state рядом с
`page_url_paths`/`timestamps`, чтобы восстановленная часть точно продолжила
выпущенную.
- Либо выбирать браузер по абсолютному `event_index` визита, а не по позиции в
остатке, чтобы restore воспроизводил те же строки.
- В проверку стыка добавить per-event браузерные поля (`dds.event`), а не только
`dds.click`, чтобы регресс ловился автоматически.
## Blocked by
- Нет. Это bug по результатам независимой проверки задач 05 и 06.
@@ -0,0 +1,47 @@
Status: needs-triage
# Гео-карта на дашборде нечитаема
## Parent
`.scratch/generator-model-time-startup-history/PRD.md` (раздел «Финальная ручная
приёмка»: если панелей не хватает для просмотра — отдельная задача на панель).
## Что нашли
На финальной ручной приёмке (шаг 2 спеки) виджет «🗺️ Geography Map» показывает
что-то, но прочитать нельзя:
- нет всплывающих подсказок (tooltip) — навёл на страну, значения не видно;
- нет легенды и подписи шкалы — непонятно, что кодирует цвет и в каких единицах;
- цветовая шкала неочевидна — подсвечено мало стран, остальное пусто.
Виз отрисовывается, но смысл непрозрачен: нельзя сказать, что именно он показывает.
## Две разные причины (не путать)
1. **Читаемость визуализации (эта задача).** Настройка чарта: включить tooltip,
добавить легенду и подпись метрики/единиц, выбрать понятную цветовую шкалу,
при необходимости — заменить мировую choropleth на более читаемую форму
(например, топ-N стран баром), раз распределение сильно разрежено.
2. **Перекос гео-данных (отдельно, ADR-0006).** Сама гео-фактура берётся из
статического сида (`geo_by_click_id`) с перекошенным распределением стран. Это
«временная опора на сид» до синтеза фактуры — лечится не настройкой чарта, а
генерацией гео. Здесь не решаем, только отмечаем как смежную причину.
## Acceptance criteria
- [ ] У гео-виза есть tooltip со значением по стране.
- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется цветом.
- [ ] Цветовая шкала читаема на текущем (разреженном) распределении, либо выбран
более подходящий тип визуализации.
- [ ] Зафиксировано, что перекос распределения стран — это вопрос гео-фактуры
(ADR-0006), а не настройки чарта.
## Notes
- Всплыло на просмотре дашборда на данных генерации (2 суток backfill).
- Смежные документы: `superset/create_dashboard.py` (определение чарта),
`docs/SUPERSET_DASHBOARD.md`.
- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и
подсказки у других виджетов), но базово задача — про гео-карту.
@@ -0,0 +1,96 @@
# 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).
+9
View File
@@ -144,3 +144,12 @@ Kafka → ClickHouse → BI).
- Переиспользование: после **первого реального прогона менти** — ретроспектива и - Переиспользование: после **первого реального прогона менти** — ретроспектива и
обобщение материала под других менти. Это валидация уже собранного курса, а не обобщение материала под других менти. Это валидация уже собранного курса, а не
часть его подготовки. часть его подготовки.
- Генератор как инфраструктура против отдельного урока (открыто, 2026-06-14).
Устройство генератора вышло сложным (марковская модель, нетривиальный Python), а
цель менти — быстро потренироваться в ClickHouse/Kafka. Вопрос: делать ли
отдельный урок про генератор (возможный «урок 7») или оставить генератор скрытой
инфраструктурой и **адаптировать существующие уроки**, не вводя марковские цепи и
сложный Python в путь менти. Решение зависит от удобства стенда: если он
поднимается одной командой и есть короткий runbook (см. бэклог фичи генератора,
`.scratch/generator-model-time-startup-history/issues/08-startup-history-portable-artifact-and-usage-docs.md`),
отдельный урок, скорее всего, не нужен.