diff --git a/.scratch/generator-model-time-startup-history/issues/08-startup-history-portable-artifact-and-usage-docs.md b/.scratch/generator-model-time-startup-history/issues/08-startup-history-portable-artifact-and-usage-docs.md new file mode 100644 index 0000000..470e32c --- /dev/null +++ b/.scratch/generator-model-time-startup-history/issues/08-startup-history-portable-artifact-and-usage-docs.md @@ -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` diff --git a/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md b/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.md new file mode 100644 index 0000000..52f3e54 --- /dev/null +++ b/.scratch/generator-model-time-startup-history/issues/09-seam-browser-fixture-not-preserved.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. diff --git a/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md b/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md new file mode 100644 index 0000000..32af33f --- /dev/null +++ b/.scratch/generator-model-time-startup-history/issues/10-dashboard-geo-map-readability.md @@ -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`. +- Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и + подсказки у других виджетов), но базово задача — про гео-карту. diff --git a/.scratch/handoffs/2026-06-14-generator-model-time-verification-review.md b/.scratch/handoffs/2026-06-14-generator-model-time-verification-review.md new file mode 100644 index 0000000..0f671b6 --- /dev/null +++ b/.scratch/handoffs/2026-06-14-generator-model-time-verification-review.md @@ -0,0 +1,96 @@ +# Handoff: независимое ревью модельного времени и стартовой истории + +Дата: 2026-06-14 +Жанр: одноразовый handoff по ADR-0003. + +## Контекст + +После прогона coordinator-loop по фичам `feature-data-generator` и +`generator-model-time-startup-history` (issues 01–06, реализация 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). diff --git a/docs/course/PRD.md b/docs/course/PRD.md index 81b5c9f..b3769f0 100644 --- a/docs/course/PRD.md +++ b/docs/course/PRD.md @@ -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`), + отдельный урок, скорее всего, не нужен.