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.