docs(generator): заморожена спека этапа 2, решения — в ADR 0015-0017
- Зачем:
- спека разрослась до 1058 строк, а журнал решений в разделе 9 читался
только целиком; агентам и людям нужен дешевый адресный заход к
основаниям решений генератора.
- Что:
- заведены ADR 0015 (пояс счетчика), 0016 (паспорт куки) и 0017 (номер
заказа); четыре адресные ссылки на «раздел 9» перешиты на них.
- раздел 9 пересобран в 11 тематических подразделов с датами и тикетами
у каждого решения и закрыт пометкой о заморозке; устаревшие
промежуточные замеры свернуты до указателей на актуальные.
- строка статуса в шапке отмечает закрытие спеки как документа этапа 2.
- Проверка:
- wc -l docs/specs/2026-08-01-generator.md (951, было 1058);
rg -n 'раздел[а-яё]* 9|разделы 8 и 9' docs -g '*.md' — 18 попаданий,
все классифицированы; разделы 1-8 нетронуты (сверка хешем).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
74061aa499
commit
6f18e9989c
9 files changed
+522
-526
No files matched your search
+410
-517
@@ -1,6 +1,8 @@
|
||||
# Генератор (этап 2): функциональный мир, детерминизм до байта, контракт схемы, числа скорости
|
||||
|
||||
Статус: Accepted — принята владельцем 2026-08-01 (PR #35).
|
||||
Статус: Accepted — принята владельцем 2026-08-01 (PR #35); закрыта
|
||||
2026-08-28 как документ этапа 2. Несущие решения — в ADR 0015–0017, новые
|
||||
решения генератора — в ADR.
|
||||
Дата: 2026-08-01. Мандат — тикет #14 (этап 2, родитель #4), карта #26.
|
||||
Развилки: модель мира (#27), детерминизм от зерна (#28), архитектура вывода
|
||||
(#29), производительность (#30); исследование скорости (#31).
|
||||
@@ -502,354 +504,328 @@
|
||||
|
||||
## 9. Вопросы нарезки этапа 2 (#34): решения и остатки
|
||||
|
||||
Туман карты разложен нарезкой по тикетам; решения фиксируются здесь по
|
||||
мере исполнения.
|
||||
Журнал закрыт 28 августа 2026 года. Он хранит историю решений этапа 2; основания
|
||||
модели времени, паспорта куки и номера заказа вынесены в
|
||||
[ADR 0015](../adr/0015-counter-timezone.md),
|
||||
[ADR 0016](../adr/0016-cookie-passport.md) и
|
||||
[ADR 0017](../adr/0017-order-numbering.md). Новые решения генератора
|
||||
записывают в ADR и в спеке этапа, на котором их принимают.
|
||||
|
||||
Решено при исполнении #38 (2026-08-02):
|
||||
Журнал собран по темам, но дата и тикет остались у каждого решения. По
|
||||
журналу видно, какие границы затрагивала каждая правка: иногда она меняла
|
||||
сразу план состава, воронку и каталог. При #40 это было дешево — опись еще
|
||||
не собрали; позже такой ход потребовал бы пересборки описи.
|
||||
|
||||
- **Числа притока и состава.** Приток — ~3 800 новых кук в средний день,
|
||||
модулируется недельным профилем мира; трафик наследует эту волну через
|
||||
дневную аудиторию, а не отдельным умножением (уточнение при исполнении
|
||||
#39 — ниже). Доля одноразовых кук — 75%;
|
||||
возвращающиеся — в среднем 3–4 возврата, профиль убывающий: почти все
|
||||
в первые 7–10 дней, тонкий хвост поздних возвратов и повторных
|
||||
покупок — до края окна (цикл повторной покупки магазина — месяцы).
|
||||
Хвост возвратов — окно активности человека от его первого дня,
|
||||
общее на обе его куки, — и глубина предыстории: 90 дней (решение
|
||||
владельца 2026-08-02: дольше квартала стенд никто не гоняет, а заказы
|
||||
старых посетителей продолжаются весь прогон; плата — чуть меньше пар,
|
||||
полностью реализованных внутри 14-дневного снимка). Покупатели
|
||||
считаются людьми, не куками; доля покупателей — 5% людей когорты, а
|
||||
людей в когорте почти столько же, сколько кук: вторые куки пар
|
||||
добавляют меньше процента. Доля — число плана, а не торгового тикета: без него не
|
||||
отобрать двухкуковые пары; #40 наследует его, не переоткрывая.
|
||||
Сходимость с разделом 5: средняя кука активна ≈1,9 дня → дневная
|
||||
аудитория ≈7 100 — середина вилки 6–8 тыс.; уникумов за 14 дней
|
||||
снимка — под 70 тыс.: новые куки плюс возвраты предыстории (точные
|
||||
числа — в измерении ниже; не путать с ~50 тыс. событий одного дня) —
|
||||
рост `uniq(ClientID)` с горизонтом виден сразу; новых покупателей
|
||||
~190 в день → конверсия ~2% на сессию.
|
||||
- **D0 = 2026-06-01, понедельник** — решение владельца при нарезке
|
||||
(2026-08-01). Дата недавняя, чтобы данные первые месяцы выглядели
|
||||
свежими; привязки к реальному календарю у констант мира всё равно нет.
|
||||
- **Измерено на собранном плане** (канонический мир, 14 дней; числа
|
||||
перемерены при исполнении #40 — рычаг долгой жизни помеченного
|
||||
покупателя сдвинул состав): приток 3 828 кук в день в среднем, дневная
|
||||
аудитория 6 637–7 595 — обе величины в вилках раздела 5. Накопленная
|
||||
аудитория за снимок — 70,2 тыс. кук: 53,6 тыс. новыми плюс 16,6 тыс.
|
||||
возвратами предыстории (при нарезке возвраты предыстории оценили
|
||||
скромнее — отсюда ходившая раньше оценка ≈60 тыс.). Пар, у которых оба
|
||||
назначенных заказа попали внутрь 14 дней, — 192; остальные пары горизонт
|
||||
переживают, их вторая кука приходит позже. Цифры пересчитываются
|
||||
прогоном плана, событий для них не нужно. До правки #40 те же измерения
|
||||
давали: аудитория 6 235–7 124, накопленная 68,1 тыс. (14,5 тыс.
|
||||
возвратами предыстории), 170 пар.
|
||||
- **Конфигурация мира — модуль чистых данных** рядом с контрактом схемы:
|
||||
все числа мира в одном месте, написанном как приглашение любопытному
|
||||
менти крутить. Правка модуля — смена мира: чек описи честно
|
||||
краснеет, опись сторожит только канон. Вне модуля — лишь то, что
|
||||
мира не меняет: своё зерно и транспортные флаги проигрывателя.
|
||||
Отклонено: внешний конфиг и env-переопределения — переменная мира,
|
||||
которую паспорт описи не видит; файл-конфиг в репозитории — по
|
||||
смыслу равен модулю, но платит загрузчиком и валидацией (довод
|
||||
раздела 3 против YAML).
|
||||
### План состава и числа мира
|
||||
|
||||
Решено при исполнении #39 (2026-08-02) — решения владельца до реализации:
|
||||
- **Числа притока и состава** (#38, 2026-08-02). Приток — ~3 800 новых кук в
|
||||
средний день, модулируется недельным профилем мира; трафик наследует эту волну
|
||||
через дневную аудиторию, а не отдельное умножение (уточнение #39). Доля
|
||||
одноразовых кук — 75%; возвращающиеся дают в среднем 3–4 возврата, почти все в
|
||||
первые 7–10 дней, а тонкий хвост поздних возвратов и повторных покупок тянется
|
||||
до края окна: цикл повторной покупки магазина измеряется месяцами. Хвост
|
||||
возвратов — общее на обе куки окно активности человека от первого дня и
|
||||
глубина предыстории: 90 дней (решение владельца 2026-08-02: дольше квартала
|
||||
стенд никто не гоняет, а заказы старых посетителей продолжаются весь прогон;
|
||||
плата — чуть меньше пар, полностью реализованных внутри 14-дневного снимка).
|
||||
Покупатели считаются людьми, не куками; их доля — 5% людей когорты. Людей
|
||||
почти столько же, сколько кук: вторые куки пар добавляют меньше процента. Доля
|
||||
— число плана: без него не отобрать двухкуковые пары, и #40 его не
|
||||
переоткрывает. Сходимость с разделом 5: средняя кука активна ≈1,9 дня, дневная
|
||||
аудитория ≈7 100, уникумов за 14 дней — под 70 тыс. Это новые куки и возвраты
|
||||
предыстории; их не надо путать с ~50 тыс. событий одного дня. Новых
|
||||
покупателей ~190 в день, что дает конверсию ~2% на сессию.
|
||||
- **Конфигурация мира — модуль чистых данных** (#38, 2026-08-02) рядом с
|
||||
контрактом схемы: все числа мира собраны в одном месте, которое менти может
|
||||
крутить. Правка модуля меняет мир, и проверка описи честно краснеет: опись
|
||||
сторожит только канон. Вне модуля остается то, что мира не меняет, — свое
|
||||
зерно и транспортные флаги проигрывателя. Отклонено: внешний конфиг и
|
||||
переопределения окружением — переменная мира, которой нет в паспорте описи;
|
||||
файл-конфиг в репозитории равен модулю по смыслу, но платит загрузчиком и
|
||||
валидацией (довод раздела 3 против YAML).
|
||||
- **Покупатель перестает быть словом — двумя рычагами сразу** (#40, решение
|
||||
владельца до реализации, 2026-08-02). Помеченный планом покупатель заметно
|
||||
чаще доходит до заказа, дольше живет и чаще возвращается. Склонность покупать
|
||||
— свойство человека, а ее след в анонимном кликстриме виден только как долгая
|
||||
жизнь куки и повторные покупки. Один рычаг этого не дает: кука живет меньше
|
||||
двух визитов за снимок, и второй покупке негде случиться. Рычаг жизни лежит в
|
||||
плане (#38), рычаг конверсии — в воронке (#39); помеченный отличается на обоих
|
||||
шагах — чаще доходит до корзины и реже бросает ее. Помеченному дано 18% до
|
||||
корзины и 60% из корзины в оформление против 6% и 45% у обычного посетителя.
|
||||
Обычная доля снижена с 8% до 6%: помеченные живут дольше и занимают большую
|
||||
долю дневной аудитории, поэтому прежнее число увело бы общую конверсию за
|
||||
вилку раздела 5. Рычаг метки не должен оплачиваться ростом конверсии; расти
|
||||
внутри вилки аудитории миру можно. Вольные покупки по-прежнему решает день
|
||||
(уточнение к разделу 1), но не одинаковой для всех случайностью. Отклонено:
|
||||
*оставить одинаковый бросок* — покупатель остается именем без следа; *поднять
|
||||
только конверсию* — короткой жизни не хватает для повторных покупок;
|
||||
*разрешить покупать только помеченным* — их малая доля дала бы слишком мало
|
||||
заказов, а ее рост завысил бы число двухкуковых пар. Промежуточные числа
|
||||
пересчитаны при #50; актуальные — в таблице замера #50.
|
||||
- **Счетчика покупок в плане состава не заводится** (#40, решение владельца до
|
||||
реализации, 2026-08-02). Тикет просил свести с планом покупки и двухкуковые
|
||||
пары, но раздел 1 обещает до генерации только состав — посетителей, приток и
|
||||
пары. Торговые счетчики складываются из поведения дня. Квота заставила бы день
|
||||
добирать точное число и сделала бы конверсию подозрительно ровной: условность
|
||||
была бы видна именно там, куда смотрит лаба. Сходятся пары; покупки
|
||||
проверяются после генерации по дню и вилке конверсии.
|
||||
|
||||
- **Каталог товаров заводится здесь, а не в #40.** У карточки товара есть
|
||||
`Title` — имя товара, а имена живут в каталоге: карточка без каталога
|
||||
невозможна, и без карточек #39 сделал бы примерно половину трафика.
|
||||
Файл `data/catalog/products.csv` (`sku`, `name`, `category`, `brand`,
|
||||
`price`) — общий у генератора и словаря ClickHouse; #40 получает готовый.
|
||||
Решена форма, а не длина: артикул — четыре буквы категории и четыре
|
||||
цифры, цена — целые копейки, категорий шесть. Строки дописываются
|
||||
механически, поэтому ни код, ни тесты их не считают, а товар для карточки
|
||||
выбирается равномерно внутри категории.
|
||||
- **Ассортимент — непродовольственная розница** (товары для дома, текстиль,
|
||||
посуда, мелкая бытовая техника, детское, одежда и обувь), ~150–200 sku.
|
||||
Довод не вкусовой: числа мира приняты под этот профиль — цикл повторной
|
||||
покупки в месяцы, 75% одноразовых кук, конверсия ~2% на визит.
|
||||
Продуктовая сеть требовала бы других чисел, то есть переоткрытия #38.
|
||||
- **География — один регион присутствия**: Поволжье с центром в Самаре,
|
||||
города своего региона и тонкий хвост остальной страны. «Топ городов
|
||||
России» дал бы магазину с одним складом карту, которой у него не бывает.
|
||||
- **Модельные сутки считаются в часовом поясе счётчика** (UTC+4), как в
|
||||
выгрузке Метрики: `EventDate` — дата в поясе счётчика, `UTCEventTime` —
|
||||
абсолютная метка. Шов суток приходится на местную полночь и не режет
|
||||
утренние сессии, а регион мира выбирается свободно. Суточная волна задана
|
||||
в местном времени посетителя: гостю из другого пояса профиль
|
||||
поворачивается на разницу, и пик слегка размазывается — как в жизни.
|
||||
Следствие для соседних этапов: `toDate(UTCEventTime)` ≠ `EventDate`; оно
|
||||
записано в описание выгрузки, иначе сторона хранилища выведет дату сама и
|
||||
разойдётся на несколько часов данных.
|
||||
- **Паспорт куки — постоянная часть мира, а не поведение дня.** Браузер, ОС,
|
||||
устройство, экран и город у куки одни во всех её днях: кука — это браузер
|
||||
на устройстве. Держит их план состава (`Cohort` и `DayAudience` прирастают
|
||||
двумя массивами), разворачивает в поля события день-функция. Выводить
|
||||
паспорт арифметикой из `ClientID` дешевле, но про двухкуковые пары знает
|
||||
только план, а два города у одного человека — ложь в данных; пара
|
||||
получает один город и разные устройства («телефон и ноутбук»,
|
||||
мастер-спека, раздел 5). Броски паспорта приписаны последними, поэтому
|
||||
измеренные числа канонического мира не сдвинулись: прогон счётчиков до и
|
||||
после дал те же 3 828 / 6 235–7 124 / 68 119 / 170.
|
||||
- **Справочники — таблицы-литералы** рядом с конфигурацией мира; Faker и
|
||||
mimesis не подключаются. Нужны не случайные строки, а связки (город → id
|
||||
региона → часовой пояс; телефон → Safari → iOS → размер экрана) — их
|
||||
фейкер не даёт, таблицу пришлось бы написать всё равно, а новая
|
||||
зависимость молча меняла бы мир при обновлении своих словарей.
|
||||
- **Гео-id — настоящие числа геобазы Яндекса.** Проверка 2026-08-02: id
|
||||
подтверждались обращением к живым сервисам Яндекса по тому же номеру
|
||||
(`yandex.ru/pogoda/<id>`, `yandex.ru/maps/225/russia/`) — страница
|
||||
открывает ожидаемое место. Оговорка стоит в коде: опубликованной таблицы
|
||||
геобазы найти не удалось, а что `RegionCityID` Метрики нумерует регионы
|
||||
той же геобазой — обоснованное допущение, не подтверждённый источником
|
||||
факт. Выдуманные по памяти числа не годятся ни в каком случае.
|
||||
- **IP-адреса — нероутируемые диапазоны**: документационные сети RFC 5737 и
|
||||
benchmark-сеть 198.18/15 по ломтю на город, телефонам — CGNAT 100.64/10,
|
||||
как у настоящих операторов. Правдоподобные публичные адреса принадлежат
|
||||
живым организациям, и в учебных данных им не место.
|
||||
- **Словарь: «визит» остаётся за сессией** и полем `VisitID`. Одноимённое
|
||||
понятие плана состава переименовано в «день активности»: у куки он один на
|
||||
день, а визитов внутри дня бывает несколько.
|
||||
### Время, визиты и границы дня
|
||||
|
||||
Решено при исполнении #39 самой реализацией:
|
||||
- **D0 = 2026-06-01, понедельник** (#38, решение владельца 2026-08-01). Недавняя
|
||||
дата делает первые месяцы данных свежими, но к календарю запуска константа
|
||||
мира не привязана.
|
||||
- **Модельные сутки считаются в часовом поясе счетчика** (#39, решение владельца
|
||||
до реализации, 2026-08-02), а местная волна гостя поворачивается —
|
||||
[ADR 0015](../adr/0015-counter-timezone.md).
|
||||
- **Слово «визит» остается за сессией** (#39, решение владельца до реализации,
|
||||
2026-08-02) и полем `VisitID`. Одноименное понятие плана переименовано в «день
|
||||
активности»: у куки он один на день, а визитов внутри дня бывает несколько.
|
||||
- **Правила резки визитов** (#39, 2026-08-02; мастер-спека, раздел 1.2,
|
||||
требовала их задокументировать): визит принадлежит одной куке; пауза дольше 30
|
||||
минут рвет его надвое; граница модельных суток режет визит, события за полночь
|
||||
в день не попадают. Единственное исключение — визит с заказом, обещанным
|
||||
планом двухкуковых пар: его старт сдвигается назад, чтобы воронка уместилась в
|
||||
сутки. Это принятое ограничение модели, а не недосмотр: обещание плана —
|
||||
гарантия, на которой стоит лаба склейки, и ради нее свобода старта сужена.
|
||||
Цена названа и мала: около 24 визитов из ~9,5 тыс. в день никогда не
|
||||
начинаются в последние минуты суток. Правила лежат в докстринге `day.py`, а
|
||||
тест заново собирает визиты и сверяет их с `VisitID`.
|
||||
- **Недельная волна применяется один раз** (#39, 2026-08-02). Профиль ведет
|
||||
приток, а трафик наследует его через дневную аудиторию; измеренный размах
|
||||
трафика — ±7% против ±10% у притока. Второе умножение удвоило бы недельный
|
||||
размах. Если недельной лабе не хватит сигнала, нужен отдельный слабый профиль
|
||||
активности, а не повторное умножение: «приходит меньше новых людей» и «каждый
|
||||
ходит меньше» — разные механизмы.
|
||||
- **Торговый хвост умещается в сутки резервом, а не зажимом** (#40, решение
|
||||
владельца до реализации, 2026-08-02). Событие покупки встает на несколько
|
||||
секунд позже страницы подтверждения, корзина — позже карточки, а граница суток
|
||||
режет вышедшее за нее. Визит с обещанным заказом уже был подвинут так, чтобы
|
||||
воронка уместилась в сутки, но без учета торгового хвоста. Резерв расширен на
|
||||
хвост: зажим к последней секунде сломал бы правило задержки ровно в
|
||||
пограничном случае. Визит со срезанным полуночью подтверждением покупки не
|
||||
дает.
|
||||
- **Торговый хвост разводит соседние визиты** (#40, 2026-08-02): таймаут
|
||||
считается от последнего события, а не страницы. Иначе лаба сессий склеила бы
|
||||
два визита и разошлась с `VisitID`; решение поймано тестом сборки визитов.
|
||||
- **Полночь забирает подтверждение вместе с покупкой** (#40, 2026-08-02).
|
||||
Сначала резерв считался только для обещанных планом заказов, поэтому у вольной
|
||||
покупки оставалось окно: подтверждение дожило до потока, а покупка вышла за
|
||||
полночь. Это создало бы потерю на клиентской стороне по недосмотру. Теперь
|
||||
запас есть у всякой страницы подтверждения: либо за границу суток уходят обе
|
||||
строки, либо остаются обе. В каноническом мире этот случай за 14 дней не
|
||||
выпал.
|
||||
- **Задержка торгового события короче самой короткой паузы между страницами**
|
||||
(#40, 2026-08-02). Событие корзины не обгоняет страницу кнопки и не выходит за
|
||||
полночь: за ним в визите всегда есть страница, пережившая полночь.
|
||||
|
||||
- **Правила резки визитов** (мастер-спека, раздел 1.2 требовала их
|
||||
задокументировать): визит принадлежит одной куке; пауза дольше 30 минут
|
||||
рвёт визит надвое; граница модельных суток режет визит, события за
|
||||
полночь в день не попадают. Единственное исключение — визит с заказом,
|
||||
обещанным планом двухкуковых пар: его старт сдвигается назад, чтобы
|
||||
воронка уместилась в сутки. Это принятое ограничение модели, а не
|
||||
недосмотр: обещание плана — гарантия, на которой стоит лаба склейки, и
|
||||
ради неё мы сужаем свободу старта. Цена названа и мала: около 24 визитов
|
||||
в день никогда не начинаются в последние минуты суток (из ~9,5 тыс.).
|
||||
Правила лежат в докстринге `day.py`, и тест собирает визиты по ним
|
||||
заново, сверяя сборку с `VisitID`.
|
||||
- **Числа дня**: ≈1,4 визита на день активности куки и ≈4,8 страницы на
|
||||
визит. Воронка при исполнении #39 была общей для всех: 8% визитов до
|
||||
корзины → 45% из них до оформления → 55% из них до подтверждения, то есть
|
||||
конверсия визита ~2% при 9,5 тыс. визитов и ~45 тыс. pageview в средний
|
||||
день. При исполнении #40 воронка развелась надвое (блок ниже): обычному
|
||||
посетителю 6% до корзины и те же 45% и 55%, помеченному покупателю — 18%
|
||||
и 60%. Перемеренный средний день: 9 879 визитов, 47 433 pageview, 4,80
|
||||
страницы на визит, конверсия визита 2,4%; полные числа — в блоке #40,
|
||||
оттуда же они лягут в опись.
|
||||
- **Недельная волна применяется один раз.** Профиль ведёт приток, трафик
|
||||
наследует его через дневную аудиторию; измеренный размах трафика — ±7%
|
||||
против ±10% у притока. Второе умножение удвоило бы недельный размах. Если
|
||||
недельной лабе однажды не хватит сигнала, принципиальный путь — отдельный,
|
||||
более слабый профиль активности, а не повторное применение профиля
|
||||
притока: механизмы разные, и «на выходных приходит меньше новых людей» —
|
||||
не тот же факт, что «на выходных каждый ходит меньше».
|
||||
- **`ParsedParamsKey1` остаётся пустой** — это отложенное решение, а не
|
||||
дыра: вариант A/B-теста был бы постоянной куки, а не поведением дня, и
|
||||
заводится тикетом #47.
|
||||
- **Шов для #40**: день отдаёт, кроме колонок, два выровненных по строкам
|
||||
ряда — какая это страница магазина и какой товар показывала карточка. По
|
||||
ним #40 знает, куда вешать торговое событие и что посетитель на самом
|
||||
деле смотрел: товар в корзине, которого никто не открывал, — видимая
|
||||
глупость в воронке. Визит с назначенным заказом всегда доходит до
|
||||
`/confirmation`, а перед корзиной у него всегда есть карточка. Своей
|
||||
случайности #39 у #40 не занимает: подпоток `COMMERCE` не тронут.
|
||||
### Каталог, география и паспорт куки
|
||||
|
||||
Решено при исполнении #40 (2026-08-02) — решения владельца до реализации.
|
||||
Три решения выходят за границы тикета: правятся план состава (#38), воронка
|
||||
(#39) и каталог товаров (#39). Сейчас это дёшево — опись ещё не собрана
|
||||
(#42); позже обошлось бы её пересборкой. Часть решений принята после
|
||||
холодного ревью первой редакции блока.
|
||||
- **Каталог товаров заводится в #39, а не в #40** (#39, решение владельца до
|
||||
реализации, 2026-08-02). Карточке нужен `Title`, а имя товара живет в
|
||||
каталоге: без каталога #39 сделал бы лишь половину трафика.
|
||||
`data/catalog/products.csv` (`sku`, `name`, `category`, `brand`, `price`)
|
||||
общий для генератора и словаря ClickHouse; #40 получает его готовым. Решена
|
||||
форма, а не длина: артикул — четыре буквы категории и четыре цифры, цена —
|
||||
целые копейки, категорий шесть. Строки дописываются механически, поэтому ни
|
||||
код, ни тесты их не считают; товар для карточки выбирается равномерно внутри
|
||||
категории.
|
||||
- **Ассортимент — непродовольственная розница** (#39, решение владельца до
|
||||
реализации, 2026-08-02): товары для дома, текстиль, посуда, мелкая бытовая
|
||||
техника, детское, одежда и обувь, ~150–200 sku. Довод не вкусовой: под этот
|
||||
профиль приняты месячный цикл повторной покупки, 75% одноразовых кук и
|
||||
конверсия ~2% на визит. Продуктовая сеть потребовала бы других чисел и
|
||||
переоткрыла бы #38.
|
||||
- **География — один регион присутствия** (#39, решение владельца до реализации,
|
||||
2026-08-02): Поволжье с центром в Самаре, города региона и тонкий хвост
|
||||
остальной страны. «Топ городов России» дал бы магазину с одним складом карту,
|
||||
которой у него не бывает.
|
||||
- **Паспорт куки хранит план состава на всю ее жизнь** (#39, решение владельца
|
||||
до реализации, 2026-08-02); пара получает один город и разные устройства
|
||||
(«телефон и ноутбук», мастер-спека, раздел 5), а броски паспорта стоят
|
||||
последними — [ADR 0016](../adr/0016-cookie-passport.md). Прежние счетчики
|
||||
пересчитаны при #40; актуальные числа — в таблице замера #50.
|
||||
- **Справочники — таблицы-литералы** (#39, решение владельца до реализации,
|
||||
2026-08-02) рядом с конфигурацией мира; Faker и mimesis не подключаются. Нужны
|
||||
связки вроде «город → id региона → часовой пояс» и «телефон → Safari → iOS →
|
||||
экран»: генератор строк их не дает, таблицу пришлось бы написать все равно, а
|
||||
обновление зависимости молча меняло бы мир.
|
||||
- **Гео-id — настоящие числа геобазы Яндекса** (#39, решение владельца до
|
||||
реализации; проверка 2026-08-02). Id подтверждались обращением к живым
|
||||
сервисам Яндекса по тому же номеру (`yandex.ru/pogoda/<id>`,
|
||||
`yandex.ru/maps/225/russia/`) — страница открывает ожидаемое место. Оговорка
|
||||
стоит в коде: опубликованной таблицы геобазы найти не удалось, а что
|
||||
`RegionCityID` Метрики нумерует регионы той же геобазой — обоснованное
|
||||
допущение, не подтвержденный источником факт. Выдуманные по памяти числа не
|
||||
годятся ни в каком случае.
|
||||
- **IP-адреса — нероутируемые диапазоны** (#39, решение владельца до реализации,
|
||||
2026-08-02): сети RFC 5737 и 198.18/15 по ломтю на город, телефонам — CGNAT
|
||||
100.64/10, как у настоящих операторов. Правдоподобные публичные адреса
|
||||
принадлежат живым организациям, и в учебных данных им не место.
|
||||
- **Вариант товара — справочник мира** (#40, 2026-08-02). Сырой `ecommerce`
|
||||
несет `variant`, которого нет в плоских колонках; строки живут рядом со
|
||||
справочниками устройств и городов, а не сочиняются на месте.
|
||||
|
||||
- **Корзина шире заказа.** Посетитель кладёт в корзину товары тех карточек,
|
||||
которые открывал в этом визите, а покупает не всё: около 15% позиций
|
||||
остаются брошенными. Иначе событие корзины не рассказывало бы ничего сверх
|
||||
покупки — заказ был бы её точной копией, и сравнивать было бы нечего.
|
||||
Замерено на мире до правок: у покупающего визита 2,1 карточки, в заказе
|
||||
выходит 1,9 позиции, однопозиционных заказов около половины. Карточка,
|
||||
открытая в визите дважды, даёт одну позицию, а не две: повторный просмотр —
|
||||
раздумье, а не второй товар. Количество штук — свой бросок: обычно одна,
|
||||
изредка две-три. Отклонено: *заказ всегда из одного товара* — список
|
||||
позиций как урок умирает, а на нём стоит единственный носитель навыка
|
||||
«вложенный JSON» (мастер-спека, раздел 2); *досыпать в корзину товары,
|
||||
которых посетитель не открывал* — ломает шов #39 ровно там, ради чего он
|
||||
строился; *бросать много* (40% и выше) — две трети заказов схлопываются в
|
||||
один товар.
|
||||
- **Покупатель перестаёт быть словом — двумя рычагами сразу.** Метка плана
|
||||
(«этот человек склонен покупать», 5% людей когорты) до сих пор ни на что не
|
||||
влияла: кто купит, решал одинаковый для всех бросок в дне. Теперь
|
||||
помеченный, во-первых, доходит до заказа заметно чаще прочих, во-вторых,
|
||||
дольше живёт и чаще возвращается. Первый рычаг — про правдоподобие:
|
||||
склонность покупать и вправду свойство человека, а не свойство визита.
|
||||
Второй — про видимость: метки в событии нет и не будет (кликстрим
|
||||
анонимен), поэтому «постоянный покупатель» читается в данных только как
|
||||
кука, которая ходит неделями и покупает не раз. Одним лифтом это
|
||||
недостижимо: кука живёт меньше двух визитов за снимок, и второй покупке
|
||||
негде случиться — лифт поднял бы долю кук с двумя покупками с 2% до 6% и на
|
||||
том исчерпался. Рычаг жизни лежит в плане (#38), рычаг конверсии — в
|
||||
воронке (#39); правятся оба. Отличается помеченный на обоих шагах воронки:
|
||||
и до корзины доходит чаще, и бросает её реже. В жизни различаются оба — кто
|
||||
пришёл смотреть, тот и кладёт реже, и до конца доводит реже; один шаг дал
|
||||
бы половину картины. Числа под каждый шаг — калибровка: помеченному 18% до
|
||||
корзины и 60% из корзины в оформление против 6% и 45% у обычного
|
||||
посетителя. Обычного калибровка тоже задела — его доля до корзины опущена
|
||||
с 8% до 6%, и это единственное число мира, изменённое вне двух рычагов.
|
||||
Довод: помеченные живут дольше и занимают уже 12% дневной аудитории вместо
|
||||
5% людей когорты, поэтому при прежних 8% конверсия мира ушла бы к 2,9% —
|
||||
за вилку. Рычаг метки не должен оплачиваться ростом конверсии, и границы
|
||||
правки — вилки раздела 5: дневная аудитория остаётся в пределах
|
||||
6–8 тыс., средний день — около 50 тыс. событий, конверсия визита —
|
||||
около 2%. Расти внутри вилки аудитории
|
||||
разрешено: помеченные ходят чаще, и это по-прежнему правдоподобный магазин.
|
||||
Измерено после правки (канонический мир, 14 дней): в средний день
|
||||
аудитория 7 166, визитов 9 879, событий 49 509, визитов с корзиной 765
|
||||
(7,7%) и с покупкой 240 (2,4%) — все три вилки выдержаны. База до правки
|
||||
была: аудитория 6 710, визитов 9 249, событий 44 436, визитов с корзиной
|
||||
780 (8,4%) и с покупкой 221 (2,4%). Числа состава мира (блок #38 выше)
|
||||
перемерены тем же прогоном. Уточнение к разделу 1: вольные покупки
|
||||
по-прежнему решает день, но
|
||||
случайность в них больше не одинакова для всех. Отклонено: *оставить
|
||||
монетку* — «покупатель» остаётся именем без следа в данных; *один лифт без
|
||||
долгой жизни* — довод выше, повторных покупок он почти не прибавляет;
|
||||
*покупают только помеченные* — 5% людей дают около 40 заказов в день вместо
|
||||
220, вернуть их можно, лишь подняв долю покупателей до 10–30% людей, а с
|
||||
ней вырастет и число двухкуковых пар.
|
||||
- **Событие корзины садится на карточку товара**, событие покупки — на
|
||||
страницу подтверждения. В жизни событие корзины шлётся нажатием кнопки на
|
||||
карточке, а не открытием страницы корзины; при нескольких товарах в заказе
|
||||
иначе его и не разложить — событий столько, со скольких карточек положили.
|
||||
- **Номер заказа читаемый**: день модельного времени и порядковый номер
|
||||
покупки в этом дне (`20260603-0042`). Магазины так и нумеруют, и по номеру
|
||||
сразу видно день. Он же — `order_id` бэкенда: обе стороны получают один
|
||||
номер, по нему соединяется сверка (мастер-спека, раздел 4). Нумеруются все
|
||||
покупки, дошедшие до потока дня, в порядке событий — до всяких потерь.
|
||||
Расхождения этапа 6 выбрасывают событие, когда номер уже присвоен: иначе
|
||||
одна потеря перенумеровала бы чужие заказы, и мост к бэкенду разъехался бы.
|
||||
- **Промокод в событии есть, скидки в сумме нет.** Часть заказов уходит с
|
||||
промокодом, но выручка, которую шлёт клиент, — сумма позиций без скидки и
|
||||
доставки. Так и в жизни: код на сайте знает корзину, а не итог расчёта.
|
||||
Класса расхождения промокод не даёт: сверка приведена к `items_total`, где
|
||||
скидки и нет (мастер-спека, раздел 7). Он объясняет разрыв между клиентской
|
||||
суммой и итогом заказа — одну из причин, по которым «суммы не сойдутся»
|
||||
(мастер-спека, раздел 4). Таблица «код → скидка» — число мира и живёт
|
||||
здесь: этап 3 берёт её готовой, а не сочиняет заново. Отклонено:
|
||||
*промокода нет никогда* — колонка формата осталась бы мёртвой.
|
||||
- **Деньги: копейки внутри, рубли в колонках, дробь только в выручке — и
|
||||
копейки в части цен каталога.** Генератор считает целыми копейками; цена
|
||||
товара в событии — целые рубли, как у Метрики (`productPrice` там `Int64`);
|
||||
дробное число одно — `purchaseRevenue` типа `Float64`. Все 180 цен каталога
|
||||
кратны рублю, и это делало урок про Float64 беспредметным: округлять
|
||||
нечего, копейки существуют только на словах, а расхождение сумм пришлось бы
|
||||
выдумывать вероятностью. Поэтому часть цен получает копейки (1 299,90 ₽ —
|
||||
обычная розница): `productPrice` округляется самим форматом, а
|
||||
`purchaseRevenue` несёт точную сумму. Разрыв живёт внутри события — считать
|
||||
выручку по разобранным массивам нельзя, и это настоящий урок формата, а не
|
||||
придуманный.
|
||||
- **Массивы `purchase*` и `product*` разной длины между собой.** Внутри своей
|
||||
группы длина общая: `purchase*` — по элементу на заказ (у нас всегда один),
|
||||
`product*` — по элементу на товар. Мастер-спека (раздел 1) говорит о
|
||||
«параллельных массивах одной длины», и прочесть это как «все массивы
|
||||
события одной длины» легко — поэтому в описании выгрузки сказано прямо.
|
||||
- **Сырой `ecommerce` несёт больше, чем плоские колонки.** Лаба «сырое против
|
||||
разобранного» (мастер-спека, раздел 1.2) имеет смысл, только если в сыром
|
||||
лежит то, чего в массивах нет: бренд товара, его вариант, блок
|
||||
`actionField` целиком. Собирает строку канонический сериализатор — руками
|
||||
это второй сериализатор со своим экранированием; зависимость `orjson`,
|
||||
названная рабочим выбором в разделе 4, появляется здесь, а не в #41.
|
||||
- **Торговый хвост умещается в сутки резервом, а не зажимом.** Событие
|
||||
покупки встаёт на несколько секунд позже просмотра страницы подтверждения,
|
||||
событие корзины — позже своей карточки, а граница суток режет всё, что за
|
||||
неё вышло. Визит с обещанным планом заказом уже подвинут так, чтобы
|
||||
уместиться в сутки (#39), но подвинут по страницам — торгового хвоста в том
|
||||
расчёте не было. Резерв расширяется на хвост: тогда правило «покупка на
|
||||
секунды позже подтверждения» держится всегда, а зажим к последней секунде
|
||||
ломал бы его ровно там, ради чего написан. Визит, у которого страница
|
||||
подтверждения срезана полуночью (1,2% покупающих), покупки не даёт — заказа
|
||||
не было.
|
||||
- **Счётчика покупок в плане состава не заводится.** Тикет просил «счётчики
|
||||
покупок и двухкуковых пар сходятся с планом», но покупок план не считает и
|
||||
считать не будет: раздел 1 обещает до генерации только состав — посетителей,
|
||||
приток, пары, — а торговые счётчики складываются из поведения дня. Квота на
|
||||
покупки заставила бы день добирать до точного числа, и конверсия в данных
|
||||
вышла бы подозрительно ровной — условность видна там, куда лаба смотрит.
|
||||
Сходятся пары; покупки проверяются постфактум по сгенерированному дню и по
|
||||
вилке конверсии (~2% на визит).
|
||||
### Корзина и состав заказа
|
||||
|
||||
Решено при исполнении #40 самой реализацией:
|
||||
После приемки #40 (2026-08-02) на грилинге «просмотр → корзина» нашли два
|
||||
точных равенства. В живых данных таких равенств нет: аналитик прочитает
|
||||
их как склейку разметки вместо поведения людей.
|
||||
|
||||
- **Торговый хвост разводит и соседние визиты, а не только сутки.** Правило
|
||||
резки требует, чтобы визиты одной куки стояли дальше таймаута; считать это
|
||||
расстояние от последней страницы, когда последним событием визита бывает
|
||||
покупка, значило отдать таймауту торговый хвост — лаба сессий склеила бы
|
||||
два визита в один и разошлась бы с `VisitID`. Расстояние считается от
|
||||
последнего события; поймано тестом сборки визитов.
|
||||
- **Полночь забирает подтверждение вместе с его покупкой.** Резерв суток
|
||||
сначала считался только для визитов с обещанным планом заказом, и у
|
||||
вольной покупки оставалось окно в несколько секунд: страница
|
||||
подтверждения дожила до потока, а событие покупки за полночь вышло. Это
|
||||
была бы потеря на клиентской стороне — класс этапов 4 и 6, заведённый по
|
||||
недосмотру. Теперь запас под торговый хвост есть у всякой страницы
|
||||
подтверждения: либо она уходит за границу суток вместе с покупкой, либо
|
||||
остаются обе. Это ровно то правило, которое блок выше называет словами
|
||||
«визит, у которого страница подтверждения срезана полуночью, покупки не
|
||||
даёт»; в каноническом мире числа от правки не сдвинулись — случай не
|
||||
выпал ни разу за 14 дней.
|
||||
- **Задержка торгового события короче самой короткой паузы между
|
||||
страницами.** Тогда событие корзины не обгоняет страницу, на которой
|
||||
посетитель нажал кнопку, и не выходит за полночь: за ним в том же визите
|
||||
всегда идёт страница, которая полночь пережила.
|
||||
- **Номера торговых событий — из торгового подпотока.** `WatchID` строки
|
||||
корзины и покупки рисует `COMMERCE`: возьми их у трафика, и правка
|
||||
торгового поведения сдвинула бы трафиковые номера. Ряды двух подпотоков
|
||||
проверяются на непересечение — обещание уникальности держит дедупликацию
|
||||
при переигровке дня, и предполагать его нельзя.
|
||||
- **Вариант товара — справочник мира.** Сырой `ecommerce` несёт `variant`,
|
||||
которого в плоских колонках нет; строки живут в справочниках рядом с
|
||||
устройствами и городами, а не сочиняются на месте.
|
||||
- **Корзина шире заказа** (#40, решение владельца до реализации, 2026-08-02).
|
||||
Посетитель кладет товары открытых карточек, а покупает не все. Иначе событие
|
||||
корзины ничего не рассказывало бы сверх покупки: заказ был бы ее точной
|
||||
копией. Карточка, открытая дважды, дает одну позицию — повторный просмотр
|
||||
означает раздумье, а не второй товар. Количество штук — отдельный бросок:
|
||||
обычно одна, изредка две-три. Отклонено: *заказ всегда из одного товара* —
|
||||
список позиций как урок умирает, а на нем стоит единственный носитель навыка
|
||||
«вложенный JSON» (мастер-спека, раздел 2); *досыпать неоткрытые товары* —
|
||||
ломается шов #39; *бросать много позиций* — большинство заказов схлопывается в
|
||||
один товар. Доля брошенных позиций позже пересмотрена в #50.
|
||||
- **Событие корзины садится на карточку товара** (#40, решение владельца до
|
||||
реализации, 2026-08-02), событие покупки — на подтверждение. В жизни корзину
|
||||
шлет кнопка на карточке, а при нескольких товарах события иначе не разложить
|
||||
по открытым карточкам.
|
||||
- **Корзина отвязана от страницы `/cart`** (#40, 2026-08-02). До правки
|
||||
множества визитов с событием корзины и со страницей `/cart` точно совпадали;
|
||||
пересчитано при #50, актуальные числа — в таблице замера #50. Теперь положить
|
||||
может любой визит, открывший карточку; `/cart` остается шагом воронки, а заказ
|
||||
по-прежнему рисуется только после подтверждения. У визита на `/cart` всегда
|
||||
есть хотя бы одна позиция — страница пустой корзины невозможна, — но открыть
|
||||
корзину после добавления необязательно. Так в мире появляются те, кто положил
|
||||
товар и не открыл корзину, — массовый сюжет магазина. Ошибка прошла нарезку,
|
||||
потому что воронку #39 описали страницами, решение #40 — событиями, а вопрос
|
||||
«какой визит кладет» заново не задали: страничное правило молча переехало на
|
||||
уровень событий.
|
||||
- **Намерение визита берется из шага воронки, а не из метки покупателя** (#50,
|
||||
2026-08-02). Визит на `/cart` кладет почти все сравненное, прочий — редко.
|
||||
Метка уже влияет через воронку; второе слагаемое посчитало бы ее дважды и
|
||||
протянуло бы план состава через шов страниц и товаров. Плата: у визита без
|
||||
намерения добавление не связано со свойством человека, но склонные покупать и
|
||||
так преобладают среди дошедших до `/cart`.
|
||||
- **Доля брошенных позиций опущена с 15% до 10%** (#50, 2026-08-02). Корзина
|
||||
сузилась с 2,4 позиции до 1,7, и прежние 15% чаще выносили ее целиком:
|
||||
спасенная позиция возвращала заказ к одному товару, и однопозиционных заказов
|
||||
набиралось под 60%. Свойство сохранено, число перемерено — ровно так, как блок
|
||||
#40 и оговаривал.
|
||||
|
||||
Решено при исполнении #40 после приёмки (2026-08-02) — грилинг тождества
|
||||
«просмотр → корзина». Приёмка нашла два точных равенства: каждое в живых
|
||||
данных не встречается и потому читается аналитиком как склейка в разметке,
|
||||
а не как поведение людей.
|
||||
### Спрос товара и границы калибровки
|
||||
|
||||
- **Корзина отвязана от страницы корзины.** Замерено до правки на всех 14
|
||||
днях канонического мира: множества «визиты с событием корзины» и «визиты
|
||||
со страницей `/cart`» совпадали в точности — 828 и 828 на дне 2, и так
|
||||
каждый день без единого расхождения. Событие корзины
|
||||
возникало только у визита, дошедшего до воронки, то есть факт добавления
|
||||
идеально предсказывался более поздним просмотром страницы. Теперь
|
||||
положить может любой визит, открывший карточку; страница `/cart`
|
||||
остаётся шагом воронки и решается как прежде, а заказ по-прежнему
|
||||
рисуется только у визита, дошедшего до подтверждения. Оговорка, без
|
||||
которой появилась бы новая ложь вместо старой: у визита, дошедшего до
|
||||
`/cart`, хотя бы одна позиция в корзине есть всегда — страница корзины
|
||||
при пустой корзине невозможна. Брошенные корзины в мире были и до правки
|
||||
(в том же дне 828 корзин против 272 покупок); правка добавляет к ним
|
||||
тех, кто положил и корзину не открывал, — самый массовый сюжет магазина.
|
||||
Почему это проехало при нарезке: воронка #39 записана в терминах
|
||||
страниц, #40 принял решение про события («событие корзины садится на
|
||||
карточку»), но вопрос «какой визит вообще кладёт» заново не задал —
|
||||
страничное правило молча переехало на уровень событий.
|
||||
- **Класть или нет решает и товар, а не только визит.** До правки в
|
||||
корзину ложились все карточки покупающего визита: внутри воронки 100%,
|
||||
вне её 0%. Привлекательность товара измерить было нечем — конверсия
|
||||
«просмотр → корзина» по sku давала чистый шум, а вопрос «какие товары
|
||||
кладут чаще» ответа не имел. Теперь у товара есть уровень спроса:
|
||||
колонка каталога с тремя значениями (магнит, обычный, залёживается), а
|
||||
вероятности трёх уровней — числа мира рядом с остальными. Отклонено:
|
||||
*свой процент у каждого из 180 sku* — рукописные числа, которых никто не
|
||||
обоснует, а каталог по решению #39 дописывается механически; *выводить
|
||||
склонность из цены* — жёсткая связь, в которой менти найдёт ровно то,
|
||||
что мы в неё вложили, и ничего сверх.
|
||||
- **Границы правки — те же вилки раздела 5**: аудитория 6–8 тыс., средний
|
||||
день около 50 тыс. событий, конверсия визита около 2%. Заказ рисуется из
|
||||
корзины, а корзина сузилась, поэтому доля брошенных позиций и состав
|
||||
заказа перемеряются вместе с остальным: сохраняется не число, а
|
||||
свойство — заказ уже корзины и однопозиционных заказов около половины.
|
||||
- **Класть или нет решает и товар, а не только визит** (#40, 2026-08-02). Раньше
|
||||
внутри воронки клали все карточки, вне ее — ни одной: конверсия по sku давала
|
||||
чистый шум, и вопрос «какие товары кладут чаще» не имел ответа. Теперь в
|
||||
каталоге три уровня спроса: магнит, обычный и залеживающийся, а их вероятности
|
||||
— числа мира. Отклонено: *свой процент у каждого из 180 sku* — необоснованные
|
||||
рукописные числа в механически дополняемом каталоге; *выводить спрос из цены*
|
||||
— жесткая связь, в которой менти найдет только вложенное авторами правило.
|
||||
- **Два ряда вероятностей, а не ряд с множителем** (#50, 2026-08-02): один для
|
||||
визита с намерением, другой для смотрящего. У намеренного визита вероятности
|
||||
высоки, иначе корзина и заказ схлопываются до одного товара; возле 100%
|
||||
множитель съел бы разницу уровней и сделал бы конверсию уровней одинаковой.
|
||||
Разрыв внутри рядов разный: у смотрящего товар решает почти все, а у
|
||||
пришедшего покупать заслонен намерением.
|
||||
- **Ни одному уровню спроса нельзя дать нулевую вероятность** (#50, 2026-08-02).
|
||||
Ноль у залежавшегося товара вернул бы исходное равенство на четверти каталога:
|
||||
добавление случалось бы только внутри визита с `/cart`. Отвязка либо относится
|
||||
ко всему ассортименту, либо это не отвязка. Поэтому уровни разводятся не нулем
|
||||
внизу второго ряда, а ослаблением залежавшегося товара в ряду визита с
|
||||
намерением: пришедший покупать берет его реже ходового.
|
||||
- **Уровень спроса — колонка `demand` в конце файла каталога** (#50,
|
||||
2026-08-02), значения словами («магнит», «обычный», «залёживается»). Раздан он
|
||||
одной колодой на весь каталог: доли по категориям расходятся сами (магнитов от
|
||||
четырёх до девяти на тридцать строк). Раскладывать колоду поровну в каждую
|
||||
категорию было бы той же неправдоподобной ровностью, какую тикет вычищал из
|
||||
данных. Связь с ценой отклонена выше, и тест сторожит её отсутствие — по
|
||||
четвертям цены и по медиане, потому что одни только края списка обманываются
|
||||
четырьмя исключениями. Хвост соседям: словарь ClickHouse над этим файлом ещё
|
||||
не построен (#37/#43) — колонку надо взять в его описание сразу, а не догонять
|
||||
правкой; описи нужен хеш каталога (хвост #42) тем более, потому что теперь в
|
||||
файле живёт ещё и поведение.
|
||||
- **Доля просмотров карточек, доходящих до корзины, выросла с 7,3% до 8,6%**
|
||||
(#50, 2026-08-02) и подошла к потолку вилки. Визитам вне `/cart` нужны свои
|
||||
позиции, а визит с намерением должен класть почти все сравненное; обе границы
|
||||
тянут число вверх, и опустить долю можно лишь ценой одной из них. Место до 9%
|
||||
невелико, следующая правка торговой половины считается против него.
|
||||
- **Границы правки — вилки раздела 5** (#40, 2026-08-02): аудитория 6–8 тыс.,
|
||||
средний день около 50 тыс. событий, конверсия визита около 2%. Корзина и
|
||||
состав заказа перемеряются вместе; сохраняется свойство — заказ уже корзины,
|
||||
однопозиционных заказов около половины.
|
||||
- **Вилка 5–9% — ориентир, а не граница** (#50, решение владельца 2026-08-02). У
|
||||
живых магазинов эта доля гуляет широко; пока она правдоподобна, сама по себе
|
||||
ничего не сторожит, и подгонять поведение под ее край не надо. Настоящий
|
||||
предел считается в другой валюте — дневной бюджет событий, на котором стоит
|
||||
опись #42. Если будущая правка упрется в него, двигать надо бюджет и его
|
||||
причины, а не долю.
|
||||
|
||||
**Измерено после правки #50** (канонический мир, 14 дней; правый столбец —
|
||||
то же до правки). Доли по уровням и состав заказа считаются по окну снимка
|
||||
целиком, а не как среднее дневных долей: заказов в дне около 240, и на одном
|
||||
дне такая доля гуляет на несколько пунктов от одной случайности выборки.
|
||||
Трафиковая половина не сдвинулась ни на строку: набор просмотров страниц
|
||||
сверен со снимком, снятым до первой правки.
|
||||
### Номер заказа, промокод и деньги
|
||||
|
||||
- **Номер заказа — день и порядковый номер покупки** (#40, решение владельца до
|
||||
реализации, 2026-08-02): событийная и бэкенд-сторона берут один номер из плана
|
||||
— [ADR 0017](../adr/0017-order-numbering.md); по нему соединяется сверка
|
||||
(мастер-спека, раздел 4).
|
||||
- **Промокод в событии есть, скидки в сумме нет** (#40, решение владельца до
|
||||
реализации, 2026-08-02). Клиент шлет сумму позиций без скидки и доставки,
|
||||
поэтому код объясняет разрыв с итогом заказа. Класса расхождения промокод не
|
||||
дает: сверка приведена к `items_total`, где скидки нет (мастер-спека, раздел
|
||||
7). Код объясняет одну из причин, по которым «суммы не сойдутся»
|
||||
(мастер-спека, раздел 4). Так и в жизни: код на сайте знает корзину, а не
|
||||
окончательный расчет. Таблица «код → скидка» — число мира, которое этап 3
|
||||
берет готовым, а не сочиняет заново. Отклонено: *промокода нет никогда* —
|
||||
колонка формата осталась бы мертвой.
|
||||
- **Деньги: копейки внутри, рубли в колонках, дробь только в выручке** (#40,
|
||||
решение владельца до реализации, 2026-08-02). Генератор считает целыми
|
||||
копейками; цена товара в событии — целые рубли, как у Метрики (`productPrice`
|
||||
там `Int64`); дробное число одно — `purchaseRevenue` типа `Float64`. Все 180
|
||||
цен каталога были кратны рублю, и это делало урок про `Float64` беспредметным:
|
||||
округлять нечего, копейки существовали только на словах. Поэтому часть цен
|
||||
получила копейки (1 299,90 ₽ — обычная розница): `productPrice` округляется
|
||||
самим форматом, а `purchaseRevenue` несет точную сумму. Разрыв живет внутри
|
||||
события — считать выручку по разобранным массивам нельзя, и это настоящий урок
|
||||
формата, а не придуманный.
|
||||
|
||||
### Событийный шов, формат и случайность
|
||||
|
||||
- **Шов для #40** (#39, 2026-08-02): день отдает выровненные по строкам ряды
|
||||
страницы магазина и товара на карточке. По ним торговая половина знает, куда
|
||||
повесить событие и какой товар посетитель видел: товар в корзине, которого
|
||||
никто не открывал, был бы видимой ошибкой воронки. Назначенный заказ всегда
|
||||
доходит до `/confirmation`, перед корзиной есть карточка. #39 не занимает
|
||||
случайность #40: подпоток `COMMERCE` остается нетронутым.
|
||||
- **Номера торговых событий — из торгового подпотока** (#40, 2026-08-02).
|
||||
`WatchID` корзины и покупки рисует `COMMERCE`, поэтому правка торговли не
|
||||
сдвигает трафиковые номера. Ряды подпотоков проверяются на непересечение:
|
||||
уникальность нужна дедупликации при переигровке дня.
|
||||
- **Массивы `purchase*` и `product*` разной длины между собой** (#40, решение
|
||||
владельца до реализации, 2026-08-02). Внутри группы длина общая: `purchase*` —
|
||||
по элементу на заказ, здесь всегда один, `product*` — по элементу на товар.
|
||||
Мастер-спека (раздел 1) говорит о «параллельных массивах одной длины», и
|
||||
прочесть это как общую длину всех массивов события легко — поэтому в описании
|
||||
выгрузки сказано прямо.
|
||||
- **Сырой `ecommerce` несет больше, чем плоские колонки** (#40, решение
|
||||
владельца до реализации, 2026-08-02): бренд, вариант и `actionField` целиком.
|
||||
Лаба «сырое против разобранного» (мастер-спека, раздел 1.2) имеет смысл,
|
||||
только если в сыром есть сведения, которых нет в массивах. Строку собирает
|
||||
канонический `orjson`-сериализатор, а не второй ручной сериализатор со своим
|
||||
экранированием (рабочий выбор раздела 4).
|
||||
|
||||
### Замеры канонического мира
|
||||
|
||||
- **Измерено на собранном плане** (#38, перемерено при #40, 2026-08-02): приток
|
||||
3 828 кук в день в среднем, дневная аудитория 6 637–7 595 — обе величины в
|
||||
вилках раздела 5. Накопленная аудитория за 14 дней — 70,2 тыс. кук: 53,6 тыс.
|
||||
новых и 16,6 тыс. возвратов предыстории. У 192 пар оба назначенных заказа
|
||||
попали в снимок; остальные горизонт переживают. Числа считаются прогоном плана
|
||||
без событий. Предыдущий замер пересчитан при #40; актуальные числа — здесь и в
|
||||
таблице замера #50.
|
||||
- **Числа дня** (#39, перемерено при #40, 2026-08-02): ≈1,4 визита на день
|
||||
активности куки и ≈4,8 страницы на визит. Обычному посетителю дано 6% до
|
||||
корзины, затем 45% и 55%; помеченному покупателю — 18% и 60%. Средний день: 9
|
||||
879 визитов, 47 433 pageview, 4,80 страницы на визит, конверсия 2,4%.
|
||||
|
||||
**Измерено после правки #50** (2026-08-02; канонический мир, 14 дней; правый
|
||||
столбец — до правки). Доли по уровням и состав заказа считаются по всему окну, а
|
||||
не как среднее дневных долей: около 240 заказов в дне дают слишком шумную долю.
|
||||
Трафиковая половина не сдвинулась ни на строку.
|
||||
|
||||
| величина | стало | было |
|
||||
|---|---|---|
|
||||
@@ -868,191 +844,108 @@
|
||||
| брошено позиций в покупающих корзинах | 7,25% | 12,0% |
|
||||
|
||||
Знаменатель «просмотров карточек» здесь — только просмотры страниц. При
|
||||
постановке #50 в него попали и строки событий корзины: событие садится на
|
||||
карточку и несёт её же страницу, отчего вышло 26 903 просмотра и 6,82%
|
||||
вместо 25 067 и 7,32%. Величина одна и та же, счёт разный.
|
||||
постановке #50 в него попали и строки событий корзины: событие несет ту же
|
||||
страницу, отчего вышло 26 903 просмотра и 6,82% вместо 25 067 и 7,32%. Величина
|
||||
одна и та же, счет разный.
|
||||
|
||||
Решено при исполнении #50 самой реализацией:
|
||||
### Запуск, упаковка и приемники
|
||||
|
||||
- **Намерение визита берётся из шага воронки, а не из метки покупателя.**
|
||||
Кладёт всякий визит, открывший карточку, но дошедший до `/cart` кладёт
|
||||
почти всё, что сравнивал, а прочий — редко. Метка покупателя в это
|
||||
решение не заходит, и не потому, что не влияет: она уже влияет — через
|
||||
воронку, где помеченному дано 18% против 6%. Взять её вторым слагаемым
|
||||
значило бы посчитать одно и то же дважды и протянуть план состава сквозь
|
||||
шов, по которому день отдаёт торговой половине только страницы и товары.
|
||||
Плата названа: у визита без намерения добавление ни с чем в человеке не
|
||||
связано — упрощение, но не ложь, потому что склонные покупать и так
|
||||
преобладают среди дошедших до `/cart`.
|
||||
- **Два ряда вероятностей, а не ряд с множителем.** Уровень спроса читается
|
||||
дважды: ряд для визита с намерением и ряд для того, кто смотрит. Один ряд
|
||||
с множителем намерения не годится: вероятности визита с намерением обязаны
|
||||
быть высокими (иначе его корзина схлопывается до одной позиции, а заказ до
|
||||
одного товара), а у потолка в 100% разница уровней съедается, и конверсия
|
||||
по уровням перестаёт различаться. Разрыв уровней внутри рядов поэтому
|
||||
разный: у смотрящего товар решает почти всё, у пришедшего покупать —
|
||||
заслонён намерением.
|
||||
- **Доля брошенных позиций опущена с 15% до 10%.** Корзина сузилась с 2,4
|
||||
позиции до 1,7, и прежние 15% чаще выносили её целиком: спасённая позиция
|
||||
возвращала заказ к одному товару, и однопозиционных заказов набиралось под
|
||||
60%. Свойство сохранено, число перемерено — ровно так, как этот блок и
|
||||
оговаривал.
|
||||
- **Ни одному уровню спроса нельзя дать нулевую вероятность.** Ноль у
|
||||
залежавшегося товара выглядел заманчиво — он дешевле всего разводил
|
||||
конверсию уровней, — но по товарам этого уровня возвращал ровно то
|
||||
равенство, ради которого правка и делалась: событие корзины случалось бы
|
||||
только внутри визита, открывшего `/cart`, без единого исключения на
|
||||
четверти каталога. Отвязка либо про весь ассортимент, либо она не отвязка.
|
||||
Разводить уровни поэтому приходится не нулём внизу второго ряда, а
|
||||
ослаблением залежавшегося в первом: пришедший покупать берёт его реже
|
||||
ходового.
|
||||
- **Уровень спроса — колонка `demand` в конце файла каталога**, значения
|
||||
словами («магнит», «обычный», «залёживается»). Раздан он одной колодой на
|
||||
весь каталог: доли по категориям расходятся сами (магнитов от четырёх до
|
||||
девяти на тридцать строк). Раскладывать колоду поровну в каждую категорию
|
||||
было бы той же неправдоподобной ровностью, какую тикет вычищал из данных.
|
||||
Связь с ценой отклонена выше, и тест сторожит её отсутствие — по четвертям
|
||||
цены и по медиане, потому что одни только края списка обманываются
|
||||
четырьмя исключениями. Хвост соседям: словарь ClickHouse над этим файлом
|
||||
ещё не построен (#37/#43) — колонку надо взять в его описание сразу, а не
|
||||
догонять правкой; описи нужен хеш каталога (хвост #42) тем более,
|
||||
потому что теперь в файле живёт ещё и поведение.
|
||||
- **Доля просмотров карточек, доходящих до корзины, выросла с 7,3% до 8,6%
|
||||
и подошла к потолку вилки.** Иначе не сошлось: треть кладущих визитов вне
|
||||
`/cart` требует своих позиций, а состав заказа требует, чтобы визит с
|
||||
намерением клал почти всё сравненное. Обе границы тянут число вверх, и
|
||||
опустить его — значит отпустить одну из них. Место до 9% невелико, и
|
||||
следующая правка торговой половины считается против него.
|
||||
- **Вилка 5–9% — ориентир, а не граница** (решение владельца, 2026-08-02).
|
||||
Доля просмотров, доходящих до корзины, у живых магазинов гуляет широко:
|
||||
пока число правдоподобно, само по себе оно ничего не сторожит, и
|
||||
подгонять поведение под его край не надо. Настоящий предел здесь другой и
|
||||
считается в другой валюте — дневной бюджет событий: события корзины
|
||||
входят в те самые «около 50 тыс.», на которых стоит опись (#42). Упрётся
|
||||
будущая правка — двигать надо бюджет и его причины, а не долю.
|
||||
Первые два решения владелец принял на грилинге 7 августа 2026 года при #41;
|
||||
остальные приняты при исполнении #41 (2026-08-07).
|
||||
|
||||
Решено при исполнении #41 (2026-08-07). Первые два пункта — решения владельца
|
||||
грилингом 7 августа 2026 года, внесённые как есть; остальные приняты при
|
||||
исполнении.
|
||||
|
||||
- **Генератор живёт в своём контейнере**, и на этапе 5 даг будет запускать
|
||||
контейнер, а не импортировать пакет в процессе воркера. Довод учебный: так
|
||||
граница между оркестратором и задачей видна глазами — у задачи своё
|
||||
окружение, свой образ, свой процесс, а оркестратор передаёт параметры и
|
||||
читает результат. Это та же ступенька, с которой в бою вырастает
|
||||
`KubernetesPodOperator`. Побочно это единственный вариант, при котором
|
||||
«генератор — отдельная и заменяемая сущность» перестаёт быть словами: его
|
||||
зависимости не смешиваются с окружением Airflow. Хостовый `uv` остаётся для
|
||||
целей генератора — `make test`, `make lint` и `make typecheck` из каталога
|
||||
`generator/`. Цена названа и принимается:
|
||||
чтобы даг запускал контейнер, в Airflow пробрасывается сокет докера, а это
|
||||
доступ, равный root на хосте; на учебном стенде размен допустим, но
|
||||
записывается уроком — в бою так не делают. Отклонено: *генератор в образе
|
||||
Airflow, даг импортирует пакет* (так у стенда v1) — механика проще, но менти
|
||||
видит лишь «даг позвал функцию», а платить пришлось бы сменой базового
|
||||
образа всего стенда, переездом контекста сборки и смешением зависимостей;
|
||||
*`ExternalPythonOperator` — задача в отдельном venv внутри образа Airflow*:
|
||||
изоляцию даёт и сокета не требует, но границу видно в конфигурации, а не в
|
||||
`docker ps` — остаётся запасным путём, если проброс сокета однажды окажется
|
||||
неприемлемым.
|
||||
- **Клиент Kafka — `confluent-kafka`.** Он уже стоит в образе Airflow, на нём
|
||||
написан пробник `dags/test_kafka.py`, и его же использует официальный
|
||||
провайдер Kafka для Airflow: один клиент на стенде вместо двух. Объявлен
|
||||
необязательной группой зависимостей генератора (`[kafka]`), а образ ставит
|
||||
её из лока: приёмник импортирует клиента внутри функции, поэтому пакет без
|
||||
этой группы остаётся живым и переносимым — с файловым приёмником.
|
||||
- **Интерфейс запуска — `python -m clickstream_generator batch|live`.**
|
||||
Зовущий — контейнер: параметры приходят аргументами или переменными
|
||||
окружения (`GENERATOR_SEED`, `GENERATOR_DAY`, `GENERATOR_DAYS`,
|
||||
`GENERATOR_LIMIT`, `GENERATOR_SPEED`, `GENERATOR_FILE`,
|
||||
`KAFKA_BOOTSTRAP_SERVERS`, `KAFKA_TOPIC`), логи идут в стандартный вывод,
|
||||
итог виден кодом возврата. Общее у режимов: `--seed`, `--day`, `--days` и
|
||||
приёмник — `--file` либо `--brokers` с `--topic`. Состояния нет: позицию на
|
||||
оси ведёт зовущий (хвост этапу 5 — раздел 8). `--days` играет несколько
|
||||
дней подряд одним запуском — этим зальётся стартовый мир (#42), восемь
|
||||
запусков службы внутри `make up` были бы плохим ответом. Отклонено:
|
||||
*режимы флагом `--speed 0`* — различие тогда прячется за числом, тогда как
|
||||
у режимов разное устройство: у пакетного есть пачка и нет ожидания, у
|
||||
живого наоборот; *приёмник по умолчанию* — молчание однажды напишет в файл
|
||||
то, чего ждали в Kafka, поэтому не назван приёмник — ошибка, названы оба —
|
||||
- **Генератор живет в своем контейнере** (#41, 2026-08-07), и на этапе 5 даг
|
||||
запускает контейнер, а не импортирует пакет в воркере. Так менти видит
|
||||
отдельные окружение, образ и процесс задачи; зависимости не смешиваются с
|
||||
Airflow. Это ступенька, из которой в бою вырастает `KubernetesPodOperator`, и
|
||||
единственный вариант, где «генератор — отдельная и заменяемая сущность» виден
|
||||
глазами. Хостовый `uv` остается для `make test`, `make lint` и `make
|
||||
typecheck` из каталога `generator/`. Цена названа: сокет Docker в Airflow дает
|
||||
доступ уровня root на хосте; для учебного стенда это принято, в бою так не
|
||||
делают. Отклонено: *пакет в образе Airflow* — менти увидит лишь вызов функции,
|
||||
а стенд заплатит сменой базового образа, контекста сборки и смешением
|
||||
зависимостей; *`ExternalPythonOperator`* — изоляция есть и сокет не нужен, но
|
||||
граница видна в конфигурации, а не в `docker ps`. Это запасной путь, если
|
||||
сокет станет неприемлем.
|
||||
- **Клиент Kafka — `confluent-kafka`** (#41, 2026-08-07). Он уже есть в образе
|
||||
Airflow и пробнике `dags/test_kafka.py`, его использует официальный провайдер.
|
||||
Клиент объявлен необязательной группой `[kafka]`, а приемник импортирует его
|
||||
внутри функции, поэтому пакет живет и без Kafka — с файловым приемником. На
|
||||
стенде остается один клиент вместо двух, а образ ставит его из файла
|
||||
блокировки зависимостей.
|
||||
- **Интерфейс запуска — `python -m clickstream_generator batch|live`** (#41,
|
||||
2026-08-07). Параметры приходят аргументами или переменными окружения
|
||||
(`GENERATOR_SEED`, `GENERATOR_DAY`, `GENERATOR_DAYS`, `GENERATOR_LIMIT`,
|
||||
`GENERATOR_SPEED`, `GENERATOR_FILE`, `KAFKA_BOOTSTRAP_SERVERS`,
|
||||
`KAFKA_TOPIC`), логи идут в стандартный вывод, итог виден кодом возврата.
|
||||
Общие параметры — `--seed`, `--day`, `--days` и приемник `--file` либо
|
||||
`--brokers` с `--topic`. Состояния нет: позицию ведет зовущий (раздел 8).
|
||||
`--days` играет несколько дней одним запуском, чтобы стартовый мир не требовал
|
||||
восьми запусков службы. Отклонено: *режимы через `--speed 0`* — у пакетного
|
||||
режима есть пачка без ожидания, у живого — ожидание без пачки, а флаг прячет
|
||||
разное устройство за числом; *приемник по умолчанию* — молчание может написать
|
||||
в файл то, чего ждали в Kafka. Не назвать приемник — ошибка, назвать оба —
|
||||
тоже.
|
||||
- **Умолчание есть только у того, что описывает мир** (решение владельца при
|
||||
разборе ревью #41, 2026-08-07). Зерно (каноническое), число дней (1) и темп
|
||||
живого дня (×60) — свойства модели: они заданы спекой и этим тикетом, и
|
||||
умолчание здесь — тот же самый ответ. Позиция на оси (`--day`), адрес
|
||||
брокера, имя топика и путь файла — факты стенда, на котором генератор
|
||||
запущен; своего умолчания у них нет, и без них прогон падает с ненулевым
|
||||
кодом до первой сгенерированной строки. Довод: генератор — отдельная и
|
||||
переносимая сущность, факты про *этот* стенд обязан приносить зовущий
|
||||
(служба compose, цель `make`, даг этапа 5). Умолчание подменило бы забытый
|
||||
параметр чужим фактом и выдало бы неверный ответ за успех — даг `next_day`,
|
||||
не передавший день, переиграл бы нулевой, вышел бы с нулём, был бы записан
|
||||
удачей и подвинул позицию: мир и позиция разъехались бы молча. Отличие от
|
||||
`--days 1` названо прямо: единица измерения — минимальный *верный* ответ, а
|
||||
`0` в позиции — конкретный *неверный*, поданный тихо. Переменные окружения
|
||||
(`GENERATOR_DAY`, `KAFKA_TOPIC`) умолчаниями не считаются: это тот же
|
||||
зовущий, только другим каналом. Следствие для обвязки: имя топика и день
|
||||
служба compose и цели `make` передают явно.
|
||||
- **Ограниченная пачка — флаг `--limit N` пакетного режима**: потолок событий
|
||||
на весь прогон, а не на каждый день. Довод от менти: тот, кто хочет
|
||||
посмотреть на конвейер, не должен ждать полсотни тысяч событий. Сочетается
|
||||
с файловым приёмником, даёт взять срез, ещё не приезжавший в ODS, а число
|
||||
отправленного печатается всегда — счёт нужен обеим сторонам (на нём стоят
|
||||
проверки #43). На живой день пачка не распространяется: ожидание там
|
||||
снимает ускорение.
|
||||
- **Ключа у сообщения Kafka нет.** `WatchID` ключом был бы ключом только на
|
||||
вид: обещание Kafka про ключ — «сообщения одного ключа лежат в одном
|
||||
разделе и сохраняют порядок», а у ряда, где ключи не повторяются, обещать
|
||||
нечего; читатель же прочёл бы в нём смысл, которого нет. Осмысленный ключ
|
||||
здесь — `ClientID`, но раскладку по разделам стенд намеренно оставляет
|
||||
транспорту (раздел 2), и лаба «какая нода читала топик» живёт как раз тем,
|
||||
что раскладка не предрешена. Проверено 2026-08-07 (Context7 по
|
||||
`CONFIGURATION.md` librdkafka и снятие умолчаний с самой библиотеки,
|
||||
поставленной `confluent-kafka` 2.15.0, через `rd_kafka_conf_dump`):
|
||||
умолчание топикового `partitioner` — `consistent_random`, то есть у
|
||||
сообщения без ключа раздел случайный; `sticky.partitioning.linger.ms` по
|
||||
умолчанию 10 — окно, в котором подряд идущие сообщения без ключа липнут к
|
||||
одному разделу. Документация умолчаний не называет, поэтому оба числа взяты
|
||||
у библиотеки. Следствие: день целиком идёт секунды и ложится в оба раздела,
|
||||
а короткая пачка укладывается в одно окно и уезжает в один — поэтому «топик
|
||||
прочитан обеими нодами» проверяется днём и разово.
|
||||
- **Приёмников два, а режимов три.** «Kafka пачкой» и «Kafka с темпом ×60» —
|
||||
один и тот же приёмник у разных зовущих: темп держит проигрыватель, знающий
|
||||
время события. Приёмник, умеющий ускорение, снова знал бы о содержимом —
|
||||
ровно то, чего правило «глупых приёмников» (раздел 4) не хочет.
|
||||
- **Раскладка образа повторяет раскладку репозитория**: `/app` — корень,
|
||||
пакет лежит исходником в `/app/generator/src` и виден через `PYTHONPATH`,
|
||||
каталог товаров — в `/app/data`. Причина не в красоте: путь к каталогу
|
||||
считается от модуля вверх по дереву (`catalog.py`, `parents[3]`), а
|
||||
поставленный не editable пакет переехал бы в `site-packages` — и те же
|
||||
`parents[3]` указали бы внутрь `.venv`. Тогда каталог в образе есть, а
|
||||
модуль его не находит. Проверено запуском в контейнере; заодно день,
|
||||
сыгранный в образе, совпал побайтово с днём, сыгранным на машине.
|
||||
- **Умолчание есть только у того, что описывает мир** (#41, решение владельца
|
||||
2026-08-07). Каноническое зерно, `--days 1` и темп живого дня ×60 — свойства
|
||||
модели: умолчание здесь означает тот же верный ответ. `--day`, адрес брокера,
|
||||
топик и путь файла — факты стенда; без них прогон падает с ненулевым кодом до
|
||||
первой строки. Генератор отдельный и переносимый, поэтому сведения об этом
|
||||
стенде приносит зовущий — Compose, `make` или даг. Иначе забытый день мог бы
|
||||
переиграть D0, выйти с успехом, подвинуть позицию и молча разойтись с миром.
|
||||
Отличие от `--days 1` в том, что единица — минимальный верный ответ, а нулевой
|
||||
день в забытом параметре — конкретный неверный. Переменные окружения — другой
|
||||
канал зовущего, не умолчания; обвязка передает топик и день явно.
|
||||
- **Ограниченная пачка — `--limit N` пакетного режима** (#41, 2026-08-07):
|
||||
потолок событий на весь прогон. Она сочетается с файлом и дает быстро взять
|
||||
срез, еще не приезжавший в ODS, для знакомства с конвейером. Менти не должен
|
||||
ждать полный дневной поток, чтобы посмотреть путь данных. Число отправленного
|
||||
печатается всегда и нужно проверкам #43. На живой день ограничение не
|
||||
действует: ожидание там снимает ускорение.
|
||||
- **Ключа у сообщения Kafka нет** (#41, 2026-08-07). Уникальный `WatchID` не
|
||||
дает обещания порядка: сообщения с таким ключом не повторяются. Осмысленным
|
||||
ключом был бы `ClientID`, но раскладку по разделам стенд намеренно оставляет
|
||||
транспорту (раздел 2), и лаба о чтении топика живет тем, что она не
|
||||
предрешена. Проверено 2026-08-07 по `CONFIGURATION.md` librdkafka и
|
||||
`rd_kafka_conf_dump` библиотеки `confluent-kafka` 2.15.0: без ключа
|
||||
`consistent_random` выбирает раздел, а `sticky.partitioning.linger.ms = 10`
|
||||
временно прикрепляет соседние сообщения к одному разделу. Документация
|
||||
умолчаний не называет, поэтому значения сняты с самой библиотеки. День идет
|
||||
секунды и ложится в оба раздела, короткая пачка может уложиться в одно окно;
|
||||
поэтому чтение обеими нодами проверяется полным днем и разово.
|
||||
- **Приемников два, а режимов три** (#41, 2026-08-07). Kafka пачкой и Kafka с
|
||||
темпом ×60 используют один приемник у разных зовущих; темп держит
|
||||
проигрыватель, знающий время события. Приемник с ускорением снова знал бы о
|
||||
содержимом — именно это запрещает правило глупых приемников из раздела 4.
|
||||
- **Раскладка образа повторяет репозиторий** (#41, 2026-08-07): `/app` — корень,
|
||||
пакет лежит в `/app/generator/src` и виден через `PYTHONPATH`, каталог — в
|
||||
`/app/data`. Путь к каталогу считается от `catalog.py` вверх на `parents[3]`.
|
||||
Обычная установка пакета перенесла бы модуль в `site-packages`, и тот же
|
||||
подъем указал бы внутрь `.venv`: каталог был бы в образе, но модуль его не
|
||||
нашел бы. Проверено в контейнере: день совпал побайтово с прогоном на машине.
|
||||
|
||||
Решено при исполнении #42 (2026-08-07):
|
||||
### Опись мира и словарь
|
||||
|
||||
- **Опись мира одна, и она же растёт до эталонной.** Стартовый мир конца
|
||||
этапа 2 — первые восемь дней оси, понедельник по понедельник (решение
|
||||
владельца при нарезке, 2026-08-01): полная неделя с выходными и первый
|
||||
замкнутый цикл окна K = 7. Отдельного «младшего» файла для них не
|
||||
заводится: `data/world-inventory.json` — та самая опись, которую раздел 5
|
||||
обещает эталонному миру, пока короткая. Этап 7 продлит её до четырнадцати
|
||||
дней, а не заведёт вторую. Форма — JSON: паспорт мира (зерно, версия
|
||||
генератора, хеш каталога) и по строке на день с датой, числом событий и
|
||||
хешем. Собирается целью `make inventory`, свежесть сторожит тест
|
||||
`test_inventory.py` — тем же способом, что свежесть «описания выгрузки».
|
||||
Отклонено: *два файла, «мини» и полный* — две правды об одном мире и
|
||||
лишнее слово в словаре.
|
||||
- **Хеш каталога — в паспорте, и работа у него объяснительная.** Правка
|
||||
цены в `data/catalog/products.csv` меняет мир так же молча, как правка
|
||||
кода, но поймают её и без хеша: хеши дней разойдутся. Хеш каталога
|
||||
отвечает на следующий вопрос — **что** правили: разошлись дни и каталог —
|
||||
CSV; разошлись только дни — код.
|
||||
- **Слова.** «Манифест» по всей спеке переименован в **опись мира**, а
|
||||
«зерновой мир» — в **стартовый мир** (решение владельца 2026-08-07).
|
||||
Довод — правило языка репозитория: есть обычное русское слово — берётся
|
||||
оно. Заодно исчезла пара «манифест / мини-манифест», в которой читателю
|
||||
пришлось бы различать две сущности там, где вещь одна.
|
||||
- **Опись мира одна и растет до эталонной** (#42, 2026-08-07; решение владельца
|
||||
при нарезке, 2026-08-01). Стартовый мир этапа 2 — первые восемь дней,
|
||||
понедельник по понедельник: полная неделя и первый замкнутый цикл K = 7.
|
||||
`data/world-inventory.json` — та самая опись, которую обещает раздел 5, пока
|
||||
короткая; этап 7 продлит ее до 14 дней, а не заведет вторую. В JSON лежат
|
||||
паспорт мира — зерно, версия генератора и хеш каталога — и строки дней с
|
||||
датой, числом событий и хешем. Опись собирает `make inventory`, свежесть
|
||||
сторожит `test_inventory.py` тем же способом, что свежесть описания выгрузки.
|
||||
Отклонено: *два файла, младший и полный* — две правды об одном мире и лишнее
|
||||
слово в словаре.
|
||||
- **Хеш каталога лежит в паспорте и объясняет расхождение** (#42, 2026-08-07).
|
||||
Правка цены меняет мир так же молча, как правка кода. Хеши дней и без
|
||||
каталожного хеша поймают смену мира; он отвечает на следующий вопрос — что
|
||||
менялось: разошлись дни и каталог — CSV, только дни — код.
|
||||
- **«Манифест» переименован в «опись мира», «зерновой мир» — в «стартовый мир»**
|
||||
(#42, решение владельца 2026-08-07). Обычные русские слова убирают пару
|
||||
«манифест / мини-манифест» и необходимость различать две сущности там, где
|
||||
вещь одна.
|
||||
|
||||
Открытых вопросов за разделом не осталось: интерфейс запуска закрыт #41,
|
||||
форма описи и хеш каталога — здесь.
|
||||
### Открытое на момент заморозки
|
||||
|
||||
- **`ParsedParamsKey1` остается пустой** (#39, 2026-08-02) — отложенное решение,
|
||||
а не дыра. Вариант A/B-теста был бы постоянным свойством куки, а не поведением
|
||||
дня; его заводит тикет #47.
|
||||
Reference in new issue
Block a user