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:
+96
@@ -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`
|
||||
+73
@@ -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.
|
||||
+47
@@ -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`.
|
||||
- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и
|
||||
подсказки у других виджетов), но базово задача — про гео-карту.
|
||||
Reference in New Issue
Block a user