# Проверки: карта целей Документ отвечает на два вопроса. Первый: что именно утверждает каждая цель `make` и сколько стоит её прогон. Второй, ради которого документ и заведён: куда положить новую проверку — так, чтобы это решалось по карте, а не чтением скриптов. Зона ответственности у документа одна — проверки. Что именно они стерегут, описано в других местах: устройство хранилища — в [storage.md](storage.md), замысел стенда — в спеке [«Боевой реализм стенда (v2)»](../specs/2026-07-30-stand-v2-realism.md). ## Ось: кого спрашивают Цели различаются не ценой, а тем, к кому обращён вопрос. ClickHouse отвечает сам и за миллисекунды. Airflow отвечает через такт планировщика. Superset — через сессию и обход метаданных. Быстрота выходит следствием этого различия, а не критерием деления. Совпадение цены и назначения здесь побочное, и это видно по дырам, которые каждая цель оставляет соседке. `make smoke` не заметит перепутанных местами макросов `shard`: обе ноды здоровы и порты отвечают. `make check-clickhouse` не заметит потерянного подключения Superset: он про ClickHouse и только. Отсюда правило для новой проверки: **спроси, кого она спрашивает.** Счётчики против паспорта — вопрос к ClickHouse: строки в `ods.event` считает сам сервер и отвечает сразу, значит дом им в `check-clickhouse`, даже если по цене они подошли бы смоуку. ## Карта целей Цели живут за двумя дверями. В корне — цели стенда; в `generator/` — цели генератора: он отдельный пакет со своим `pyproject.toml`, локом и образом, и спрашивают его отдельно. Стенд нужен трём целям из восьми. Цена — замер, см. «Что проверено». `make smoke` и `make check-services` перемерены 28 августа 2026 года, `make config-test` — 21 августа, `make check-clickhouse` — 19 августа, корневой `make lint` — 3 сентября, `make test` — 30 августа; остальные стоят с замера 6 августа. | Цель | Откуда | Что утверждает | Стенд | Цена | |---|---|---|---|---| | `make config-test` | корень | Compose разбирается, файлы DAG и дашборды Grafana синтаксически целы, в diff нет пробельных ошибок. О работоспособности не говорит ничего | не нужен | 1 с | | `make lint` | корень | Код стенда и сборщик карты отформатированы и проходят ruff; готовый HTML карты соответствует исходникам | не нужен | 0,8 с | | `make smoke` | корень | Стенд **собран**: службы живы, порты отвечают, подключения настроены друг на друга. Вширь и по касательной к каждой службе. Единственная цель, которая здесь правда смоук | нужен | 25 с | | `make check-clickhouse` | корень | Всё, что спрашивают **у ClickHouse** и он отвечает сам: макросы, шарды, реплики, путь в keeper, ключ шардирования, очередь распределённых DDL, ответ словаря товаров, счёт событий стартового мира против паспорта | нужен | 24 с | | `make check-services` | корень | **Службы работают**: DAG запускается и доходит, топик создаётся и удаляется, Superset логинится и ходит в базу | нужен | 71 с | | `make lint` | `generator/` | Код генератора отформатирован и проходит ruff | не нужен | 0,4 с | | `make typecheck` | `generator/` | Типы генератора сходятся (ty) | не нужен | 0,5 с | | `make test` | `generator/` | Генератор делает то, что обещает; схема события остаётся объявленным контрактом, а собранные из кода [описание выгрузки](../formats/clickstream-event.md) и [паспорт мира](../../data/world-inventory.json) — свежими | не нужен | 176,5 с | **Строк с именем `make lint` две, и это не опечатка.** Одноимённые цели за разными дверями охватывают разное: корневая не видит ни одного файла генератора, генераторная — ни одного файла стенда. Человек в корне видит зелёное и может решить, что зелёный весь репозиторий. Две тонкости, которых не видно из таблицы: - **Версия ruff у корневой цели закреплена в самом вызове** (`uvx ruff@0.16.1`) и равна той, что в `generator/uv.lock`. Равенство держится руками: обновили лок — поправьте и вызов, сверять их некому. - **Охват держат аргументы вызова, а не устройство дерева.** Python, положенный вне двух названных в таблице путей, не проверит ни одна из дверей. **Корневого `typecheck` нет, и это решение, а не пропуск.** Проверке типов мало прочитать файл: чтобы понять `from airflow.sdk import dag`, ей нужен установленный Airflow, а он живёт в образе, не на машине — 132 пакета ради двух файлов пробников (замер 9 августа 2026 года, `uv pip compile`). Цена решения называется честно: две настоящие находки `ty` в `dags/test_kafka.py` постоянного сторожа не получают. Вопрос вернётся на этапе 5, когда придут настоящие даги: тогда окружение окупится, и условие возврата — именно это, а не «стало неудобно». Цена самого `make up` с пустых томов — 8 м 18 с до переезда жизненного цикла в Airflow. В цену целей она не входит. На готовом мире подъём больше не пересчитывает данные: `world_initialize` зеленеет без изменений. `make up` собирает локальные образы Airflow, Superset и Grafana, поднимает весь стенд и ждёт здорового состояния долгоживущих контейнеров. В образ Airflow добавлены закреплённые клиенты ClickHouse и Kafka. Одноразовые `airflow-init` и `superset-init` завершаются с кодом 0. Первый обновляет схему Airflow, подготавливает администратора и два подключения к `clickhouse-01`. Второй обновляет Superset, создаёт администратора и импортирует подключение к `clickhouse-02`. На ноутбуке в режиме энергосбережения приёмочный подъём с пустых томов занял около десяти минут, повторный на живом стенде — около трёх. `make rebuild-storage` тоже не проверка. Команда через Airflow удаляет прикладные базы ClickHouse, топики и позиции групп Kafka, заново применяет DDL, заливает стартовый мир и возвращает его позицию к концу стартовых дней. Службы и их тома она сохраняет, поэтому возврат к каноническим данным не требует холодного запуска всего стенда. ## Правило: смоук обязан оставаться быстрым `make smoke` — быстрая проверка для регулярного прогона: её гоняют не задумываясь, и потому она обязана укладываться в секунды. Тяжёлые проверки в неё включать не следует: тяжёлая — та, из-за которой смоук перестаёт быть быстрым. Поодиночке это единицы и десятки секунд, вместе — минуты. Такие проверки живут в `make check-services`. На практике дорого обходится не работа, а ожидание службы: такт планировщика Airflow, сессия Superset. `make check-clickhouse` создаёт таблицы, вставляет строки и гоняет распределённые DDL — и укладывается в семь секунд, потому что ClickHouse отвечает сразу. Первый абзац — правило, второй — наблюдение, по которому тяжёлую проверку узнают заранее, не замеряя. ## Какую проверку когда запускать Деление целей ценно ровно до тех пор, пока оно не превратилось в «гонять всегда всё». Состояние перед проверкой восстанавливается в самой узкой достаточной области: сначала объект или партиция, затем база слоя. Если нужен целиком чистый прикладной мир, `make rebuild-storage` пересобирает его базы, топики, позиции потребителей и данные. `make clean` нужен, только когда меняется или сломано постоянное состояние вне этих баз либо проверяется сам запуск с пустых томов. Требование к обоснованию полного сброса задаёт `AGENTS.md`. Неизвестную поломку сначала исследуют: полный сброс уничтожает её следы. | Когда | Что гонять | |---|---| | Правка в работе | `make config-test` и `make smoke` | | PR или задача | плюс цели, которых правка касалась: DAG-и, Superset или Kafka — `check-services`; DDL, кластер или данные — `check-clickhouse` | | Приёмка этапа | все три цели на стенде; запуск с пустых томов — только для изменений начальной сборки или постоянного состояния служб | Правки генератора добавляют к этому `make lint`, `make typecheck` и `make test` из каталога `generator/`: стенд им не нужен, а `make test` из них самая дорогая. Правки дагов, Superset и карты — корневой `make lint`. Двадцать секунд из ее ста семидесяти шести — пересборка восьми модельных дней для сверки с паспортом мира (20,5 с из 176,5). Дешевле хеши не сравнить: чтобы узнать, тот ли получается мир, его надо получить. Место выбрано по той же оси «кого спрашивают» — вопрос обращён к коду генератора, стенд ему не нужен, — и краснеет проверка там, где надо: сразу после правки генератора. Не будь её, расхождение всплыло бы получасом позже, на поднятом стенде, где выглядит поломкой хранилища, а не забытой пересборкой. ## Порогов по времени здесь нет Цена в таблице — замеренное число с датой замера, а не назначенный порог. Автоматической проверки времени в репозитории нет и заводить её не следует. Число, вписанное в проверку, становится законом, которого никто не выбирал: [ADR 0004](../adr/0004-resource-limits.md) разбирает ровно этот случай — оценка из спеки попала жёстким порогом в `make smoke`, `make smoke` стал критерием приёмки каждого этапа, и дальше решения сверялись уже с порогом, а не с исходным доводом. Спека генератора формулирует ту же позицию прямо: «наблюдаемость без порогов». Смотрит на время человек. `make smoke` и `make check-services` печатают его сами — последней строкой `ИТОГ: пройдено N, ошибок M, время T с`. Замер при приёмке делается руками и называется в теле PR. ## Проверка, которая не умеет краснеть, бесполезна Правило, которое стоит держать в голове, правя любую проверку. Проверка, никогда не видевшая своей поломки, доказывает только то, что она умеет печатать «ЗЕЛЁНО». Убедиться дешевле всего руками: сломайте то, что она стережёт — остановите `prometheus`, удалите служебную таблицу пробника на второй ноде, — и посмотрите, покраснеет ли прогон и назовёт ли виновника. Не покраснел — проверка не работает, и чинить надо её, а не стенд. ## Интеграционная проверка постоянной целью не становится Проверка, которая не спрашивает стенд, а **кормит** его — шлёт сообщения, вставляет строки, портит данные нарочно, — постоянной целью `make` не живёт. Такую проверку гоняют один раз при исполнении, убеждаются, что механизм работает, и оставляют след записью в теле PR. Регулярная жизнь стенда — это жизнь с менти, а интеграционный прогон в неё не входит; самое большее, он становится темой урока. Довод не про цену прогона, а про данные. Вложенное остаётся: у сырых данных срок жизни трое суток, а ODS хранит всё, и события, которых мир не рождал, лягут в лабы менти неотличимо от настоящих — считая конверсию, он молча посчитает и их. Хуже явного мусора: явный видно. Отсюда правило для того, что всё-таки вкладывает: **прибираться за собой.** Дешевле всего это выходит партицией — опыт играет модельный день за границей оси мира, его строки ложатся в собственную партицию `EventDate`, и она сносится по окончании. Заодно менти видит партицию как единицу работы — тот же механизм, на котором стоит переобработка дня X. Что тогда стережёт цепочку постоянно — проверки на **настоящих** данных, тех, что стенд произвёл сам: подневный счёт событий против паспорта мира ловит и сломанный разбор (события уехали в брак — счёт разошёлся), и потерю по дороге, ничего при этом не вкладывая. Это единственный постоянный сторож цепочки Kafka → STG → ODS, и живёт он в `make check-clickhouse`. Правило, которое такая проверка обязана выдержать и которое стоит держать в голове для следующей: **на растущих данных утверждать можно только про зафиксированный кусок.** Мир растёт — менти переиграет день, этап 5 добавит следующий, — и всякое утверждение про таблицу целиком однажды покраснеет законно, то есть впустую. Как это сделано здесь, написано в самом скрипте. ## Корректность процессов живёт в дагах DQ, а не в целях `make` Цели `make` отвечают на вопрос «стенд собран и работает». Вопрос «данные в нём правильные» — работа отдельных дагов качества данных: они приходят вместе со следующими уровнями хранилища и заберут многое из того, что просится проверкой сейчас. Поэтому находка «это стоило бы проверять регулярно» сначала проверяется вопросом, не туда ли ей: если она про свойства данных — про полноту дня, про свежесть слоя, про сходимость витрины с источником, — её дом даг, а не `check-clickhouse`. Разница видна и менти, и она содержательная. Цель `make` он зовёт руками, когда чинит стенд; даг DQ идёт по расписанию рядом с остальным конвейером, пишет историю проверок и разбирается как обычная задача Airflow — то есть учит тому, как качество данных устроено в бою. ## Устройство скриптов | Цель | Скрипт | |---|---| | `make config-test` | `scripts/config-test.sh` | | `make smoke` | `scripts/stand-smoke.sh` | | `make check-services` | `scripts/stand-services.sh` | | `make check-clickhouse` | `scripts/check-clickhouse.sh` | Особняком стоит `scripts/run-dag.sh`: это не проверка, а общий REST-переход от `make` к Airflow. `make up` запускает через него `world_initialize`, а `make rebuild-storage` — `world_recreate`; сценарий ждёт появления дага в API, а затем до минуты повторяет только стартовый ответ `Dag with dag_id not found`. Прочие ответы и сетевые ошибки он не повторяет: при потерянном ответе запуск мог уже создаться. Сценарий печатает идентификаторы дага и прогона и возвращает вызывающему его итог. Ожидание доставки событий и запуск `orders_ingest` теперь видны задачами `world_initialize`, а не спрятаны в shell. Общее у смоука и `check-services` — счёт проверок, обращение к Compose и две проверки — вынесено в `scripts/stand-common.sh`; сам он не запускается. Оттуда же приходят два решения, которые видно по счёту прогонов: - **`check_containers_survived` стоит в конце обеих целей.** Убитый за память контейнер Docker поднимает сам, и проверка здоровья об этом промолчит (ADR 0004). Стенд нагружает `check-services`, а увидеть последствия нужно и тому, кто гонял один смоук. Это единственная проверка, которая считается дважды: 21 у смоука плюс 7 у `check-services` — это 27 разных проверок. - **Зависимости машины считает только смоук.** «На машине есть Docker, curl и jq» — вопрос к машине, а не к службам, и на оси он стоит рядом с «стенд собран». Для `check-services` это условие запуска: без них он не начнёт работу и громко скажет об этом, но ЗЕЛЁНО за это не печатает. Вход в Airflow `check-services` выполняет заново — общих переменных у двух скриптов нет. Отдельной проверкой этот вход тоже не считается: то же самое утверждает смоук. ### Что спрашивает смоук `make smoke` за секунды спрашивает, собран ли стенд: зависимости машины, здоровье контейнеров, устройство keeper, ответ Kafka с машины через отображённый порт, четыре цели Prometheus, два источника Grafana, компоненты Airflow и подготовленное подключение к `clickhouse-01`. Двенадцать проверок здоровья сразу после `make up --wait` повторяют то, чего Compose уже дождался: у каждой долгоживущей службы есть своя `healthcheck`. Оставлены они потому, что первый вопрос к стенду всё равно «всё ли живо», а ответ на него стоит меньше секунды. Устройство keeper — другое дело: он должен работать от пользователя `clickhouse`, с пределом в 262144 открытых файла и со своим томом под данные. Всё это объявлено в `compose.yaml`, но здоровым keeper выглядит и без этого, поэтому смоук спрашивает у живого контейнера, дошли ли объявленные настройки до процесса. Kafka по той же причине спрашивают снаружи. Её `healthcheck` обращается к брокеру изнутри контейнера и по внутреннему слушателю, поэтому здоровой Kafka остаётся и тогда, когда объявленный наружу адрес ведёт не туда. Клиент с машины в этом случае подключается, получает метаданные и молча виснет на адресе, которого с его стороны не существует. Смоук идёт тем же путём, что и будущий генератор: стучится в отображённый порт и просит список топиков. ### Что спрашивают глубокие проверки `make check-services` проверяет то, ради чего приходится ждать службу: работу с топиком Kafka с машины — создан, найден, удалён, — ручной запуск пробников `test_clickhouse` и `test_kafka`, вход в Superset, его метаданные и подключение к `clickhouse-02`. Первый пробник создаёт таблицы на обеих нодах и читает через `Distributed` на ноде 2 строку из локальной таблицы ноды 1. Второй пишет в Kafka и читает свой маркер. Временный топик проверки с машины и запуски DAG удаляются; постоянный топик пробника сохраняется, а старые записи чистит Kafka. `make check-clickhouse` запускает отдельную глубокую проверку ClickHouse: описание кластера, макросы, связь с keeper, `ReplicatedMergeTree`, `Distributed`, очередь распределённых DDL и очистку временных таблиц. Девятая из десяти проверок спрашивает словарь товаров на обеих нодах. Последняя — подневный счёт событий против паспорта, о нем сказано выше. ## Что проверено **Перезамер 3 сентября 2026 года при исполнении #222.** `make lint` прошел за 0,768 секунды; в таблице 0,8 с. `ruff format --check` проверил 13 файлов стенда, включая исходники карты, а затем проверка рендера подтвердила свежесть HTML. **Калибровка заказной стороны 30 августа 2026 года при исполнении #194.** Пересборки эталонного мира не будет (#192), поэтому доли заказной стороны сверены замером внутренностей генератора — окно 28 модельных дней, канонический seed, без транспорта, — тем же приемом, каким `test_plan.py` и `test_commerce.py` меряют мир. Команда (из `generator/`): ```bash uv run python - <<'PY' from collections import Counter from clickstream_generator import day, inventory, orders, world from clickstream_generator.seeds import CANONICAL_SEED WINDOW = 28 class_totals: Counter[str] = Counter() outcome_totals: Counter[int] = Counter() delivery_totals: Counter[int] = Counter() delay_totals: Counter[int] = Counter() total_orders = 0 for d in range(WINDOW): today = day.stream(CANONICAL_SEED, d) for key, value in inventory._order_class_counts(today).items(): class_totals[key] += value money = today.orders total_orders += len(money) for value in money.outcome.tolist(): outcome_totals[value] += 1 for value in money.delivery.tolist(): delivery_totals[value] += 1 for value in money.snapshot_delay.tolist(): delay_totals[value] += 1 print(total_orders, dict(class_totals), dict(outcome_totals), dict(delivery_totals), dict(delay_totals)) PY ``` 6973 заказов за 28 дней. Доли из таблицы сверки (после приоритета классов): отменено 5,15% (ориентир мастер-спеки ~5%, в пределах; из них 3,03% оплачен-потом-отменен и 2,12% отменен неоплаченным — веса `world.py` 95/3/2); дубль `purchase` 1,89% (ориентир ~2%, в пределах); дельта суммы 1,32% (ориентир 1–2%, в пределах); потеря `purchase` 1,78% при броске 3% — вычеты штатные: назначенные планом покупки из броска исключены (мост склейки), часть случаев уходит в отмену приоритетом; наблюдаемая доля принята решением владельца (#194). Стоимость доставки — 35,5% / 49,2% / 15,4%, совпадает с весами `DELIVERY_KOPECKS_WEIGHTS`. Опоздание слепков (`ORDER_SNAPSHOT_DELAY_WEIGHTS`) — доля заказов с δ>0 составила 8,39% (ориентир мастер-спеки ~10%, в пределах). Скрипт замера в репозиторий не входит; вывод — в отчете issue #194. **Перезамер 30 августа 2026 года при исполнении #192.** Команды запускались из `generator/`: ```bash time make inventory uv run python -m clickstream_generator batch --day 0 --days 8 \ --file /tmp/stage7-events.jsonl for snapshot_day in 1 2 3 4 5 6 7; do uv run python -m clickstream_generator snapshot --day "$snapshot_day" \ --file "/tmp/stage7-snapshot-${snapshot_day}.jsonl" done wc -c /tmp/stage7-events.jsonl /tmp/stage7-snapshot-{1,2,3,4,5,6,7}.jsonl time make test ``` Паспорт пересобрался за 20,515 с. Файловый приемник записал 460 731 812 байт: 458 478 771 байт событий и 2 253 041 байт слепков. `make test` прошел 437/0 за 176,476 с. Прежние 71 с (7 августа, #42) выросли по двум причинам: тестов стало 437 против 407, и потяжелел сам мир — та же пересборка восьми дней стоила 13 с, теперь 20,5. Эта правка добавила один тест — проигрыш дня. **Приемка #172 28 августа 2026 года.** Холодный `create_app()` занимал 28,3-28,4 секунды, а чтение подключения после запуска - 0,07 секунды. Предел 30 секунд почти целиком уходил на запуск второго приложения. Увеличивать его не стали: Superset не обещает постоянного времени `create_app()`. После входа проверка делает три GET-запроса к уже работающему Superset: находит `ClickHouse`, сверяет UUID и безопасный URI, затем требует схему `dm`. Так без второго запуска проверяются сохраненное подключение и хранилище пароля. Форму API сверили через Context7 и с исходным кодом Superset 6.1.0. `superset test-db` отклонен, потому что тоже запускает приложение; прямой SQL к `dbs` не проверяет соединение. Проверка больше не подтверждает, что сам Superset использует Postgres для метаданных. Короткий запрос к Postgres показывает лишь готовность базы, а REST API - подключение к ClickHouse. Для `check-services` этого достаточно. После перезапуска Superset три прогона прошли 7/7 за 71, 67 и 70 секунд. `make config-test` прошел. `make smoke` дал 21/21 за 25 секунд; его код не менялся, прежний замер был 17 секунд. Неверный UUID дал 5/1: красной стала только проверка метаданных, пароль в вывод не попал. **Приемка #156 27 августа 2026 года.** `make rebuild-storage` занял 7 минут. На чистом прикладном мире `world_initialize` сам довел позицию до `8`, дождался `etl_pipeline` и оставил заполненными DDS и обе витрины. После пересборки прошли `make smoke` 21/21 и `make check-clickhouse` 10/10. Перед стендовыми сценариями прошли `make config-test` и `make lint`. Два прогона `world_next_day` не изменили слои; один ручной `etl_pipeline` без параметров догнал оба дня. Полный прогон `world_live_day` занял 27 м 2 с и завершился только после успешных DDS и DM. Временная синтаксическая поломка `sql/dm/daily_traffic_rebuild.sql` дала красную цепочку: DDS завершился успешно, а `daily_traffic`, DM, `etl_pipeline` и `world_live_day` покраснели. После возврата исправного SQL ручной `etl_pipeline` без параметров завершился успешно на позиции `12` и восстановил слои. Для проверки пакетного барьера `stg.hits_raw_mv` временно отсоединили командой `DETACH` на обеих нодах. После `world_next_day` отставание Kafka составило 46 367 (19 128 + 27 239). Во время ожидания матвью вернули командой `ATTACH`. Журнал готовности записал повторные опросы с отставанием 46 367, затем 27 239 и 0; после этого очередь `ods.event_dist` содержала 0 файлов на обеих нодах, и ETL завершился успешно. Проверка `EXISTS` снова дала `1` на обеих нодах, позиция равна `13`. После пересборки `make check-services` дважды дал 5/1: только импорт Superset не уложился в существующий тайм-аут 30 с. Диагностический пробник импорта с ожиданием 90 с прошел 3/0. Финальный штатный повтор прошел 7/7 за 129 с. **Перезамер 21 августа 2026 года при исполнении #124.** Образ Airflow собран с `apache-airflow-providers-clickhousedb==1.0.0`, `apache-airflow-providers-common-sql==2.0.1` и `clickhouse-connect==1.6.0`; `pip check` не нашёл несовместимых зависимостей. Адресная пересборка мира через `make rebuild-storage` заняла 3 м 5 с: все шесть задач DDL, `pull_batch` и `parse_batch` завершились успешно. После неё прошли смоук 21/21 за 17 с, проверка ClickHouse 10/10 и межсервисная проверка 7/7 за 100 с. Первый `make up` после замены образа попал в короткий рассинхрон запуска Airflow: API уже сообщил о здоровье и отдал `world_initialize` по GET, но POST нового запуска ещё ответил `Dag with dag_id not found`. Ошибок импорта не было; через несколько секунд повторный `make up` завершился успешно и не менял готовый мир. **Перезамер 21 августа 2026 года при исполнении #114.** После добавления `kafka-exporter` и источника ClickHouse смоук прошёл 21/21 за 16–17 секунд, `check-services` — 7/7 за 93 секунды. При остановленном экспортёре смоук дал 19/2: назвал нездоровый сервис и отсутствие одной из четырёх целей. После возврата сервиса прогон снова дал 21/21. После переноса #119 поверх этого состава повторный `make up` сохранил готовый мир, смоук прошёл 21/21, проверка ClickHouse — 10/10, межсервисная — 7/7. **Приёмка логики #119 до переноса поверх мониторинга 21 августа 2026 года.** `make rebuild-storage` через Airflow прошёл за 3 м 9 с: позиция прошла задачи `-1` → `0` → `8`, топики получили новые идентификаторы, группы дочитали новые топики без отставания, а ODS принял 401 185 событий без брака и 2 851 актуальный заказ. Задача `build_generator_image` заняла по 1,5 с в двух прогонах. Повторный `make up` занял 2 м 41 с вместе со штатным повторным запуском служб инициализации; `world_initialize` завершился за один цикл опроса и оставил счётчики неизменными. Отдельно проверены красные отказы `world_initialize` при `-1` и `world_next_day` при отсутствующей позиции, `0` и `-1`; `lifecycle` отказано в чтении `system.users`, но разрешено чтение `system.settings`. Итоговый холодный `make up` из пустых томов занял 9 м 39 с после 25 с на `make clean`. Даг создал отсутствующую позицию и закончил на `8`; ODS снова получил 401 185 событий без брака и 2 851 заказ. У `hits` два раздела, у `orders` один, отставание обеих групп равно нулю. На тогдашнем составе стенда прошли смоук 20/20, проверка ClickHouse 10/10 и межсервисная проверка 7/7. Оборванную очистку проверили после записи `world_position=-1`: остановили первую ноду ClickHouse, и `world_recreate` покраснел на удалении баз. Следующий `make up` тоже покраснел и оставил `-1`. После возврата ноды повторный `make rebuild-storage` восстановил мир. **Прототип 21 августа 2026 года при исследовании #110.** Предшествующая shell-версия узкой пересборки за 2 м 25 с пересоздала прикладные базы, отправила 401 185 событий и 6 656 версий заказов и вернула позицию с 11 на 8. Замер оставлен как ориентир цены, но не подтверждает нынешнюю реализацию Airflow: тот вариант не очищал Kafka и был отклонён как второй оркестратор мира. **Перезамер 20 августа 2026 года при исполнении #96, до переезда в Airflow.** `make up` после `make clean` прошёл за 8 м 18 с, повтор на живом стенде — за 3 м 35 с. Отправка семи слепков заняла около 28 секунд. Прежний замер ниже — 2 м 50 с и 1 м 5 с до появления приёма заказов и дагов модельного времени. Весь рост приписывать слепкам нельзя: на холодном прогоне основное время ушло ещё до сценариев ожидания: `airflow-init` завершился примерно на 320-й секунде, обработчик DAG стал здоров примерно на 458-й. Числа здесь называют цену нынешнего стенда, а не выносят вердикт о регрессии. **Перезамер 19 августа 2026 года при исполнении #95.** После добавления десятой проверки словаря `make check-clickhouse` на чисто поднятом стенде прошёл за 24 секунды. Это цена всей цели, а не одной новой строки. **Замер 9 августа 2026 года при появлении корневого `lint` (#65).** Три прогона подряд — 0,39, 0,36 и 0,39 секунды на трех файлах стенда, которые тогда входили в проверку; в таблице 0,4 с. На машине с непрогретым кешем первый прогон дороже: `uvx` сначала скачивает ruff. Тогда же снято, что двери не перекрываются: `ruff check --show-files` из корня перечислял ровно три файла стенда, из `generator/` — только файлы генератора. Корневой конфиг правила генератору не подменяет: конфиг ищется вверх по дереву от файла, и ближний выигрывает. **Перезамер 7 августа 2026 года при исполнении #42.** Подъём стенда с нуля (`make clean && make up`) — 2 м 50 с, из них 22,5 с занимает сама заливка: 18,7 с генерация восьми дней и 3,8 с доставка 401 185 сообщений в Kafka. Повторный `make up` на живом стенде — 1 м 5 с: мир заливается заново, и это не оплошность — `WatchID` те же, повтор схлопнет `ReplacingMergeTree`. Смоук — 9 с, `check-clickhouse` — 8 с с новой девятой проверкой. `make test` вырос до 71 с: 58 с прежних тестов плюс 13 с на пересборку восьми дней для сверки с паспортом. Прежние 40 с в таблице устарели ещё до #42 — тесты добавляли #41 и #43. Что новая проверка **умеет краснеть**, снято двумя поломками того же дня, и проверялись обе ветви её диагноза. Снесли партицию `2026-06-03` в `ods.event_rep` — проверка покраснела, показала недостающий день и назвала адрес: «таблица брака пуста, события не доехали до ODS, начните с чтеца топика». Затем положили в STG заведомо негодную строку — брак появился, и диагноз сменился на второй, с признаком для различения: «сойдётся недостача с числом брака — сломан разбор, не сойдётся — брак от прежних опытов». Строки опыта убраны, день переигран `make generate-batch GENERATOR_DAY=2`; счёт вернулся к 401 185, и это заодно показало дедупликацию: повтор дня не удвоил счёт под `FINAL`. Замеры 6 августа 2026 года, стенд поднят заранее. Время взято по `time` и совпадает с тем, что цель печатает сама. Оно плавает от прогона к прогону: смоук дал 8 секунд дважды (без проверки Kafka, до её появления, — 5 и 6), `check-services` — 43 и 44. В таблице стоит большее из замеренных. Перемер 7 августа 2026 года — приёмка этапа 2 целиком: `make clean` и `make up` подряд заняли 2 м 58 с и залили стартовый мир, дальше по порядку все три цели на стенде и три без него. Смоук дал 9 секунд, `check-clickhouse` — 8, `make test` — 71. `check-services` — 59 и 48; в таблицу по прежнему правилу пошло большее. Против 44 секунд накануне цель подорожала, и причину назвать нечем: между замерами и стартовый мир в хранилище, и холодный подъём перед первым прогоном. Число здесь — замер с датой, а не бюджет, и повод разбираться появится, когда за него кто-нибудь начнёт держаться. До деления `scripts/stand-smoke.sh` шёл 48 секунд на 25 проверок, из них 42 секунды съедали шесть: Kafka с машины, два запуска пробников Airflow и три проверки Superset. После деления те же 25 проверок разошлись по двум целям: 19 в смоуке и 6 в `check-services`. Содержание ни одной из них не менялось. Двадцатая проверка смоука — единственная новая: Kafka спрашивают с машины через отображённый порт. Деление оставило дыру, которой раньше не было. Про Airflow, Superset, Prometheus и Grafana смоук стучится с машины в отображённый порт, а про Kafka после переезда знал только «контейнер здоров» — а это вердикт проверки состояния из `compose.yaml`, и та спрашивает брокер изнутри по внутреннему слушателю. Внешняя дверь оставалась непроверенной до `check-services` с его минутой, хотя генератор пишет в Kafka именно с машины. Стоит проверка 2,6 секунды, и почти всё это — старт JVM в разовом контейнере; ожидание ответа ограничено пятнадцатью секундами, впятеро больше замеренного. Что обе разделённые цели умеют краснеть, проверено руками в тот же день. Со снятым `prometheus` смоук дал три ошибки и ненулевой код возврата. С подменённым ожидаемым UUID подключения Superset так же покраснел `check-services`. Новую проверку Kafka проверили её собственной поломкой: брокеру объявили адрес `kafka-nowhere:29092`, оставив внутренний слушатель целым, — проверка состояния контейнера осталась зелёной, а смоук покраснел именно на этой строке. Краснеют они и когда на машине не хватает команды из списка зависимостей. Семантика счётчиков Docker `OOMKilled` и `RestartCount`, на которой держится `check_containers_survived`, снята отдельными контейнерами и записана в ADR 0004, раздел «Что проверено». Разбор Bash из `make config-test` срезан 6 августа 2026 года, и вместе с ним — двенадцать строк обхода репозитория со списком файлов. Проверка была сломана с рождения: `bash -n` со списком файлов разбирает только первый, остальные уходят ему в аргументы, — то есть из пяти скриптов проверялся один, и за всё время цели этого никто не заметил. Заводить её заново незачем: скрипты стенда запускаются с той же машины, и синтаксическая ошибка вылезает при первом же запуске с номером строки. Разбор файлов DAG остался и по другому основанию — их на машине не запускает никто.