diff --git a/CONTEXT.md b/CONTEXT.md index 593d27f..61637b2 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -63,11 +63,23 @@ _Избегать_: визит (визит — про сессию) устройстве. Держит их план состава, разворачивает в поля события день-функция; у двухкуковой пары город один на две куки, устройства разные. +**Покупатель**: +Человек, которого план состава пометил склонным покупать. Метка значима: +помеченный доходит до заказа заметно чаще прочих, но и непомеченный иногда +покупает. Из покупателей отбираются двухкуковые пары. +_Избегать_: «покупатель» про того, кто купил в конкретный день — это визит +с заказом. + **День-функция**: Функция (зерно, номер дня), выдающая упорядоченный поток событий этих модельных суток. Состояния между днями нет: день D не зависит от того, прожиты ли дни до него. +**Торговое событие**: +Событие корзины или покупки — отдельная строка потока, а не просмотр +страницы. Садится на ту страницу, где случилось: корзина — на карточку +товара, покупка — на страницу подтверждения заказа. + **Каталог товаров**: `data/catalog/products.csv` — общий справочник генератора и словаря ClickHouse. Форма файла решена, длина — нет: строки дописываются. diff --git a/docs/specs/2026-08-01-generator.md b/docs/specs/2026-08-01-generator.md index 5617957..138bada 100644 --- a/docs/specs/2026-08-01-generator.md +++ b/docs/specs/2026-08-01-generator.md @@ -390,6 +390,13 @@ pytest-тест с маркером `perf` и таймаутом-обрубан ## 8. Хвосты следующим этапам +- **Этап 3 (заказы бэкенда)**: заказу, у которого в клиентском событии стоит + промокод, бэкенд обязан дать скидку — иначе данные соврут. Номер заказа обе + стороны берут один и тот же (раздел 9). +- **Этап 4 (сверка)**: расхождение сумм само не появится — цены каталога + кратны рублю, и `Float64` на таких числах не плывёт. Класс `amount_delta` + придётся создавать намеренно, вероятностью в генераторе, — мастер-спека + (раздел 4) это и допускает. - **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг по расписанию (~раз в 24 минуты); генератор не дорабатывается. - **Этап 7 (эталонный мир)**: пересборка — это манифест, не артефакт; @@ -542,6 +549,73 @@ pytest-тест с маркером `perf` и таймаутом-обрубан `/confirmation`, а перед корзиной у него всегда есть карточка. Своей случайности #39 у #40 не занимает: подпоток `COMMERCE` не тронут. +Решено при исполнении #40 (2026-08-02) — решения владельца до реализации: + +- **Корзина шире заказа.** Посетитель кладёт в корзину товары тех карточек, + которые открывал в этом визите, а покупает не всё: около 15% позиций + остаются брошенными. Иначе событие корзины не рассказывало бы ничего сверх + покупки — заказ был бы её точной копией, и сравнивать было бы нечего. + Замерено на каноническом мире до правки воронки (ниже): у покупающего + визита 2,1 карточки, в заказе + выходит 1,9 позиции, однопозиционных заказов около половины. Отклонено: + *заказ всегда из одного товара* — список позиций как урок умирает, а на нём + стоит единственный носитель навыка «вложенный JSON» (мастер-спека, + раздел 2); *досыпать в корзину товары, которых посетитель не открывал* — + ломает шов #39 ровно там, ради чего он строился; *бросать много* (40% и + выше) — две трети заказов схлопываются в один товар. +- **Покупатель перестаёт быть словом.** Метка плана — «этот человек склонен + покупать», 5% людей когорты — до сих пор ни на что не влияла: кто купит, + решал одинаковый для всех бросок в дне. Свойство человека в данных не + читалось, постоянных покупателей в мире не было. Теперь метка правит + воронкой: помеченный доходит до заказа заметно чаще прочих, но и + непомеченный иногда покупает. Общее число покупок и общая доля визитов с + корзиной остаются прежними — меняется лишь то, кто эти визиты совершает; + точные доли — калибровка при реализации, требование сторожит тест. + Уточнение к разделу 1: вольные покупки по-прежнему решает день, но + случайность в них больше не одинакова для всех. Плата + названа: воронка живёт в коде #39, поэтому правка выходит за исходные + границы тикета и сдвигает трафиковый поток. Сейчас это ничего не стоит — + манифест ещё не собран (#42); позже обошлось бы его пересборкой. + Отклонено: *оставить как есть* — числа сходятся, но «покупатель» остаётся + именем без следа в данных; *покупают только помеченные* — 5% людей дают + около 40 заказов в день вместо 220; вернуть их можно, лишь подняв долю + покупателей до 10–30% людей, а с ней вырастет число двухкуковых пар и + измеренные числа #38 придётся пересчитывать. +- **Событие корзины садится на карточку товара**, событие покупки — на + страницу подтверждения. В жизни событие корзины шлётся нажатием кнопки на + карточке, а не открытием страницы корзины; при нескольких товарах в заказе + иначе его и не разложить — событий столько, со скольких карточек положили. +- **Номер заказа читаемый**: день модельного времени и порядковый номер + покупки в этом дне. Магазины так и нумеруют, и по номеру сразу видно день. + Он же — `order_id` бэкенда: обе стороны получают один номер, по нему + соединяется сверка (мастер-спека, раздел 4). +- **Промокод в событии есть, в сумме его нет.** Часть заказов уходит с + промокодом, но выручка, которую шлёт клиент, — сумма позиций без скидки и + доставки. Так и в жизни: код на сайте знает корзину, а не итог расчёта. Это + ровно то расхождение с бэкендом, которое менти будет раскапывать + (мастер-спека, раздел 4). Отклонено: *промокода нет никогда* — колонка + формата осталась бы мёртвой. +- **Деньги: копейки внутри, рубли в колонках, дробь только в выручке.** + Генератор считает целыми копейками; цена товара в событии — целые рубли, + как у Метрики (`productPrice` там `Int64`); дробное число одно — + `purchaseRevenue` типа `Float64`. Деление на сто верно, лишь пока цены + каталога кратны рублю: это сторожит тест, иначе копейки утекали бы молча. +- **Массивы `purchase*` и `product*` разной длины между собой.** Внутри своей + группы длина общая: `purchase*` — по элементу на заказ (у нас всегда один), + `product*` — по элементу на товар. Мастер-спека (раздел 1) говорит о + «параллельных массивах одной длины», и прочесть это как «все массивы + события одной длины» легко — поэтому в описании выгрузки сказано прямо. +- **Сырой `ecommerce` собирает канонический сериализатор.** Поле — строка + JSON внутри события, и собирать её руками значит писать второй + сериализатор со своим экранированием. Зависимость `orjson`, названная + рабочим выбором в разделе 4, появляется здесь, а не в #41. +- **Хвост покупки не выходит за полночь.** Событие покупки встаёт на + несколько секунд позже просмотра страницы подтверждения, а граница суток + режет всё, что за неё вышло. Визит с обещанным планом заказом уже подвинут + так, чтобы уместиться в сутки (#39), но подвинут по страницам — торгового + хвоста в том расчёте не было. Хвост прижимается к последней секунде суток; + иначе гарантия двухкуковых пар порвалась бы молча. + Остаётся открытым, за тикетами: - интерфейс запуска генератора (CLI / цели make) и как он делит режимы