Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому
же состоянию, а в git лежит то, чем это состояние проверяется.
Что
- Опись мира `data/world-inventory.json`: паспорт (зерно, версия
генератора, хеш каталога) и по строке на каждый из восьми дней — дата,
число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит
`test_inventory.py` — тем же способом, что свежесть описания выгрузки.
- Разовая служба `world-init` вышла из-под профиля и играет в топик восемь
дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait`
считает успешно отработавшую службу упавшей.
- `scripts/wait-for-world.sh` — вторая половина `make up`: приём
асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения
заливки. Ограниченный цикл опроса, не пауза наугад.
- Девятая проверка `make check-clickhouse`: подневный счёт событий против
описи, рамка по датам стартового мира, счёт через `FINAL`. При
расхождении называет, где искать, — в событиях или в браке.
- Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал
1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с
датой. Раздел 9 спеки закрыт: открытых вопросов не осталось.
- Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром
(решение владельца). Оба заведены в словарь CONTEXT.md.
Проверка
`make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий.
`make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с),
`make test` — 407 тестов за 71 с, `make lint`, `make typecheck`,
`make config-test` зелёные.
Что проверка умеет краснеть, снято двумя поломками: снос партиции
2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье —
«сломан разбор». Строки опыта убраны, день переигран, счёт вернулся.
Тест свежести проверен молчаливой правкой цены в каталоге: покраснел.
Ссылка: #42
- Зачем:
- находка «это стоило бы проверять регулярно» решалась заново в каждом
тикете и каждый раз тянулась в make check-clickhouse. У неё есть
назначенный дом: даги качества данных, которые придут со следующими
уровнями хранилища.
- Что:
- добавлен раздел «Корректность процессов живёт в дагах DQ, а не в целях
make»: цели make отвечают «стенд собран», свойства данных — работа дага.
- назван фильтр: про полноту дня, свежесть слоя, сходимость витрины с
источником — это даг, а не цель.
- названа учебная сторона: даг идёт по расписанию, пишет историю проверок
и разбирается как обычная задача Airflow — так качество данных устроено
в бою.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: parseDateTimeBestEffort на непонятной строке не краснеет, а достраивает
недостающее — обрезанное «20:00:21» становится первым января текущего года.
Такое сообщение проходило строгий приём с тихо неверным временем, то есть с
той самой порчей, ради которой класс key_field_unparsed и заведён.
Что:
- В обеих матвью разбор метки идёт parseDateTimeOrNull по формату
'%Y-%m-%dT%H:%i:%SZ'. Форма на проводе одна и каноническая, поэтому широта
best-effort не нужна вовсе, а платится за неё отключённой проверкой.
- Замеры в ADR 0005: три записи, которые best-effort достраивает; проверка,
что настройка cast_string_to_date_time_mode не спасает JSONExtract; сверка
на настоящих данных — по всем 101 252 строкам сырья модельного дня точный
формат разобрал метку у каждой и ни на одной не разошёлся с best-effort.
- Записано наблюдение стенда: пересозданная на живом чтеце матвью пропускает
ближайшее сообщение мимо ODS, через минуту то же сообщение разбирается.
Воспроизведено дважды; на нём я сам споткнулся при проверке этой правки.
Проверка: опыт строгого приёма прогнан заново — три сообщения дали событие и
два key_field_unparsed, включая обрезанную метку, которая раньше проходила
годной. DDL применяется на живом кластере.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: холодное ревью по двум линиям нашло дыру в следе опытов и три места,
где текст утверждает не то, что построено.
Что:
- Опыт «_load_ts переносится из сырья» прогнан и записан: у двух тысяч
событий метка совпала с меткой одной из доставок, случаев «метки нет среди
доставок» ноль. Туда же — ответ про форму ключа ODS: вопрос раздела 11
спеки закрывался молча.
- Дока хранилища говорила, что предикат собран из функций, не возвращающих
NULL; построено иначе — обнуляемый разбор есть, но кончается IS NOT NULL.
- Записана гарантия на JSONType: на не-JSON и пустой строке она отдаёт Null и
не бросает, то есть годится в предикат. Раньше первый класс брака стоял на
замере соседней функции.
- ttl_only_drop_parts у таблицы ошибок назван в доке хранилища.
- Комментарий матвью ужат: три вопроса строгого приёма пересказывали ADR 0005
целиком. Осталось то, чего по коду не видно, — запрет трогать arraySort и
замер про ISO-8601. Убрано неверное «в полусотне строк» и упоминание имени
таблицы хранилища в докстринге контракта генератора.
Проверка: DDL применяется на живом кластере; make lint, typecheck, docs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: цепочка Kafka → STG → ODS достраивается последним этажом. Сырьё уже
доезжает (#37), настоящие события в топике есть (#41), а типизированного слоя
не было — событие негде было прочитать колонками, а брак негде увидеть.
Что:
- sql/ddl/20-ods-tables.sql — ods.event_rep/_dist на ReplacingMergeTree с
версией _load_ts, партиция по EventDate, ключ по разделу 1.3 спеки,
шардирование cityHash64(ClientID); ods.event_errors_rep/_dist с классом
брака, своими ключами и сроком жизни в месяц.
- sql/ddl/30-ods-views.sql — две матвью над stg.hits_raw_dist. Годность
считает предикат из трёх частей, вторая матвью берёт его дословное
отрицание, класс брака пишется первым совпавшим из трёх.
- Метку времени разбирает parseDateTimeBestEffortOrNull, а не JSONExtract:
ISO-8601 с суффиксом Z JSONExtract не берёт вовсе. Спека генератора
обещала обратное — обещание поправлено, форма на проводе не менялась.
- Сверка объявлений (contract-тест) снята из документов и из докстрингов
schema.py: сверх строгого приёма она ловила только смену типа.
- Документация приведена в соответствие: ADR 0005, дока хранилища и обе
спеки; группа «сказано по памяти» в доке хранилища опустела.
Проверка: make up && make check-clickhouse (8 проверок, 7,5 с); make lint,
make typecheck, make test (406), make docs без диффа. Разовые опыты при
исполнении — в теле PR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: имя файла осталось от прежней цели smoke-cluster, которой больше нет,
и читатель ищет проверку ClickHouse не там, где она лежит.
Что: git mv scripts/clickhouse-smoke.sh scripts/check-clickhouse.sh, тем же
коммитом — вызов в Makefile и строка в карте целей. Абзац-объяснение в
docs/architecture/testing.md снят: он обещал переименование, которое здесь и
случилось.
Проверка: make check-clickhouse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- до сих пор генератор умел собирать день, но не умел его отдать: топик
hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
события до стенда и закрывает форму на проводе, на которую обопрётся
типизированный ODS (#43).
- сериализатор один по решению спеки: второе место, печатающее событие в
JSON, разошлось бы с первым молча.
- Что:
- serialize.py — канонический сериализатор на orjson: единственное место,
где событие целиком становится JSON; 47 ключей всегда, «пусто» это
пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
ecommerce в commerce.py вторым сериализатором не считается — правило
про событие, а не про блок внутри него.
- sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
ключом он был бы ключом лишь на вид.
- player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
live (темп ×60), несколько дней одним запуском, ограниченная пачка,
раздельные тайминги генерации и доставки, лаг в логе.
- день на оси и имя топика умолчаний не имеют: параметр, описывающий
среду или позицию, приходит от зовущего, иначе отказ до генерации.
Умолчания зерна, числа дней и темпа остаются — они описывают мир.
- generator/Dockerfile — свой образ: зависимости из uv.lock, база
закреплена до патча, раскладка репозитория сохранена ради каталога
товаров. Образ Airflow не тронут.
- разовая служба compose под профилем, цели generate-batch и
generate-live, .dockerignore, tmp/ в .gitignore.
- решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
- make test 406 passed, make lint, make typecheck, make config-test.
- побайтовый детерминизм: два прогона дня в независимых процессах дают
один sha256; день в контейнере совпадает с днём на машине.
- на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
Distributed сошёлся — отправлено 50626, в таблице 50626.
- топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
clickhouse-02 раздел 1 (24258).
- живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
темп ×60, лаг печатается.
- форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
строкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- у вопроса «чему на этом научится менти?» не хватало второй половины:
он отсеивал бесполезное, но не взвешивал цену полезного. Стенд
учебный, и чем он сложнее, тем хуже как учебный материал.
- прогон #41 показал механизм: две слепые линии ревью дали 15 находок,
саморевью ещё 7, и каждая по устройству триажа превратилась в правку.
Шага, на котором кто-нибудь вычитает, в конвейере не было ни разу —
только воронка. Проход на вычитание потом срезал 149 строк и 6 тестов,
и всё срезанное появилось после ревью, а не при замысле.
- Что:
- в раздел «Цель репозитория» добавлены три абзаца: внимание менти как
конечная валюта и вопрос «что он платит и что получает»; почему
оборонительный код дороже прочего и врёт про опасность; проход на
вычитание как обязательный шаг перед приёмкой заметной работы.
- записано частное правило, выведенное владельцем на двух резах подряд:
не сторожить ошибку, которую человек делает сам себе и тут же видит —
разбирательство с ней и есть урок.
- Проверка:
- правка текстовая, целей make не задевает; make config-test зелёный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- проверка, которая кормит стенд данными, оставляет их в мире менти
навсегда: у ODS срока хранения нет, и события, которых мир не рождал,
неотличимы в лабах от настоящих. Решение принято при разборе постановок
#41 и #43, чтобы оно решалось по карте, а не заново в каждом тикете.
- Что:
- добавлен раздел «Интеграционная проверка постоянной целью не становится»:
такой прогон делается один раз при исполнении, след — запись в теле PR.
- названо требование прибираться за собой и дешёвый способ это сделать:
модельный день за границей оси мира и снос его партиции.
- названо, что стережёт цепочку постоянно вместо неё — проверки на
настоящих данных, которые стенд произвёл сам.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- проверка была сломана с рождения цели: bash -n со списком файлов
разбирает только первый, остальные уходят ему в аргументы. Из пяти
скриптов проверялся один, и за всё время этого никто не заметил.
- чинить незачем: скрипты стенда запускают с той же машины, и
синтаксическая ошибка вылезает при первом же запуске с номером строки.
Учебной ценности в проверке нет — из неё не узнаёшь ничего, кроме того,
что у bash есть ключ -n.
- держалась она не строчкой, а двенадцатью: обход репозитория, временный
файл со списком, mapfile и две ветки на пустой список.
- Что:
- из scripts/config-test.sh убраны разбор Bash и весь аппарат сбора
списка файлов; 53 строки стали 40.
- разбор файлов DAG остался и получил комментарий с основанием: их на
машине не запускает никто, обработчик разбирает их внутри контейнера, и
ошибка всплывает не сообщением, а молча пропавшим DAG.
- README и карта проверок больше не обещают проверку синтаксиса Bash.
- в карте записано, почему проверку не стоит заводить заново.
- Проверка:
- make config-test зелёный.
- оставшийся разбор DAG краснеет: незакрытая скобка в dags/test_kafka.py
роняет цель с SyntaxError и ненулевым кодом; файл восстановлен.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- деление целей оставило дыру: про Airflow, Superset, Prometheus и Grafana
смоук стучится с машины в отображённый порт, а про Kafka после переезда
check_kafka_from_host знал только «контейнер здоров».
- вердикт этот приходит из healthcheck в compose.yaml, а тот спрашивает
брокер изнутри и по внутреннему слушателю: объявленный наружу адрес может
вести не туда, и Kafka всё равно останется здоровой.
- поломка популярная и показательная: клиент подключается, получает
метаданные и молча виснет на адресе, которого с его стороны нет. Менти
узнаёт, что у брокера два слушателя и зачем нужен advertised.listeners.
Генератор будет писать в Kafka именно с машины.
- Что:
- check_kafka_external_listener в make smoke: запрос списка топиков с машины
через отображённый порт, ответ приходит только если объявленный адрес ведёт
туда же. Комментарий у проверки объясняет, от чего она заведена.
- ожидание ответа ограничено 15 секундами при замеренных 2,6 — впятеро
больше, чем стоит зелёный прогон.
- README и карта проверок: новая проверка названа, доводы записаны, цена
смоука обновлена с 6 до 8 секунд.
- Проверка:
- make config-test, make smoke (20 проверок, 8 с), make check-clickhouse,
make check-services — зелёные.
- краснеет на своей поломке: брокеру объявлен адрес kafka-nowhere:29092 при
целом внутреннем слушателе — проверка состояния контейнера осталась
зелёной, смоук покраснел именно на этой строке. После проверки Kafka
возвращена в исходное состояние, посторонних контейнеров не осталось.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- смоук перестал быть быстрым: 42 секунды из 48 съедали шесть проверок,
которые ждут службу — запуск DAG, вход в Superset, Kafka с машины.
- имена целей врали: смоуком звались и глубокая проверка кластера, и
интеграционные проверки; префикс достался им от общего происхождения.
- нигде не было записано, зачем в репозитории каждая цель и куда класть
новую проверку, — без записи скрипт дорастёт снова.
- Что:
- ось деления — кого спрашивают, а не сколько стоит: make smoke (стенд
собран), make check-clickhouse (спрашивают у ClickHouse), новая
make check-services (службы работают).
- шесть тяжёлых проверок переехали в scripts/stand-services.sh; общее —
счёт, обращение к Compose, зависимости машины и check_containers_survived
— вынесено в scripts/stand-common.sh, копипасты нет.
- smoke-cluster переименована в check-clickhouse; имя файла скрипта не
тронуто (в него встраивается проверка договора со схемой), расхождение
названо в карте.
- смоук и check-services печатают своё время в строке ИТОГ; порога по
времени нет — по доводу ADR 0004.
- docs/architecture/testing.md: карта всех семи целей, правило быстрого
смоука словами, лесенка по частоте и правило про краснеющую проверку,
переехавшее из README; указатель из AGENTS.md.
- README: описания целей сокращены, карта не дублируется; быстрый старт
показывает работающий стенд, а не только собранный.
- планка приёмки этапа в спеке названа поимённо: три цели вместо
«smoke-проверки».
- Проверка:
- make config-test, make smoke (19 проверок, 6 с), make check-clickhouse
(8 проверок, 7 с), make check-services (7 проверок, 44 с) — зелёные.
- 19 + 7 = 25 разных проверок, как и до деления: check_containers_survived
считается дважды намеренно.
- краснеют обе разделённые цели: со снятым prometheus смоук дал три ошибки,
с подменённым UUID подключения Superset покраснел check-services.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- горячее ревью: перевезённая проверка keeper потеряла привычку файла
ограничивать обращения к контейнерам по времени и глушить их ошибки.
- Что:
- опрос keeper идёт через keeper_exec с timeout 20s и тихим stderr,
в отчёте об отказе пустой ответ назван словами.
- комментарий к проверке и абзац README переписаны на проверяемое
утверждение: настройки объявлены в compose.yaml, смоук спрашивает,
дошли ли они до процесса.
- в пробнике ClickHouse ожидаемой строке возвращено имя expected_rows.
- Проверка:
- make config-test, make smoke, make smoke-cluster; отдельно проверено,
что на паузе keeper проверка краснеет, а не виснет.
- Зачем:
- проверок стало больше, чем продукта, и росли они из критериев приёмки,
а не из учебной ценности (#56).
- Что:
- удалены tests/dag-probes-unit.py, tests/stand-smoke-guards.sh,
tests/stand-smoke-static.sh и tests/smoke-guards.sh вместе с целью
make smoke-guards и запуском юнит-тестов в scripts/config-test.sh.
- из scripts/stand-smoke.sh убран check_env_consistency, туда же переехал
check_keeper_runtime; счёт проверок остался 25.
- в пробниках свёрнуты функции _assert_*, комментарий про отложенный импорт
переписан на причину из документации Airflow и продублирован в test_kafka.
- Проверка:
- make config-test, make up, make smoke, make smoke-cluster.
Зачем: учебная ценность в AGENTS.md заявлена дважды, но обе строки —
утверждения о ценности, а не действие в момент письма. При них проверок
в репозитории выросло больше, чем продукта.
Что: в раздел «Цель репозитория» добавлен проверяемый вопрос к любой
доработке — чему на ней научится менти — с ветвью: не складывается ответ,
значит это вопрос владельцу, а не строчка кода.
Проверка: не требуется, правка документа.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: тикнуть чекбокс в критериях приёмки нужно при закрытии каждой задачи,
а рецепта в доке не было. Наступили на это при закрытии #37: `tea issues edit
--description` требует тело целиком строкой, но взять её неоткуда — вывод
`tea issues <номер>` обёрнут и разрисован для терминала.
Что: раздел «Правка тела issue — только через API» с рабочим рецептом
(забрать сырой JSON, поправить body, вернуть через `-X PATCH -d @файл`) и
разбором флагов `tea api`. Оттуда же общее правило: CLI удобен, пока команда
создаёт объект или меняет его свойство, и мешает, как только надо изменить
уже написанный текст. Ссылка из раздела про автозакрытие и запись в «Что
проверено и когда».
Проверка: рецепт снят живыми запросами при закрытии #37 — так проставлены
чекбоксы тикета и пункт в чек-листе карты #4. `make config-test` зелёный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: стенду нужен воспроизводимый холодный старт, при котором схема
хранилища и топик появляются сами, а сырьё из Kafka доезжает в STG обеими
нодами кластера — без ручных шагов между `make clean` и рабочим приёмом.
Что:
- `sql/ddl/` — три файла, применяются по порядку имён: базы `stg` и `ods`,
Kafka-чтец `hits_raw_kafka` формата RawBLOB, реплицируемая `hits_raw_rep`
с окном TTL в трое суток, распределённая `hits_raw_dist` и матвью
`hits_raw_mv`, переносящая сырьё вместе с метаданными доставки.
- `compose.yaml` — службы `kafka-init` (топик `hits` на две партиции, с
ремонтом уже созданного однопартиционного) и `clickhouse-init` (применяет
`/ddl/*.sql`); `hostname:` у обеих нод, чтобы `hostName()` отдавал имя узла,
а не идентификатор контейнера; `airflow-init` зависит от `clickhouse-init` —
без зависимого успешный одноразовый сервис считается упавшим для `--wait`.
- Доки: конвенции и раздел «Что проверено» в справочнике хранилища, указатели
и границы обещаний в ADR 0005, снятые пункты в разделе 11 спеки.
Проверка: `make lint`, `make typecheck`, `make config-test`, `make smoke`
(25 проверок), `make smoke-guards` — зелёные. Приёмочный прогон с чистого
тома подтвердил все пять критериев #37: холодный старт и идемпотентный
повтор, две партиции у `hits`, метаданные доставки у доехавшего сообщения,
обе партиции на обеих потребляющих нодах в одном прогоне, некорректный JSON
лежит сырым и приём не встаёт.
Известная граница: RawBLOB молча теряет запись с пустым значением и
запись-надгробие; принято как свойство, замер и довод — в справочнике
хранилища.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- холодное ревью связности нашло девять мест, где вставленный текст спорит с
соседним; отдельно вскрылось, что представление дат в JSON не зафиксировано
нигде, а #43 обязан его знать раньше, чем #41 напишет сериализатор.
- Что:
- гарантия приёма переписана: после снятия синхронной вставки «хотя бы один
раз» стало неправдой — есть и окно потери, и окно дубля.
- критерий выбора пяти опорных колонок приведён к списку, который он
порождает; `CounterID` оговорён отдельно.
- «переобработки у ODS нет вовсе» смягчено до пакетной: ручная вставка из
сырья в пределах окна возможна.
- в спеку генератора добавлена форма дат на проводе — ISO-8601, с доводом от
читаемости слоя сырья.
- убраны осиротевшая фраза про порядок сервисов, дубль порядка классов брака,
устаревшая датировка сверки и ещё три следа вставок.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- список «чего не переоткрывать» устарел за день: снятая настройка
синхронной вставки и прежняя раскладка DDL остались в нём как принятые
решения, а документ, утверждающий решённым переехавшее, — ловушка.
- Что:
- удалён .scratch/handoffs/2026-08-04-storage-conventions.md; содержание
живёт в ADR 0005 и 0006, доке хранилища и тикетах #37 и #43.
- Проверка:
- make config-test
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- три холодных ревью и сверка с документацией ClickHouse нашли противоречия
между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
один вопрос, а два утверждения о движке оказались неверными.
- Что:
- раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
последней: иначе часть событий тихо минует ODS.
- синхронная вставка снята с пути приёма: настройка недостижима для потока
Kafka-движка и связывает шарды; на ETL-вставках осталась.
- у таблицы ошибок появился класс брака с порядком проверки, у сырья и
ошибок названы движки и ключи сортировки.
- в доку добавлен раздел «Что проверено»: сверенное с документацией,
проверяемое на стенде и сказанное по памяти разведены.
- в спеке выправлены источник матвью разбора, пять опорных колонок, имена
четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
- make config-test
- grep по устаревшим именам файлов DDL и витрин — пусто
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- работа над #37 продолжится в новой сессии, а решения сессии разбросаны по
двум ADR, доке хранилища, двум спекам и двум тикетам; свежему агенту нужен
один вход с указателями и списком того, что переоткрывать не надо.
- Что:
- добавлен .scratch/handoffs/2026-08-04-storage-conventions.md: указатели на
артефакты, перечень принятых развилок, открытые хвосты и особенности
работы с трекером.
- Проверка:
- make config-test
- Зачем:
- тикет #37 молча опирался на конвенции хранилища, которых в проекте не
было; без них #43 и следующие этапы разъехались бы в именах, служебных
колонках и механике приёма.
- Что:
- ADR 0005: топик читается байтами в STG, разбор идёт функциями в матвью
ODS; строгий приём — сверка набора ключей плюс Nullable на пяти опорных
колонках.
- ADR 0006: суффикс вида в именах объектов (_rep, _dist, _kafka, _mv, _v).
- docs/architecture/storage.md: конвенции имён и служебных колонок, путь в
keeper, раскладка по шардам, срок жизни сырья, свойства приёма, раскладка
файлов DDL и карта таблиц.
- спеки приведены в соответствие: механизм строгого приёма, имена объектов,
контракт транспорта «одно событие — одно сообщение Kafka», три проверки
при исполнении.
- Проверка:
- make config-test
- Зачем:
- после #50 доля просмотров, доходящих до корзины, подошла к краю вилки
5–9%, и следующий исполнитель стал бы калибровать поведение под это
число. Владелец решил: пока доля правдоподобна, она ничего не сторожит —
у живых магазинов она гуляет широко.
- Что:
- в блок «Решено при исполнении #50» добавлено решение владельца: вилка —
ориентир; настоящий предел здесь дневной бюджет событий, на котором
стоят манифест (#42) и порог скорости дня.
- Проверка:
- git show --stat; следующий исполнитель читает решение, не переспрашивая.
- Зачем:
- приёмка #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 канонических дней совпадает со снимком до правки.
- Зачем:
- блок «Решено при исполнении #40 после приёмки» цитировал замер на зерне 0,
а канонический мир, на котором стоят все тесты, — CANONICAL_SEED
(20260601); числа относились к другому миру.
- Что:
- замер перемерен на каноническом зерне за 14 дней: тождество «событие
корзины ↔ страница /cart» держится каждый день, на дне 2 это 828 и 828.
- уточнены средний день (49 509 событий), конверсия визита (2,43%), состав
заказа (1,87 позиции) и доля брошенных позиций (число мира 15%, замер по
покупающим корзинам 12%).
- Проверка:
- скрипт замера на 14 днях CANONICAL_SEED; git show --stat.
- Зачем:
- приёмка #40 нашла в торговых данных два точных равенства, каких в живом
магазине не бывает: событие корзины возникало ровно у визитов со
страницей /cart, а внутри такого визита в корзину ложились все открытые
карточки.
- Что:
- в спеку генератора, раздел 9, добавлен блок «Решено при исполнении #40
после приёмки»: корзина отвязана от страницы корзины, уровень спроса
становится свойством товара, границы правки — вилки раздела 5.
- записаны отклонённые варианты: свой процент у каждого sku и вывод
склонности из цены.
- в CONTEXT.md заведён термин «уровень спроса».
- Проверка:
- git show --stat; текст решений читается без обращения к переписке.
- Зачем:
- клиентская сторона мира становится целой: без 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>
- Зачем:
- холодное ревью отметило, что это решение, а не калибровка: от выбора
шага зависит, как «покупатель» читается в данных, и оставлять выбор
реализации нельзя.
- Что:
- в блоке решений #40 записано, что помеченный отличается на обоих шагах
воронки — и до корзины доходит чаще, и бросает её реже; числа под
каждый шаг остаются калибровкой.
- Проверка:
- чтением: docs/specs/2026-08-01-generator.md, раздел 9, блок #40.
- Зачем:
- холодное ревью нашло две дыры уровня решений: лифт конверсии не делает
покупателя видимым в данных, а цены каталога кратны рублю — от этого
урок про Float64 остаётся без материала.
- Что:
- метка покупателя получает второй рычаг: помеченные дольше живут и чаще
возвращаются; границы правки заданы вилками раздела 5, база до правок
записана числами.
- часть цен каталога получит копейки; хвост этапу 4 переписан честнее.
- дописаны нерешённые места: количество штук и повторная карточка,
множество нумерации заказов, таблица «код — скидка», состав сырого
ecommerce, резерв времени вместо зажима к полуночи.
- два расхождения внесены в мастер-спеку и записаны в раздел 7: длина
массивов purchase* и product*, механика класса amount_delta.
- Проверка:
- чтением: docs/specs/2026-08-01-generator.md, разделы 7-9;
docs/specs/2026-07-30-stand-v2-realism.md, разделы 1, 4, 7.
- Зачем:
- перед реализацией торговых событий грилинг закрыл шесть развилок; без
записи решения и отклонённые варианты потерялись бы, а часть из них
выходит за границы тикета и меняет постановку.
- Что:
- в раздел 9 спеки генератора добавлен блок «Решено при исполнении #40»:
корзина шире заказа, метка покупателя становится значимой, место
торговых событий, номер заказа, промокод, единицы денег, разная длина
массивов purchase* и product*, orjson, хвост покупки у границы суток.
- в раздел 8 добавлены хвосты этапам 3 и 4: скидка по промокоду и
намеренное расхождение сумм.
- в словарь добавлены «покупатель» и «торговое событие».
- Проверка:
- чтением: docs/specs/2026-08-01-generator.md, разделы 8 и 9; CONTEXT.md.
Зачем: код генератора компактен, но нетривиален: многое в нём — соглашения, а
не конструкции, и чтением они не выводятся. Ближайшая опасность конкретна —
следующий этап трогает и план, и день, а вставка броска в середину функции
выглядит безобидно и молча меняет весь мир.
Что: цепочка от корневого зерна до строк событий; ленивость мира — когорта по
требованию и предыстория до D0, отсюда «любой день собирается сам по себе»;
правило порядка бросков внутри подпотока с механикой и способом обнаружения
промаха; причина целочисленной случайности со ссылкой на спеку. Раздел помечен
черновым: по сути верен, но на понятность читателем со стороны не выверен —
выверка вместе с полным путеводителем, тикет #48.
Проверка: `make lint` чист, `make test` — 353 passed; правка только в README.
Co-Authored-By: Claude Opus 5 (1M context) <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>
- Зачем:
- код генератора будет расти в #38/#39 — проверку типов лучше
завести до реализации, чем типизировать задним числом.
- Что:
- ty добавлен dev-зависимостью генератора (закреплён в uv.lock).
- в Makefile добавлена цель typecheck, README обеих сторон обновлены.
- Проверка:
- make typecheck && make lint && make test — всё зелено, 248 тестов.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- тикет #38 требует решить и внести в спеку генератора числа притока,
D0 и формат конфигурации мира до реализации плана состава.
- Что:
- раздел 1 спеки: ленивый план по когортам дня, предыстория с полкой
от D0, гарантия двухкуковых пар назначенными заказами (единица —
человек), пять отклонённых вариантов с доводами.
- раздел 9: приток 3 800 кук/день, окно активности 90 дней (решение
владельца), доля покупателей 5% людей, D0 = 2026-06-01,
конфигурация мира — модуль чистых данных; шапка Proposed → Accepted.
- CONTEXT.md: термины «план состава», «приток», «хвост возвратов»,
«предыстория»; «состав мира» уточнён, словарь очищен от решений.
- Проверка:
- двойное слепое ревью правок (Codex + Claude), все 20 находок
закрыты; арифметика чисел пересчитана ревьюерами независимо.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>