- Зачем:
- приёмка #40 нашла в торговых данных два точных равенства, каких в живом
магазине не бывает: событие корзины случалось ровно у визитов со
страницей /cart, а внутри такого визита в корзину уходили все открытые
карточки. Привлекательность товара было нечем измерить, а аналитик читал
бы эти равенства как склейку в разметке.
- Что:
- класть в корзину может любой визит, открывший карточку; страница /cart
осталась шагом воронки, а у визита, дошедшего до неё, корзина непуста.
- у товара появился уровень спроса — колонка каталога с тремя значениями
и два ряда вероятностей в числах мира: намерение визита берётся из шага
воронки, а не из метки покупателя, которая уже действует через неё.
- уровни рассыпаны по каталогу одной колодой: одинакового расклада по
категориям нет, связи с ценой нет, и то и другое сторожится тестами.
- числа мира перемерены: конверсия карточки в корзину 8,62% (по уровням
13,1 / 9,2 / 5,8), кладут без открытия корзины 41,4% таких визитов,
средний день 49 834 события, конверсия визита в покупку 2,43%.
- спека генератора и мастер-спека приведены к новой форме каталога.
- Проверка:
- make lint && make typecheck && make test — 394 passed (было 383).
- трафиковая половина побайтово та же: sha256 по (URL, времени, WatchID)
всех просмотров за 14 канонических дней совпадает со снимком до правки.
- Зачем:
- клиентская сторона мира становится целой: без add_to_cart и purchase
в данных нет ни таксономии событий, ни вложенного JSON, ни денег,
а метка «покупатель» из плана состава ни на что не влияла (#40).
- Что:
- добавлен модуль commerce: корзина шире заказа, номер заказа вида
ГГГГММДД-NNNN, промокод без скидки в сумме, сырой ecommerce через orjson;
- метка покупателя получила два рычага — долгую жизнь куки в плане и
свою воронку в дне; CART_PERCENT опущен с 8 до 6, чтобы конверсия
мира осталась около 2%;
- часть цен каталога получила копейки: productPrice округляется форматом,
purchaseRevenue несёт точную сумму — расхождение живёт внутри события;
- граница суток забирает страницу подтверждения вместе с её покупкой:
потерь на клиентской стороне этот этап не заводит;
- решения и перемеренные числа мира записаны в спеку генератора, §9.
- Проверка:
- make lint && make typecheck && make test — 383 passed (было 353).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: план состава отдаёт дневную аудиторию, но событий у мира ещё не было.
День-функция превращает аудиторию в поток просмотров — на нём стоят лабы про
сборку визитов и про витрины, а следующий этап вешает на него торговые события.
Что:
- `day.py` — день как чистая функция зерна и номера дня: суточная волна в
местном времени посетителя, визиты по документированным правилам нарезки,
все 47 колонок выгрузки; шов для торговых событий — ряды `page` и `product`;
- `reference.py` — справочники-литералы: профили устройств, города Поволжья с
настоящими гео-id Яндекса, источники трафика, карта сайта;
- `catalog.py` и `data/catalog/products.csv` — каталог на 180 позиций, общий у
генератора и будущего словаря ClickHouse;
- `weights.py` — выбор по целым весам, один на план и на день;
- паспорт куки (устройство и город) переехал в план состава; броски приписаны
последними, поэтому измеренные числа канонического мира не сдвинулись;
- словарь: «визит» закреплён за сессией, одноимённое понятие плана стало
«днём активности»; статьи в `CONTEXT.md`;
- решения по ходу — в спеку генератора, раздел 9; наполнение
`ParsedParamsKey1` отложено тикетом #47.
Проверка: `make lint`, `make typecheck`, `make test` — 353 passed (было 297).
Счётчики плана после правки те же: приток 3827,64/день, дневная аудитория
6235–7124, 68 119 посетителей за 14 дней, 170 двухкуковых пар. День 0 —
45 810 событий за 0,6 с, снимок 14 дней — 5,9 с при пороге 30 с на день.
Две слепые линии ревью, десять находок, все закрыты и перепроверены.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- третья линия ревью (Кодекс, другое семейство моделей) нашла дыру в
стороже неизменности когорты, отставший словарь и тест, который
сторожил не тот адрес дерева зёрен.
- Что:
- когорта отдаётся видом на замороженный массив: флаг только для
чтения вызывающий мог снять и испортить память, которой пользуются
все дни окна. Граница защиты названа в докстроке — от случайности,
не от умысла.
- CONTEXT.md и комментарий `RETURN_TAIL_DAYS`: окно активности — от
первого визита человека, общее на обе куки (спека это уже говорила,
словарь отстал).
- сторож предыстории сверял день −N с днём N, а сталкиваются −N и
N−1; тем же классом слепоты страдали сторожа независимости состава
и дня и различия компонентов — все три переписаны на сверку со всем
куском адресов, куда подпоток мог бы попасть.
- Проверка:
- make test (297), make lint, make typecheck;
- батарея из 17 мутантов по plan/world/seeds — выживших нет; гоняется
с PYTHONDONTWRITEBYTECODE=1: цикл правки и отката внутри одной
секунды оставлял устаревший .pyc, и тесты шли по старому байт-коду.
- Отклонено с доводом:
- перепроверка настаивала, что сторож неизменности не закрыт: через
`.base` вида владелец данных размораживается. Верно фактически, но
закрывающего состояния у находки нет — владелец памяти в numpy
размораживается всегда, а копия когорты на каждый вызов меняет 2 мс
на 16 МиБ копирования и убивает смысл запоминания.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- ревью нашло два места, где код и документы говорили неправду, и
несколько мест, где имена или комментарии вводили в заблуждение.
- Что:
- обещание докстроки `seeds.py` подкреплено тестом: адрес в дереве
даёт тот же подпоток, что цепочка `spawn`.
- в тесте гарантии пар убран сторож-тавтология, вместо него проверка,
что заказы назначены с двух разных кук.
- `Cohort.visitors_on` — «кто пришёл в день D» спрашивается у когорты,
а не собирается снаружи из четырёх её массивов.
- имена: `CLIENT_ID_LIMIT`, `_RETURN_*_CUMULATIVE`, `first_of_day`,
`WEEKLY_PROFILE_PERCENT` — профиль недели один на весь мир, по нему
же пойдёт трафик дня-функции (#39).
- спека: в дерево зёрен внесена ветвь предыстории; окно активности —
от первого визита человека, общее на обе куки (иначе загляд назад
ленивой формы удваивается); оценка накопленной аудитории больше не
спорит с измерением.
- CONTEXT.md: «подпоток» и «конфигурация мира».
- Проверка:
- make test (296), make lint, make typecheck; мутации перепроверены
после переноса среза дня в `Cohort`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- тикет #38: состав мира должен быть чистой функцией зерна, а счётчики
будущего манифеста — известны до генерации хоть одного события.
- Что:
- `world.py` — конфигурация мира одним модулем чистых данных: приток,
недельная волна, профиль возвратов, доли покупателей и пар, D0.
- `seeds.py` — иерархия подпотоков на `SeedSequence`: состав мира
(ось и предыстория) отдельно от дней и их компонентов.
- `plan.py` — ленивый план состава: когорта дня, аудитория дня из
окна возвратов, гарантированные заказы пар, счётчики горизонта.
Случайность — только целыми числами.
- спека, раздел 1: вторая кука пары рождается по затухающему профилю
возвратов; раздел 9: измеренные числа канонического мира, оценка
накопленной аудитории поправлена с ≈60 до 68 тыс.
- CONTEXT.md: термин «когорта дня»; README генератора — новые модули.
- Проверка:
- make test (295 тестов), make lint, make typecheck;
- тесты проверены мутациями: 12 подмен в плане и конфигурации,
каждая роняет ровно свой тест.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>