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 index 52f3e54..f1b1de3 100644 --- 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 @@ -1,4 +1,4 @@ -Status: needs-triage +Status: ready-for-agent # Браузерная фактура не сохраняется на стыке восстановления визита @@ -9,7 +9,8 @@ Status: needs-triage ## Что нашли Визит, переживший стык backfill->live (активен на `T_end` и продолжается живьём), -меняет браузерную фактуру в live-продолжении. Внутри одного `click_id` человек как +меняет браузерную фактуру в live-продолжении (фактура — конкретные значения полей +события: какой `browser_name`, user agent, язык стоят в строках). Внутри одного `click_id` человек как будто «пересел» с одного браузера на другой посреди сессии. Эмпирика. Чистый стенд, 2 суток backfill + 6 live-тиков за `T_end`, @@ -37,16 +38,39 @@ Status: needs-triage внешний review gate другой родословной после задачи 5 — а он был выполнен той же родословной, и дефект прошёл. -## Причина (гипотеза по коду) +## Причина (подтверждена чтением кода, 2026-07-04) -Конкретные браузерные строки визита (`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` между уже выпущенной частью и восстановленной — -вероятен рассинхрон индекса. Точную точку уточнить при фиксе. +Первоначальная гипотеза «рассинхрон `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. Стык и рестарт на уровне генератора — один и тот же +код, различие только в отбрасывании просроченных визитов. ## Масштаб и важность @@ -58,15 +82,77 @@ Status: needs-triage - Для учебного демо урон низкий, но это реальное нарушение стыковой однородности и сигнал, что браузерная фактура не входит в восстановимое состояние визита. -## Направление фикса +## Решение по направлению фикса (пользователь, 2026-07-04) -- Либо сериализовать браузерную фактуру визита в state рядом с - `page_url_paths`/`timestamps`, чтобы восстановленная часть точно продолжила - выпущенную. -- Либо выбирать браузер по абсолютному `event_index` визита, а не по позиции в - остатке, чтобы restore воспроизводил те же строки. -- В проверку стыка добавить per-event браузерные поля (`dds.event`), а не только - `dds.click`, чтобы регресс ловился автоматически. +**Хранить донора в 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 + +- [ ] Быстрый автотест на уровне генератора: сгенерировать визит, сохранить state + посреди визита, восстановить и сверить оставшиеся события с продолжением без + рестарта — по **всем** per-event полям, кроме `event_id` (браузерные и + location: referer, utm — не только browser_name; см. «Расхождение шире» выше). + Это один код восстановления для стыка backfill->live и crash-recovery + (задача 04) — одного теста на него достаточно. +- [ ] Проверка стыка на стенде расширена per-event полями: на сценарии из «Что + нашли» (2 суток backfill + live за `T_end`, `GEN_SEED=4242`) визиты через стык + однородны в `dds.event` по браузерным полям (`browser_name`, + `browser_language`) **и** полям источника перехода (referer, utm) — 0 + расхождений из `crossing_visits`. Запрос/скрипт проверки сохранён как + повторяемый, а не разовый. +- [ ] Без регрессий на том же сценарии: `duplicate_events=0`, конфликтов + device / os / geo на уровне ODS по-прежнему 0. +- [ ] `STATE_VERSION` поднята, новое поле валидируется; несовместимый старый + state обрабатывается по действующему правилу (см. детали выше). + +## Notes + +- Рекомендуемый режим ревью по coordinator-loop: **гейт** (state, сериализация — + из порогов риска). Ровно эта слепая зона уже пропустила дефект один раз: + критерий задач 04/05 проверяли только по `dds.click`. +- Задача 13 (доливка) идёт после этого фикса — restore-механизм должен быть + исправлен до того, как на него навешивать доливку. ## Blocked by 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 index 32af33f..b0c86c6 100644 --- 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 @@ -1,4 +1,4 @@ -Status: needs-triage +Status: ready-for-agent # Гео-карта на дашборде нечитаема @@ -9,39 +9,81 @@ Status: needs-triage ## Что нашли -На финальной ручной приёмке (шаг 2 спеки) виджет «🗺️ Geography Map» показывает +На финальной ручной приёмке (шаг 2 спеки) виджет «🌍 Geography Map» показывает что-то, но прочитать нельзя: - нет всплывающих подсказок (tooltip) — навёл на страну, значения не видно; - нет легенды и подписи шкалы — непонятно, что кодирует цвет и в каких единицах; - цветовая шкала неочевидна — подсвечено мало стран, остальное пусто. -Виз отрисовывается, но смысл непрозрачен: нельзя сказать, что именно он показывает. +График рисуется, но непонятно, что именно он показывает. ## Две разные причины (не путать) -1. **Читаемость визуализации (эта задача).** Настройка чарта: включить tooltip, - добавить легенду и подпись метрики/единиц, выбрать понятную цветовую шкалу, - при необходимости — заменить мировую choropleth на более читаемую форму - (например, топ-N стран баром), раз распределение сильно разрежено. -2. **Перекос гео-данных (отдельно, ADR-0006).** Сама гео-фактура берётся из - статического сида (`geo_by_click_id`) с перекошенным распределением стран. Это - «временная опора на сид» до синтеза фактуры — лечится не настройкой чарта, а - генерацией гео. Здесь не решаем, только отмечаем как смежную причину. +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 -- [ ] У гео-виза есть tooltip со значением по стране. -- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется цветом. -- [ ] Цветовая шкала читаема на текущем (разреженном) распределении, либо выбран - более подходящий тип визуализации. -- [ ] Зафиксировано, что перекос распределения стран — это вопрос гео-фактуры - (ADR-0006), а не настройки чарта. +- [ ] У гео-графика есть tooltip со значением по стране (на выбранном типе + визуализации — карте или замене). +- [ ] Есть легенда и подпись: какая метрика и в каких единицах кодируется + (для текущей метрики `COUNT(*)` честный ответ — «событий, штук»). +- [ ] Выбранный тип визуализации читаем на текущем (разреженном) распределении; + выбор типа (оставить карту или заменить) зафиксирован с коротким «почему». +- [ ] Зафиксировано, что перекошенное распределение стран — свойство гео-фактуры + из сида (своя генерация гео — отдельный шаг по ADR-0006), а не настройки + чарта. +- [ ] Приёмка пройдена по скриншотам (см. «Как принимать» ниже), скриншот «после» + приложен к итогам задачи. + +## Как принимать (дописано 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`. + `docs/SUPERSET_DASHBOARD.md` — при смене типа визуализации обновить в этом же + изменении. - Возможно, стоит расширить до общего прохода по читаемости дашборда (легенды и - подсказки у других виджетов), но базово задача — про гео-карту. + подсказки у других виджетов), но базово задача — про гео-карту. Если проход + делается, он не должен раздувать задачу: заметил — запиши отдельным issue. +- Рекомендуемый режим ревью по coordinator-loop: обычный (риска для state и + данных нет, правка ограничена определением чарта и документацией). diff --git a/.scratch/handoffs/2026-07-04-generator-backlog-triage.md b/.scratch/handoffs/2026-07-04-generator-backlog-triage.md index 32f9d2d..61145a7 100644 --- a/.scratch/handoffs/2026-07-04-generator-backlog-triage.md +++ b/.scratch/handoffs/2026-07-04-generator-backlog-triage.md @@ -43,20 +43,34 @@ ## Следующий шаг -Дооформить до `ready-for-agent` два независимых issue (можно параллельно с -остальной очередью): +Весь бэклог готов к передаче Codex: `/goal` на отдельные задачи (09 и 10 +независимы, можно параллельно) или `/coordinator-loop` на цепочки 07 -> 08 и +07 -> 11 -> 12. Задача 13 — после фикса 09. -1. **09 — баг браузерной фактуры на стыке.** Нет секции Acceptance criteria; - зафиксировать выбор направления фикса (сериализация фактуры в state против - выбора по абсолютному `event_index`; второе не трогает схему state) и - требование теста по per-event полям `dds.event`, включая crash-recovery - (гипотеза: тот же restore-механизм). Режим ревью — гейт (state). -2. **10 — гео-карта.** Прописать способ приёмки (Playwright-скриншоты, что - считать «читаемо»); учесть, что стенд с 14 июня мог умереть, а полная - пересборка регенерирует backfill. +## Дооформление 09 и 10 — что изменилось против прежнего понимания -После этого весь бэклог готов к передаче Codex (`/goal` на задачу или -`/coordinator-loop` на цепочку 07 -> 08 / 07 -> 11 -> 12). +Сделано в этот же день, вторым заходом. 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 с режимом «гейт» + так стоит делать и дальше. ## Не забыть (вне бэклога)