Типизированный ODS: ods.event, строгий приём и таблица ошибок #43

Closed
opened 2026-08-01 22:04:18 +03:00 by ddmitry · 9 comments
Owner

Part of #4.

Цель

Типизированный слой событий: ods.event со строгим приёмом, ошибки
разбора — в ods.event_errors. DDL пишется по рендеренному «описанию
выгрузки» из #36 — по правилу data contract: хранилище строится по
документации источника, как в бою.

Всё, что нужно, уже стоит: сырьё, топик и чтец — из #37, настоящие события
в топике — из #41. Тикет достраивает последний этаж цепочки
Kafka → STG → ODS.

Механика приёма решена до начала — docs/adr/0005-event-ingestion.md, имена
объектов — docs/adr/0006-object-naming.md, конвенции и свойства приёма —
docs/architecture/storage.md.

Откуда берутся события для проверок

Проигрыватель из #41 уже есть, поэтому фикстуру не выдумывать. Проверки
идут на настоящих событиях генератора: управляемую пачку даёт проигрыватель
ограниченным прогоном (способ решается в #41 и описан в спеке, раздел 9).

Пачка обязана нести события, которых в ODS ещё нет. Повтор схлопнет
ReplacingMergeTree, и сумма «события плюс ошибки» не сойдётся с числом
отправленного — опыт покраснеет без поломки. Отсюда и выбор дня: пачка
играет день за границей оси мира. Он заведомо не пересекается ни с зерновым
миром, ни с тем, что менти прожил дагом, а его строки лежат в собственной
партиции EventDate — той самой, которую опыт за собой и сносит.

Порченые сообщения делаются мутацией настоящего события, и берётся оно
из файла.
У проигрывателя #41 есть файловый приёмник, заведённый как раз
для проверок: срез дня пишется в файл, три строки из него портятся — опечатка
в имени поля, скаляр вместо объекта, битое значение UTCEventTime, — и в
топик уходит всё сразу, целая пачка и порченые вместе.

Брать образец из доехавшего сырья было бы естественнее, но дороже: пришлось
бы ждать доезда дважды. Приём асинхронный — Kafka-движок копит блок и
отдаёт его либо по размеру, либо по stream_flush_interval_ms, умолчание
7,5 секунды (сверено по документации ClickHouse через Context7, 6 августа
2026 года). Файл снимает второе ожидание: одна отправка, одно ожидание,
дальше все утверждения по уже доехавшему.

Что выяснилось при #37 — учесть здесь

  • Пустое сообщение и запись-надгробие строки не дают вовсеRawBLOB их
    читает, двигает офсет и молча не порождает строки; замер и довод — в доке
    хранилища. Для счётной сверки это значит: пустых сообщений в пачке быть не
    должно, а если счёт разошёлся — сначала посмотреть на дыру в kafka_offset,
    а не искать ошибку в разборе.
  • Порченые сообщения кладутся консольным продюсером из контейнера Kafka:
    docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic hits.
  • Ожидание доезда — опросом с таймаутом, а не sleep наугад. Образцы
    ограниченного цикла есть в scripts/stand-services.sh — ожидание удаления
    топика и ожидание состояния запуска DAG. В scripts/clickhouse-smoke.sh
    своего цикла нет, там только query_with_timeout — обёртка timeout.
  • Если разовый показ красного правит Python (например, ломает
    schema.py) — гонять с PYTHONDONTWRITEBYTECODE=1. Откат внутри секунды
    оставляет байт-код мутанта, и проверка зеленеет на сломанном коде.

Первым делом: четыре SELECT

На них стоят порядок классов брака, сам выбор формата чтеца и запасной путь
свёртки разбора, а документация поведение на не-объекте не описывает:

  • isValidJSON('123') — ожидается единица: скаляр тоже законный JSON;
  • JSONExtractKeys('123') — ожидается пустой массив;
  • JSONAsString на некорректном вводе — падает или пропускает;
  • форма именованного кортежа в JSONExtract с Nullable-членами — ею
    сворачиваются 47 вызовов в один, если разбор окажется дорогим.

Список — из ADR 0005, раздел «Что проверено», последние три пункта; четвёртый
стоит ещё и в разделе 11 мастер-спеки. Результаты записать в ADR 0005, туда же.

Если ответ разойдётся с ожиданием — это вопрос владельцу, а не починка на
месте: на этих SELECT стоит постановка, а не деталь реализации.

Что войдёт

  • ods.event_rep / ods.event_dist: ReplacingMergeTree по _load_ts,
    PARTITION BY EventDate, ключ по разделу 1.3 мастер-спеки (точную форму
    ORDER BY/SAMPLE BY проверить на стенде — раздел 11), шардирование
    cityHash64(ClientID). _load_ts переносится из STG как есть, не
    ставится заново: колонка отвечает на «когда строка приехала в хранилище».
    Работа у версии ровно одна — схлопнуть повтор доставки.
  • ods.event_errors_rep / _dist: сырой текст, метаданные доставки
    (kafka_topic, kafka_partition, kafka_offset, kafka_timestamp,
    consumer_host), метка загрузки _load_ts и класс брака error_class
    (LowCardinality(String)). Движок — ReplicatedMergeTree без замены
    версий: схлопывать брак не по чему. ORDER BY (error_class, kafka_partition, kafka_offset), шардирование cityHash64(raw), нарезка по
    дню загрузки, срок жизни месяц — дольше сырья, иначе разбираться будет уже
    нечем.
  • Файлы 20-ods-tables.sql (таблицы) и 30-ods-views.sql (матвью). Номера не
    косметика: матвью приёма из #37 лежит в 40-stg-views.sql и обязана
    создаваться последней — она включает чтение топика, и до появления разбора
    всё доехавшее минует ODS молча.
  • Две матвью над stg.hits_raw_dist: одна наполняет событие, вторая — ошибки.
    Условие второй — буквальное отрицание первой, а предикат собирается только из
    функций, которые не возвращают NULL и не бросают исключений. Первое —
    чтобы трёхзначная логика не съела строку между двумя матвью. Второе — потому
    что упавшая матвью роняет вставку и останавливает потребление до починки:
    это единственное, что сейчас держит правило «грязные записи не валят
    пайплайн». Приведение Nullable к необнуляемому типу и любую арифметику
    держать за предикатом, который NULL уже отсёк.
  • Строгий приём. Присутствие держит одно сравнение
    arraySort(JSONExtractKeys(raw)) с контрактным списком, завёрнутым в тот же
    arraySort: 47 CamelCase-имён, выписанных руками ровно в байтовом порядке, —
    ошибка, которая увела бы в брак вообще всё. Обязательны все 47 полей:
    генератор шлёт их все, «пусто» по контракту — пустое значение, а не
    отсутствие ключа. Тип держит Nullable-разбор пяти опорных колонок —
    WatchID, VisitID, ClientID, EventDate, UTCEventTime; остальные 42
    достаются обычными типами.
  • Три класса брака проверяются по порядку и пишутся в error_class первым
    совпавшим: not_an_object, keyset_mismatch, key_field_unparsed. Порядок
    обязателен, потому что классы пересекаются: скаляр проваливает и проверку на
    объект, и сверку ключей.
  • Сверка разобранного события против сырого текста, по всем 47 колонкам.
    Опечатка внутри JSONExtract даёт
    умолчание типа молча — пустой массив, ноль, пустую строку, — и это верно не
    только для двенадцати массивов, но и для сорока двух обычных колонок. Ловится
    это тем, что источники у двух сторон разные: выражения сверки собираются
    из контракта, выражения матвью пишутся руками по описанию выгрузки. Опечатка
    в одном месте не повторится в другом. Собрать выражения из контракта — разовая
    работа при исполнении (cd generator && uv run python -c ... по
    schema.COLUMNS), в репозиторий для этого не кладётся ничего: постоянный
    модуль обслуживал бы опыт, который проходит один раз.
  • Переименование scripts/clickhouse-smoke.sh в scripts/check-clickhouse.sh
    — отдельным первым коммитом PR, одним git mv. Имя досталось от старой цели
    smoke-cluster (#58). Тем же коммитом едут Makefile и таблица в
    docs/architecture/testing.md, иначе в истории останется коммит со сломанной
    целью; абзац-объяснение из testing.md уходит туда же.
  • Учебные комментарии в DDL — четыре мандата. Три из мастер-спеки: Sign
    колонка формата без механики (раздел 1.1); партиции по дням — осознанное
    отступление от месячных у Метрики (раздел 1.3); вырождение ключа сортировки —
    CounterID и EventDate константы, реальная сортировка по посетителю и
    событию (раздел 1.3). Четвёртый — от этого тикета: _load_ts переносится из
    сырья и служит колонкой версии, работа у которой ровно одна — схлопнуть
    повтор доставки.

Критерии приёмки

Постоянных проверок этот тикет не добавляет ни одной (решение владельца
7 августа 2026 года). Все три утверждения ниже проверяются разовыми опытами
при исполнении
, и след у них — запись в теле PR, как у «настоящего дня».
Правило, его довод и требование прибираться за собой — в
docs/architecture/testing.md, раздел «Интеграционная проверка постоянной целью
не становится»; здесь оно не переписывается.

Постоянным сторожем цепочки остаются счётчики #42 против мини-манифеста.
Что при этом честно теряется — тихая подмена значения в одной колонке:
понадобится сторож и на неё — дом у неё в паспорте манифеста (пара агрегатов
по детерминированному миру), а не в проверке, вкладывающей данные.

  • Четыре SELECT из «первым делом» прогнаны, результаты записаны
    в ADR 0005.
  • Разовый опыт. Строгий приём, три класса. Опечатка в имени поля даёт громкую ошибку
    в *_errors с классом keyset_mismatch, а не молчаливые нули (имена
    CamelCase регистрозависимы). Скаляр — not_an_object, а не
    keyset_mismatch: приоритет классов работает. Битое значение
    UTCEventTimekey_field_unparsed: Nullable-разбор опорных колонок
    работает. Одна проверка, три сообщения.
  • Разовый опыт. Инвариант слоёв. На управляемой пачке: отправили N сообщений —
    получили N строк сырья и N в сумме событий и ошибок. Пачка играет день
    за границей оси мира, и её партиция сносится по окончании опыта.
    Пачку обязательно
    обрамить
    : считать не по всей таблице, а по диапазону _load_ts этого
    прогона. Офсеты для обрамления ODS не годятся — метаданные доставки живут
    у stg.*_raw, а ods.event их не несёт (раздел 7 спеки); по офсетам
    можно считать только сырьё. Счёт по ODS — через FINAL: голый count()
    по ReplacingMergeTree зависит от числа прошедших мержей и запрещён
    разделом 6 спеки. Счёт — только по Distributed-таблицам.
    Осторожно с сочетанием фильтра и FINAL: документация ClickHouse
    говорит, что PREWHERE исполняется до FINAL и даёт перекос, если поле
    фильтра не входит в ORDER BY (сверено через Context7 6 августа
    2026 года). _load_ts туда не входит и не может — она колонка версии,
    а версия в ключе сортировки запрещена разделом 1.3 спеки. Значит
    обрамление по _load_ts под FINAL надо либо считать подзапросом,
    либо ставить apply_prewhere_after_final, и выбранный способ назвать
    в PR.
  • Разовый опыт. Сверка разобранного события. Известное событие сходится с сырым
    текстом по всем 47 колонкам; выражения сверки собраны из контракта, а не
    списаны с матвью. Пару «сырьё и событие» брать внутри окна хранения
    сырья: за трое суток строка в STG истекает, и сравнивать становится
    не с чем.
  • Каждый из трёх опытов один раз показан красным — руками, при
    исполнении, и это записано в теле PR: что сломали и что опыт сказал.
    Правило — docs/architecture/testing.md, «Проверка, которая не умеет
    краснеть, бесполезна»: утверждение, ни разу не видевшее красного,
    ничего не утверждает, даже разовое.
  • Настоящий день доезжает целиком — разовая приёмка, записанная в теле
    PR, а не постоянная проверка. Прогон нужен новый: матвью задним
    числом не досыпают, и день, залитый при исполнении #41, в ODS сам не
    появится — он останется только в сырье. Это не лишняя работа, а
    свойство матвью, и заодно первая сцена урока про переобработку.
    День в полсотни тысяч событий в повторяемую цель не ставится.
    Проверить: день доезжает до ods.event_dist; счёт после FINAL сходится с числом
    сгенерированных событий; *_errors пуст на честном прогоне; повторная
    заливка того же дня счёт в ODS не меняет. Три утверждения переехали
    сюда из #41 при развороте порядка: они про хранилище, а не про
    сериализатор.
  • Группа «сказано по памяти» в доке хранилища опустела. В ней осталось
    ровно два утверждения, и оба закрываются здесь:
    упавшая матвью останавливает потребление до починки — проверить
    опытом, удалив у матвью её цель, и цель эта — распределённая
    ods.event_dist. Удаление локальной ods.event_rep опыт не ставит:
    вставка в Distributed кладёт блок в спул и сразу возвращает управление
    (дока хранилища, «Приём»), ошибка всплывёт фоном, и утверждение
    покажется опровергнутым. На этом опыте висит правило
    «грязные записи не валят пайплайн»;
    матвью с источником-Distributed срабатывает на вставку именно в
    распределённую таблицу
    — обе матвью этого тикета стоят над
    stg.hits_raw_dist, так что утверждение подтверждается самим фактом
    доезда; записать это явно, а не считать закрытым молча.
  • _load_ts в ods.event равен _load_ts исходной строки сырья, а не
    времени разбора — проверено разово при исполнении, сравнением по
    WatchID, и записано в «Что проверено». Постоянной проверки не заводить:
    она сторожила бы исполнителя, а менти учит комментарий в DDL и абзац в
    доке хранилища.
  • Четыре учебных комментария в DDL на месте.
  • Документация хранилища обновлена тем же PR, включая раздел «Что
    проверено». Contract-тест снят, и это правка документов и кода.
    Места собрать грепом, а не по памяти: перечень в постановке был
    неполон и неточен — на 7 августа 2026 года упоминаний в документах
    шесть, и ADR 0005 упоминает один раз, а не два. Не были названы три,
    и два из них видны первому читателю: спека генератора, строка 37
    («Целевая картина одним взглядом») и строка 286 — обещание, что
    «каждый make up бесплатно прогоняет весь конвейер и contract-тест на
    настоящих данных». В коде мест три, все в
    generator/src/clickstream_generator/schema.py: обещание сторожа в
    докстринге модуля, «от этой записи зависит будущий contract-тест» в
    пункте про clickhouse_type и сам довод этого пункта — требование писать
    тип «ровно в той записи, в какой его вернёт system.columns» держалось на
    снимаемой сверке. Правило остаётся (параметры входят в имя типа целиком,
    без сокращений), но довод у него другой: по этой записи человек пишет DDL.
    Механизм остаётся один — строгий приём, — и рядом с
    ним сверка разобранного события, которой в спеке нет вовсе: она
    появилась позже. Довод снятия: сверка объявлений ловила сверх строгого
    приёма только смену типа, а сверка разобранного события ловит её же и
    на живых данных.
  • Карта целей в docs/architecture/testing.md постоянными проверками не
    пополняется — тикет их не добавляет; правится там только строка про
    переименованный скрипт.

Границы

  • Механизм применения DDL, топик, формат чтеца и имена решены в #37 и в
    ADR 0005/0006 — не переоткрывать.
  • Таблицы заказов — этап 3; dds.* и витрины — этап 4.
  • Логику генератора не трогать: события даёт проигрыватель из #41, сериализатор
    там же и он единственный. Постоянного модуля, печатающего контракт, тикет не
    заводит: выражения сверки собираются разовым запуском при исполнении.
  • Постоянных проверок в make check-clickhouse не заводить ни одной.
    Наблюдения, которые просятся проверкой, но учат лабой, — пошардовая
    раскладка (сырьё и событие ложатся на разные шарды,
    docs/architecture/storage.md), форма ключей и срок жизни в
    system.tables. Это материал лаб и содержание DDL, а не приёмки.
  • Мутационных сторожей не заводить: tests/smoke-guards.sh и цель
    make smoke-guards срезаны в #56 вместе с практикой. Доказуемость поломки
    несут форма проверки и разовый показ красного в теле PR.

Сначала прочитать

  • docs/adr/0005-event-ingestion.md — механика приёма и почему она такая.
    Оговорка к разделу «Цена решения»: утверждение, что опечатка в имени
    скалярного поля уводит строки в брак и видна сразу, верно только для пяти
    опорных колонок с Nullable-разбором; для остальных сорока двух она так же
    молчалива, как у массивов. В том же абзаце устарела и соседняя строка —
    «эти двенадцать колонок сторожит smoke»: цель переименована в
    check-clickhouse, а сторож массивов заменён сверкой по всем 47 колонкам.
    Поправить тем же PR обе.
  • docs/architecture/storage.md — конвенции имён, служебные колонки, свойства
    приёма, инвариант двух матвью, раздел «Что проверено».
  • docs/architecture/testing.md — карта целей: что утверждает каждая цель make
    и куда кладётся новая проверка.
  • docs/specs/2026-07-30-stand-v2-realism.md — разделы 1.1, 1.3, 1.4, 6, 7, 11.
  • docs/specs/2026-08-01-generator.md — раздел 3, форма на проводе в разделе 4
    и интерфейс проигрывателя в разделе 9.
  • generator/src/clickstream_generator/schema.py — источник контракта;
    schema_doc.py рядом — образец потребителя.
  • sql/ddl/ — DDL из #37; туда же кладутся файлы 20 и 30, служба
    clickhouse-init подхватывает их по порядку имён сама.
  • scripts/clickhouse-smoke.sh — его этот тикет только переименовывает в
    scripts/check-clickhouse.sh первым коммитом PR; проверок в него не
    добавляется.
  • scripts/stand-services.sh — образцы ограниченного цикла опроса.

Проверка

  • make up && make check-clickhouse — цель должна остаться такой же
    быстрой и с тем же числом проверок, что до тикета
Part of #4. ## Цель Типизированный слой событий: `ods.event` со строгим приёмом, ошибки разбора — в `ods.event_errors`. DDL пишется по рендеренному «описанию выгрузки» из #36 — по правилу data contract: хранилище строится по документации источника, как в бою. Всё, что нужно, уже стоит: сырьё, топик и чтец — из #37, настоящие события в топике — из #41. Тикет достраивает последний этаж цепочки Kafka → STG → ODS. Механика приёма решена до начала — docs/adr/0005-event-ingestion.md, имена объектов — docs/adr/0006-object-naming.md, конвенции и свойства приёма — docs/architecture/storage.md. ## Откуда берутся события для проверок Проигрыватель из #41 уже есть, поэтому **фикстуру не выдумывать**. Проверки идут на настоящих событиях генератора: управляемую пачку даёт проигрыватель ограниченным прогоном (способ решается в #41 и описан в спеке, раздел 9). **Пачка обязана нести события, которых в ODS ещё нет.** Повтор схлопнет `ReplacingMergeTree`, и сумма «события плюс ошибки» не сойдётся с числом отправленного — опыт покраснеет без поломки. Отсюда и выбор дня: пачка играет день за границей оси мира. Он заведомо не пересекается ни с зерновым миром, ни с тем, что менти прожил дагом, а его строки лежат в собственной партиции `EventDate` — той самой, которую опыт за собой и сносит. **Порченые сообщения делаются мутацией настоящего события, и берётся оно из файла.** У проигрывателя #41 есть файловый приёмник, заведённый как раз для проверок: срез дня пишется в файл, три строки из него портятся — опечатка в имени поля, скаляр вместо объекта, битое значение `UTCEventTime`, — и в топик уходит всё сразу, целая пачка и порченые вместе. Брать образец из доехавшего сырья было бы естественнее, но дороже: пришлось бы ждать доезда дважды. **Приём асинхронный** — Kafka-движок копит блок и отдаёт его либо по размеру, либо по `stream_flush_interval_ms`, умолчание 7,5 секунды (сверено по документации ClickHouse через Context7, 6 августа 2026 года). Файл снимает второе ожидание: одна отправка, одно ожидание, дальше все утверждения по уже доехавшему. ## Что выяснилось при #37 — учесть здесь - **Пустое сообщение и запись-надгробие строки не дают вовсе** — `RawBLOB` их читает, двигает офсет и молча не порождает строки; замер и довод — в доке хранилища. Для счётной сверки это значит: пустых сообщений в пачке быть не должно, а если счёт разошёлся — сначала посмотреть на дыру в `kafka_offset`, а не искать ошибку в разборе. - **Порченые сообщения кладутся консольным продюсером из контейнера Kafka:** `docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic hits`. - **Ожидание доезда — опросом с таймаутом, а не `sleep` наугад.** Образцы ограниченного цикла есть в `scripts/stand-services.sh` — ожидание удаления топика и ожидание состояния запуска DAG. В `scripts/clickhouse-smoke.sh` своего цикла нет, там только `query_with_timeout` — обёртка `timeout`. - **Если разовый показ красного правит Python** (например, ломает `schema.py`) — гонять с `PYTHONDONTWRITEBYTECODE=1`. Откат внутри секунды оставляет байт-код мутанта, и проверка зеленеет на сломанном коде. ## Первым делом: четыре `SELECT` На них стоят порядок классов брака, сам выбор формата чтеца и запасной путь свёртки разбора, а документация поведение на не-объекте не описывает: - `isValidJSON('123')` — ожидается единица: скаляр тоже законный JSON; - `JSONExtractKeys('123')` — ожидается пустой массив; - `JSONAsString` на некорректном вводе — падает или пропускает; - форма именованного кортежа в `JSONExtract` с `Nullable`-членами — ею сворачиваются 47 вызовов в один, если разбор окажется дорогим. Список — из ADR 0005, раздел «Что проверено», последние три пункта; четвёртый стоит ещё и в разделе 11 мастер-спеки. Результаты записать в ADR 0005, туда же. **Если ответ разойдётся с ожиданием** — это вопрос владельцу, а не починка на месте: на этих `SELECT` стоит постановка, а не деталь реализации. ## Что войдёт - `ods.event_rep` / `ods.event_dist`: ReplacingMergeTree по `_load_ts`, `PARTITION BY EventDate`, ключ по разделу 1.3 мастер-спеки (точную форму `ORDER BY`/`SAMPLE BY` проверить на стенде — раздел 11), шардирование `cityHash64(ClientID)`. `_load_ts` **переносится из STG как есть**, не ставится заново: колонка отвечает на «когда строка приехала в хранилище». Работа у версии ровно одна — схлопнуть повтор доставки. - `ods.event_errors_rep` / `_dist`: сырой текст, метаданные доставки (`kafka_topic`, `kafka_partition`, `kafka_offset`, `kafka_timestamp`, `consumer_host`), метка загрузки `_load_ts` и класс брака `error_class` (`LowCardinality(String)`). Движок — `ReplicatedMergeTree` без замены версий: схлопывать брак не по чему. `ORDER BY (error_class, kafka_partition, kafka_offset)`, шардирование `cityHash64(raw)`, нарезка по дню загрузки, срок жизни месяц — дольше сырья, иначе разбираться будет уже нечем. - Файлы `20-ods-tables.sql` (таблицы) и `30-ods-views.sql` (матвью). Номера не косметика: матвью приёма из #37 лежит в `40-stg-views.sql` и обязана создаваться последней — она включает чтение топика, и до появления разбора всё доехавшее минует ODS молча. - Две матвью над `stg.hits_raw_dist`: одна наполняет событие, вторая — ошибки. Условие второй — буквальное отрицание первой, а предикат собирается только из функций, которые не возвращают NULL **и не бросают исключений**. Первое — чтобы трёхзначная логика не съела строку между двумя матвью. Второе — потому что упавшая матвью роняет вставку и останавливает потребление до починки: это единственное, что сейчас держит правило «грязные записи не валят пайплайн». Приведение `Nullable` к необнуляемому типу и любую арифметику держать за предикатом, который NULL уже отсёк. - Строгий приём. Присутствие держит одно сравнение `arraySort(JSONExtractKeys(raw))` с контрактным списком, завёрнутым в тот же `arraySort`: 47 CamelCase-имён, выписанных руками ровно в байтовом порядке, — ошибка, которая увела бы в брак вообще всё. Обязательны все 47 полей: генератор шлёт их все, «пусто» по контракту — пустое значение, а не отсутствие ключа. Тип держит `Nullable`-разбор пяти опорных колонок — `WatchID`, `VisitID`, `ClientID`, `EventDate`, `UTCEventTime`; остальные 42 достаются обычными типами. - Три класса брака проверяются по порядку и пишутся в `error_class` первым совпавшим: `not_an_object`, `keyset_mismatch`, `key_field_unparsed`. Порядок обязателен, потому что классы пересекаются: скаляр проваливает и проверку на объект, и сверку ключей. - **Сверка разобранного события против сырого текста, по всем 47 колонкам.** Опечатка внутри `JSONExtract` даёт умолчание типа молча — пустой массив, ноль, пустую строку, — и это верно не только для двенадцати массивов, но и для сорока двух обычных колонок. Ловится это тем, что **источники у двух сторон разные**: выражения сверки собираются из контракта, выражения матвью пишутся руками по описанию выгрузки. Опечатка в одном месте не повторится в другом. Собрать выражения из контракта — разовая работа при исполнении (`cd generator && uv run python -c ...` по `schema.COLUMNS`), в репозиторий для этого не кладётся ничего: постоянный модуль обслуживал бы опыт, который проходит один раз. - Переименование `scripts/clickhouse-smoke.sh` в `scripts/check-clickhouse.sh` — отдельным первым коммитом PR, одним `git mv`. Имя досталось от старой цели `smoke-cluster` (#58). Тем же коммитом едут `Makefile` и таблица в docs/architecture/testing.md, иначе в истории останется коммит со сломанной целью; абзац-объяснение из testing.md уходит туда же. - Учебные комментарии в DDL — четыре мандата. Три из мастер-спеки: `Sign` — колонка формата без механики (раздел 1.1); партиции по дням — осознанное отступление от месячных у Метрики (раздел 1.3); вырождение ключа сортировки — `CounterID` и `EventDate` константы, реальная сортировка по посетителю и событию (раздел 1.3). Четвёртый — от этого тикета: `_load_ts` переносится из сырья и служит колонкой версии, работа у которой ровно одна — схлопнуть повтор доставки. ## Критерии приёмки **Постоянных проверок этот тикет не добавляет ни одной** (решение владельца 7 августа 2026 года). Все три утверждения ниже проверяются **разовыми опытами при исполнении**, и след у них — запись в теле PR, как у «настоящего дня». Правило, его довод и требование прибираться за собой — в docs/architecture/testing.md, раздел «Интеграционная проверка постоянной целью не становится»; здесь оно не переписывается. Постоянным сторожем цепочки остаются **счётчики #42 против мини-манифеста**. Что при этом честно теряется — тихая подмена значения в одной колонке: понадобится сторож и на неё — дом у неё в паспорте манифеста (пара агрегатов по детерминированному миру), а не в проверке, вкладывающей данные. - [x] Четыре `SELECT` из «первым делом» прогнаны, результаты записаны в ADR 0005. - [x] **Разовый опыт. Строгий приём, три класса.** Опечатка в имени поля даёт громкую ошибку в `*_errors` с классом `keyset_mismatch`, а не молчаливые нули (имена CamelCase регистрозависимы). Скаляр — `not_an_object`, а не `keyset_mismatch`: приоритет классов работает. Битое значение `UTCEventTime` — `key_field_unparsed`: `Nullable`-разбор опорных колонок работает. Одна проверка, три сообщения. - [x] **Разовый опыт. Инвариант слоёв.** На управляемой пачке: отправили N сообщений — получили N строк сырья и N в сумме событий и ошибок. Пачка играет день за границей оси мира, и её партиция сносится по окончании опыта. **Пачку обязательно обрамить**: считать не по всей таблице, а по диапазону `_load_ts` этого прогона. Офсеты для обрамления ODS не годятся — метаданные доставки живут у `stg.*_raw`, а `ods.event` их не несёт (раздел 7 спеки); по офсетам можно считать только сырьё. Счёт по ODS — через `FINAL`: голый `count()` по `ReplacingMergeTree` зависит от числа прошедших мержей и запрещён разделом 6 спеки. Счёт — только по Distributed-таблицам. **Осторожно с сочетанием фильтра и `FINAL`:** документация ClickHouse говорит, что PREWHERE исполняется до `FINAL` и даёт перекос, если поле фильтра не входит в `ORDER BY` (сверено через Context7 6 августа 2026 года). `_load_ts` туда не входит и не может — она колонка версии, а версия в ключе сортировки запрещена разделом 1.3 спеки. Значит обрамление по `_load_ts` под `FINAL` надо либо считать подзапросом, либо ставить `apply_prewhere_after_final`, и выбранный способ назвать в PR. - [x] **Разовый опыт. Сверка разобранного события.** Известное событие сходится с сырым текстом по всем 47 колонкам; выражения сверки собраны из контракта, а не списаны с матвью. Пару «сырьё и событие» брать внутри окна хранения сырья: за трое суток строка в STG истекает, и сравнивать становится не с чем. - [x] Каждый из трёх опытов один раз показан красным — руками, при исполнении, и это записано в теле PR: что сломали и что опыт сказал. Правило — docs/architecture/testing.md, «Проверка, которая не умеет краснеть, бесполезна»: утверждение, ни разу не видевшее красного, ничего не утверждает, даже разовое. - [x] **Настоящий день доезжает целиком** — разовая приёмка, записанная в теле PR, а не постоянная проверка. Прогон нужен **новый**: матвью задним числом не досыпают, и день, залитый при исполнении #41, в ODS сам не появится — он останется только в сырье. Это не лишняя работа, а свойство матвью, и заодно первая сцена урока про переобработку. День в полсотни тысяч событий в повторяемую цель не ставится. Проверить: день доезжает до `ods.event_dist`; счёт после `FINAL` сходится с числом сгенерированных событий; `*_errors` пуст на честном прогоне; повторная заливка того же дня счёт в ODS не меняет. Три утверждения переехали сюда из #41 при развороте порядка: они про хранилище, а не про сериализатор. - [x] Группа «сказано по памяти» в доке хранилища опустела. В ней осталось ровно два утверждения, и оба закрываются здесь: **упавшая матвью останавливает потребление до починки** — проверить опытом, удалив у матвью её цель, и цель эта — **распределённая** `ods.event_dist`. Удаление локальной `ods.event_rep` опыт не ставит: вставка в `Distributed` кладёт блок в спул и сразу возвращает управление (дока хранилища, «Приём»), ошибка всплывёт фоном, и утверждение покажется опровергнутым. На этом опыте висит правило «грязные записи не валят пайплайн»; **матвью с источником-`Distributed` срабатывает на вставку именно в распределённую таблицу** — обе матвью этого тикета стоят над `stg.hits_raw_dist`, так что утверждение подтверждается самим фактом доезда; записать это явно, а не считать закрытым молча. - [x] `_load_ts` в `ods.event` равен `_load_ts` исходной строки сырья, а не времени разбора — проверено разово при исполнении, сравнением по `WatchID`, и записано в «Что проверено». Постоянной проверки не заводить: она сторожила бы исполнителя, а менти учит комментарий в DDL и абзац в доке хранилища. - [x] Четыре учебных комментария в DDL на месте. - [x] Документация хранилища обновлена тем же PR, включая раздел «Что проверено». **Contract-тест снят, и это правка документов и кода.** Места собрать **грепом, а не по памяти**: перечень в постановке был неполон и неточен — на 7 августа 2026 года упоминаний в документах шесть, и ADR 0005 упоминает один раз, а не два. Не были названы три, и два из них видны первому читателю: спека генератора, строка 37 («Целевая картина одним взглядом») и строка 286 — обещание, что «каждый `make up` бесплатно прогоняет весь конвейер и contract-тест на настоящих данных». В коде мест три, все в `generator/src/clickstream_generator/schema.py`: обещание сторожа в докстринге модуля, «от этой записи зависит будущий contract-тест» в пункте про `clickhouse_type` и сам довод этого пункта — требование писать тип «ровно в той записи, в какой его вернёт `system.columns`» держалось на снимаемой сверке. Правило остаётся (параметры входят в имя типа целиком, без сокращений), но довод у него другой: по этой записи человек пишет DDL. Механизм остаётся один — строгий приём, — и рядом с ним сверка разобранного события, которой в спеке нет вовсе: она появилась позже. Довод снятия: сверка объявлений ловила сверх строгого приёма только смену типа, а сверка разобранного события ловит её же и на живых данных. - [x] Карта целей в docs/architecture/testing.md постоянными проверками не пополняется — тикет их не добавляет; правится там только строка про переименованный скрипт. ## Границы - Механизм применения DDL, топик, формат чтеца и имена решены в #37 и в ADR 0005/0006 — не переоткрывать. - Таблицы заказов — этап 3; `dds.*` и витрины — этап 4. - Логику генератора не трогать: события даёт проигрыватель из #41, сериализатор там же и он единственный. Постоянного модуля, печатающего контракт, тикет не заводит: выражения сверки собираются разовым запуском при исполнении. - Постоянных проверок в `make check-clickhouse` не заводить ни одной. Наблюдения, которые просятся проверкой, но учат лабой, — пошардовая раскладка (сырьё и событие ложатся на разные шарды, docs/architecture/storage.md), форма ключей и срок жизни в `system.tables`. Это материал лаб и содержание DDL, а не приёмки. - Мутационных сторожей не заводить: `tests/smoke-guards.sh` и цель `make smoke-guards` срезаны в #56 вместе с практикой. Доказуемость поломки несут форма проверки и разовый показ красного в теле PR. ## Сначала прочитать - docs/adr/0005-event-ingestion.md — механика приёма и почему она такая. Оговорка к разделу «Цена решения»: утверждение, что опечатка в имени скалярного поля уводит строки в брак и видна сразу, верно только для пяти опорных колонок с `Nullable`-разбором; для остальных сорока двух она так же молчалива, как у массивов. В том же абзаце устарела и соседняя строка — «эти двенадцать колонок сторожит smoke»: цель переименована в `check-clickhouse`, а сторож массивов заменён сверкой по всем 47 колонкам. Поправить тем же PR обе. - docs/architecture/storage.md — конвенции имён, служебные колонки, свойства приёма, инвариант двух матвью, раздел «Что проверено». - docs/architecture/testing.md — карта целей: что утверждает каждая цель `make` и куда кладётся новая проверка. - docs/specs/2026-07-30-stand-v2-realism.md — разделы 1.1, 1.3, 1.4, 6, 7, 11. - docs/specs/2026-08-01-generator.md — раздел 3, форма на проводе в разделе 4 и интерфейс проигрывателя в разделе 9. - generator/src/clickstream_generator/schema.py — источник контракта; `schema_doc.py` рядом — образец потребителя. - `sql/ddl/` — DDL из #37; туда же кладутся файлы 20 и 30, служба `clickhouse-init` подхватывает их по порядку имён сама. - scripts/clickhouse-smoke.sh — его этот тикет только переименовывает в scripts/check-clickhouse.sh первым коммитом PR; проверок в него не добавляется. - scripts/stand-services.sh — образцы ограниченного цикла опроса. ## Проверка - `make up && make check-clickhouse` — цель должна остаться такой же быстрой и с тем же числом проверок, что до тикета
ddmitry added the ready-for-agent label 2026-08-01 22:04:31 +03:00
Author
Owner

Постановка переписана после грилинга и двух холодных ревью (2026-08-03/04).

Что изменилось по существу:
— механизм строгого приёма другой: input_format_skip_unknown_fields при чтении байтами беспредметен. Присутствие держит сверка набора ключей (одно сравнение JSONExtractKeys с контрактным списком), тип — Nullable-разбор пяти опорных колонок, а не всех 47;
— служебная колонка зовётся _load_ts, не _ingested_at;
— имена объектов на суффиксе вида (ADR 0006): ods.event_rep/_dist, ods.event_errors_rep/_dist;
— у таблицы ошибок собственные ключи: шардирование cityHash64(raw), нарезка по дню загрузки, срок жизни месяц. У брака нет ни ClientID, ни EventDate;
— матвью две, и их условия обязаны делить поток без зазора и нахлёста: второе пишется отрицанием первого, предикат — без NULL;
— добавлен сторож колонок-массивов в smoke: contract-тест до выражений матвью не дотягивается, а опечатка в имени массива даёт пустой массив тихо;
— сверка «сырьё = события + ошибки» разовая, на управляемой пачке: постоянной она была бы красной в норме, сроки хранения слоёв разные.

Механика и доводы — docs/adr/0005-event-ingestion.md, конвенции — docs/architecture/storage.md.

Постановка переписана после грилинга и двух холодных ревью (2026-08-03/04). Что изменилось по существу: — механизм строгого приёма другой: input_format_skip_unknown_fields при чтении байтами беспредметен. Присутствие держит сверка набора ключей (одно сравнение JSONExtractKeys с контрактным списком), тип — Nullable-разбор пяти опорных колонок, а не всех 47; — служебная колонка зовётся _load_ts, не _ingested_at; — имена объектов на суффиксе вида (ADR 0006): ods.event_rep/_dist, ods.event_errors_rep/_dist; — у таблицы ошибок собственные ключи: шардирование cityHash64(raw), нарезка по дню загрузки, срок жизни месяц. У брака нет ни ClientID, ни EventDate; — матвью две, и их условия обязаны делить поток без зазора и нахлёста: второе пишется отрицанием первого, предикат — без NULL; — добавлен сторож колонок-массивов в smoke: contract-тест до выражений матвью не дотягивается, а опечатка в имени массива даёт пустой массив тихо; — сверка «сырьё = события + ошибки» разовая, на управляемой пачке: постоянной она была бы красной в норме, сроки хранения слоёв разные. Механика и доводы — docs/adr/0005-event-ingestion.md, конвенции — docs/architecture/storage.md.
Author
Owner

Переписано после холодного ревью и сверки утверждений о ClickHouse с
документацией. Против прежней постановки изменилось вот что.

У таблицы ошибок появился класс брака — колонка error_class и
обязательный порядок проверки. Классы пересекаются: сообщение, не являющееся
объектом JSON, проваливает заодно и сверку набора ключей, потому что
JSONExtractKeys от скаляра даёт пустой массив. Там же названы движок и ключ
сортировки таблицы.

Сверка ключей завёрнута в arraySort с обеих сторон. Иначе сорок семь
CamelCase-имён пришлось бы выписать руками ровно в байтовом порядке — ошибка,
которая увела бы в брак вообще всё.

_load_ts переносится из STG, а не ставится заново при разборе: колонка
отвечает на «когда строка приехала в хранилище». Работа у версии
ReplacingMergeTree одна — схлопнуть повтор доставки.

Счёт в smoke идёт через FINAL. Голый count() по ReplacingMergeTree
зависит от числа прошедших мержей, и раздел 6 спеки его прямо запрещает.

К предикату добавлено требование не бросать исключений. Упавшая матвью
роняет вставку и останавливает потребление до починки — это единственное, что
сейчас держит правило репозитория про грязные записи.

Появился шаг «первым делом» — три SELECT, на которых стоит порядок
классов брака и сам выбор формата чтеца. Документация поведение на не-объекте
не описывает, и до прогона это предположения.

Файлы теперь 20-ods-tables.sql и 30-ods-views.sql; нумерация связана с #37.

Переписано после холодного ревью и сверки утверждений о ClickHouse с документацией. Против прежней постановки изменилось вот что. **У таблицы ошибок появился класс брака** — колонка `error_class` и обязательный порядок проверки. Классы пересекаются: сообщение, не являющееся объектом JSON, проваливает заодно и сверку набора ключей, потому что `JSONExtractKeys` от скаляра даёт пустой массив. Там же названы движок и ключ сортировки таблицы. **Сверка ключей завёрнута в `arraySort` с обеих сторон.** Иначе сорок семь CamelCase-имён пришлось бы выписать руками ровно в байтовом порядке — ошибка, которая увела бы в брак вообще всё. **`_load_ts` переносится из STG**, а не ставится заново при разборе: колонка отвечает на «когда строка приехала в хранилище». Работа у версии `ReplacingMergeTree` одна — схлопнуть повтор доставки. **Счёт в smoke идёт через `FINAL`.** Голый `count()` по `ReplacingMergeTree` зависит от числа прошедших мержей, и раздел 6 спеки его прямо запрещает. **К предикату добавлено требование не бросать исключений.** Упавшая матвью роняет вставку и останавливает потребление до починки — это единственное, что сейчас держит правило репозитория про грязные записи. **Появился шаг «первым делом»** — три `SELECT`, на которых стоит порядок классов брака и сам выбор формата чтеца. Документация поведение на не-объекте не описывает, и до прогона это предположения. Файлы теперь `20-ods-tables.sql` и `30-ods-views.sql`; нумерация связана с #37.
Author
Owner

Вторая правка за день, по итогам холодного ревью тикетов как наряда на работу.

Главное: откуда берутся события. #41 с сериализатором и приёмником идёт
после этого тикета, а smoke здесь стоит на реальных событиях. Исполнитель
выписал бы событие из 47 полей руками — шестая копия контракта там, где
ADR 0005 насчитал пять, и вдобавок общая опечатка прошла бы зелёной сразу через
фикстуру и через контрольный список ключей в матвью. Теперь тикет требует
выводить фикстуру из generator/src/clickstream_generator/schema.py.

Форма дат на проводе зафиксирована — ISO-8601, записана в спеку генератора,
раздел 4. Раньше её не было нигде: ни в описании выгрузки (там типы ClickHouse
и numpy), ни в спеке генератора. Этот тикет форму пинит как первый потребитель,
#41 соблюдает.

Счётная сверка обрамлена. «N сообщений — N строк» по всей таблице красна со
второго прогона по построению: в сырье уже лежат сообщения приёмки #37, сырьё
копит строку за прогон, а ODS схлопывает повторы. Теперь считать по диапазону
_load_ts или по офсетам прогона, и рецепт проверки идёт с make clean.

Сторожа сторожей получили дом. Критерии «падает при рукотворном
расхождении» оформляются мутационными сторожами в tests/smoke-guards.sh, где
такая практика уже заведена. Разовое «сломал руками, посмотрел, вернул» следа
не оставляет и при ревью непроверяемо.

Из критерия про опечатку убрано «и на массиве» — это ровно та же
единственная сверка набора ключей, отдельного пути кода нет. Настоящий риск
массивов закрывает отдельный сторож, и он уже отдельным критерием.

Добавлены критерии, наблюдающие форму таблиц. Раньше ни один не смотрел на
то, что тикет же и назначает: исполнитель мог написать now64() вместо
переноса _load_ts из сырья и пройти приёмку — contract-тест видит только
system.columns.

Вторая правка за день, по итогам холодного ревью тикетов как наряда на работу. **Главное: откуда берутся события.** #41 с сериализатором и приёмником идёт **после** этого тикета, а smoke здесь стоит на реальных событиях. Исполнитель выписал бы событие из 47 полей руками — шестая копия контракта там, где ADR 0005 насчитал пять, и вдобавок общая опечатка прошла бы зелёной сразу через фикстуру и через контрольный список ключей в матвью. Теперь тикет требует выводить фикстуру из `generator/src/clickstream_generator/schema.py`. **Форма дат на проводе зафиксирована** — ISO-8601, записана в спеку генератора, раздел 4. Раньше её не было нигде: ни в описании выгрузки (там типы ClickHouse и numpy), ни в спеке генератора. Этот тикет форму пинит как первый потребитель, #41 соблюдает. **Счётная сверка обрамлена.** «N сообщений — N строк» по всей таблице красна со второго прогона по построению: в сырье уже лежат сообщения приёмки #37, сырьё копит строку за прогон, а ODS схлопывает повторы. Теперь считать по диапазону `_load_ts` или по офсетам прогона, и рецепт проверки идёт с `make clean`. **Сторожа сторожей получили дом.** Критерии «падает при рукотворном расхождении» оформляются мутационными сторожами в `tests/smoke-guards.sh`, где такая практика уже заведена. Разовое «сломал руками, посмотрел, вернул» следа не оставляет и при ревью непроверяемо. **Из критерия про опечатку убрано «и на массиве»** — это ровно та же единственная сверка набора ключей, отдельного пути кода нет. Настоящий риск массивов закрывает отдельный сторож, и он уже отдельным критерием. **Добавлены критерии, наблюдающие форму таблиц.** Раньше ни один не смотрел на то, что тикет же и назначает: исполнитель мог написать `now64()` вместо переноса `_load_ts` из сырья и пройти приёмку — contract-тест видит только `system.columns`.
Author
Owner

Третья правка, и повод у неё внешний: тикет отстал от репозитория на день.
Последняя редакция была 5 августа вечером, а шестого в main приехало четыре
вещи, которые его задевают, — шлагбаум учебной ценности в AGENTS.md (12:37),
срез проверок стенда #56 вместе с tests/smoke-guards.sh и целью
make smoke-guards (13:06), деление целей на smoke / check-clickhouse /
check-services #58 (14:29) и срез разбора Bash из config-test #59 (16:59).

Главное: критерии прогнаны через шлагбаум. Их было четырнадцать, и почти
все — проверки; это ровно тот механизм, который #56 назвал причиной
разрастания. Осталось одиннадцать пунктов, из них проверок в
make check-clickhouse — четыре вместо восьми: contract-тест, строгий приём,
инвариант слоёв, сторож массивов. Список объявлен закрытым прямо в критериях.

Под нож пошли три критерия.

Мутационные сторожа — дома у них больше нет, и снесён он по принципу, а не
по недосмотру. Критерий просил воскресить практику, срезанную накануне, и
просил именно критерием приёмки. Вместо него — форма проверки, которая не
умеет позеленеть вхолостую (сверка обоюдная, пустой список — ошибка; строка
сначала найдена, потом прочитана), плюс разовый показ красного с записью
в тело PR.

_load_ts перенесён, а не now64() — фраза «чему научится менти» не
складывается: единственный способ сломать это свойство — переписать матвью,
то есть проверка сторожит исполнителя. Тикет мотив и называл прямо: «иначе
now64() в матвью пройдёт приёмку незамеченным». Свойство осталось критерием,
но как разовый опыт с записью в «Что проверено». Самый спорный из трёх срезов:
проверка дешёвая. Но подмена на now64() версию ReplacingMergeTree не
ломает — повтор доставки схлопнется и так, — значит урок словесный, и несут
его комментарий в DDL и абзац в доке.

Ключ шардирования и TTL в system.tables — проверка утверждает, что
написанное написано, читая тот же DDL через другое окно. Ценно тут другое, и
это лаба, а не проверка: сырьё и разобранное из него событие ложатся на разные
шарды, потому что ключи у слоёв разные. Раздел 6 спеки прямо относит
пошардовые наблюдения к лабам. Оба наблюдения записаны в «Границы», чтобы не
вернулись самотёком.

Слиты две пары: «событие доезжает до ods.event_dist» поглощён инвариантом
слоёв (сошёлся счёт — доезд доказан), а not_an_object и keyset_mismatch
стали одной проверкой с двумя сообщениями.

Закрыты три вопроса, висевшие в постановке.

Где живёт код фикстуры и contract-теста — в пакете генератора, рядом со
schema_doc. Контракт уже кормит трёх потребителей, стенд становится
четвёртым, и docstring schema.py этого потребителя прямо ждёт. В
generator/tests/ нельзя: make test обязан работать без стенда. Граница
«генератор не трогать» переформулирована — не трогать логику, сериализатор и
приёмник это #41; читающий контракт модуль частью генератора не является.

Что делать с проверочными событиямиEventDate фикстуры 2000-01-01, вне
модельного календаря. ods.event нарезан по EventDate и хранит всё, а
проверка гоняется повторно; дата вне календаря даёт отдельную партицию, строки
видно глазами, ни в один срез модельного дня они не попадают и снимаются одной
командой, если помешают. Уборки в проверке поэтому нет. CounterID — тот же,
что у мира: второе значение сломало бы учебный комментарий про вырождение
ключа.

Переименование скрипта — да, в этом PR, отдельным первым коммитом одним
git mv, до содержательных правок. Иначе git в одном коммите перестанет
видеть переименование: файл сильно правится. Абзац-объяснение в
testing.md уходит вместе с ним.

Починены мёртвые ссылки. make smoke-clustermake check-clickhouse.
wait_for_local_table как образец опроса — файла нет, готового цикла в
clickhouse-smoke.sh тоже (там только query_with_timeout, обёртка
timeout), про это сказано прямо. Приём uv run из config-test.sh не
годился: там uv run --no-project, окружения проекта он не даёт и
clickstream_generator из него не импортируется — правильный образец
cd generator && uv run, как у make docs.

Три SELECT стали четырьмя. Тикет расщепил второй пункт списка ADR 0005
на два и потерял форму именованного кортежа в JSONExtract с
Nullable-членами — а она стоит ещё и в разделе 11 мастер-спеки как
открытая. Теперь список сходится с ADR.

Рецепт закрытия «упавшая матвью останавливает потребление» уточнён.
Прежний пример — создать матвью на несуществующую цель — может и не пройти:
случай в документации ClickHouse не описан, сверено через Context7 6 августа.
Рабочий путь назван: создать матвью нормально, удалить цель, посмотреть,
встало ли потребление, восстановить.

Правило «фикстура выводится из контракта» получило недостающую половину.
schema.py несёт имена и типы, но не значения — правила «тип ClickHouse →
значение на проводе» нет нигде, и вывести его придётся этим тикетом. Раньше
шаг подавался как бесплатный.

Третья правка, и повод у неё внешний: тикет отстал от репозитория на день. Последняя редакция была 5 августа вечером, а шестого в main приехало четыре вещи, которые его задевают, — шлагбаум учебной ценности в AGENTS.md (12:37), срез проверок стенда #56 вместе с `tests/smoke-guards.sh` и целью `make smoke-guards` (13:06), деление целей на `smoke` / `check-clickhouse` / `check-services` #58 (14:29) и срез разбора Bash из `config-test` #59 (16:59). **Главное: критерии прогнаны через шлагбаум.** Их было четырнадцать, и почти все — проверки; это ровно тот механизм, который #56 назвал причиной разрастания. Осталось одиннадцать пунктов, из них проверок в `make check-clickhouse` — четыре вместо восьми: contract-тест, строгий приём, инвариант слоёв, сторож массивов. Список объявлен закрытым прямо в критериях. Под нож пошли три критерия. **Мутационные сторожа** — дома у них больше нет, и снесён он по принципу, а не по недосмотру. Критерий просил воскресить практику, срезанную накануне, и просил именно критерием приёмки. Вместо него — форма проверки, которая не умеет позеленеть вхолостую (сверка обоюдная, пустой список — ошибка; строка сначала найдена, потом прочитана), плюс разовый показ красного с записью в тело PR. **`_load_ts` перенесён, а не `now64()`** — фраза «чему научится менти» не складывается: единственный способ сломать это свойство — переписать матвью, то есть проверка сторожит исполнителя. Тикет мотив и называл прямо: «иначе `now64()` в матвью пройдёт приёмку незамеченным». Свойство осталось критерием, но как разовый опыт с записью в «Что проверено». Самый спорный из трёх срезов: проверка дешёвая. Но подмена на `now64()` версию `ReplacingMergeTree` не ломает — повтор доставки схлопнется и так, — значит урок словесный, и несут его комментарий в DDL и абзац в доке. **Ключ шардирования и TTL в `system.tables`** — проверка утверждает, что написанное написано, читая тот же DDL через другое окно. Ценно тут другое, и это лаба, а не проверка: сырьё и разобранное из него событие ложатся на разные шарды, потому что ключи у слоёв разные. Раздел 6 спеки прямо относит пошардовые наблюдения к лабам. Оба наблюдения записаны в «Границы», чтобы не вернулись самотёком. Слиты две пары: «событие доезжает до `ods.event_dist`» поглощён инвариантом слоёв (сошёлся счёт — доезд доказан), а `not_an_object` и `keyset_mismatch` стали одной проверкой с двумя сообщениями. **Закрыты три вопроса, висевшие в постановке.** *Где живёт код фикстуры и contract-теста* — в пакете генератора, рядом со `schema_doc`. Контракт уже кормит трёх потребителей, стенд становится четвёртым, и docstring `schema.py` этого потребителя прямо ждёт. В `generator/tests/` нельзя: `make test` обязан работать без стенда. Граница «генератор не трогать» переформулирована — не трогать логику, сериализатор и приёмник это #41; читающий контракт модуль частью генератора не является. *Что делать с проверочными событиями* — `EventDate` фикстуры `2000-01-01`, вне модельного календаря. `ods.event` нарезан по `EventDate` и хранит всё, а проверка гоняется повторно; дата вне календаря даёт отдельную партицию, строки видно глазами, ни в один срез модельного дня они не попадают и снимаются одной командой, если помешают. Уборки в проверке поэтому нет. `CounterID` — тот же, что у мира: второе значение сломало бы учебный комментарий про вырождение ключа. *Переименование скрипта* — да, в этом PR, отдельным первым коммитом одним `git mv`, до содержательных правок. Иначе `git` в одном коммите перестанет видеть переименование: файл сильно правится. Абзац-объяснение в `testing.md` уходит вместе с ним. **Починены мёртвые ссылки.** `make smoke-cluster` → `make check-clickhouse`. `wait_for_local_table` как образец опроса — файла нет, готового цикла в `clickhouse-smoke.sh` тоже (там только `query_with_timeout`, обёртка `timeout`), про это сказано прямо. Приём `uv run` из `config-test.sh` не годился: там `uv run --no-project`, окружения проекта он не даёт и `clickstream_generator` из него не импортируется — правильный образец `cd generator && uv run`, как у `make docs`. **Три `SELECT` стали четырьмя.** Тикет расщепил второй пункт списка ADR 0005 на два и потерял форму именованного кортежа в `JSONExtract` с `Nullable`-членами — а она стоит ещё и в разделе 11 мастер-спеки как открытая. Теперь список сходится с ADR. **Рецепт закрытия «упавшая матвью останавливает потребление» уточнён.** Прежний пример — создать матвью на несуществующую цель — может и не пройти: случай в документации ClickHouse не описан, сверено через Context7 6 августа. Рабочий путь назван: создать матвью нормально, удалить цель, посмотреть, встало ли потребление, восстановить. **Правило «фикстура выводится из контракта» получило недостающую половину.** `schema.py` несёт имена и типы, но не значения — правила «тип ClickHouse → значение на проводе» нет нигде, и вывести его придётся этим тикетом. Раньше шаг подавался как бесплатный.
Author
Owner

Четвёртая правка. Тикет переставлен после #41 и заметно упростился;
заодно в него легли находки холодного ревью.

Перестановка. Прежний порядок заставлял этот тикет выдумывать событие —
своим модулем, печатающим JSON, вторым местом сериализации против правила
раздела 4 спеки. Теперь события даёт проигрыватель, и всё, что вокруг
фикстуры наросло, ушло: правило «тип ClickHouse → значение на проводе»,
дата фикстуры вне модельного календаря, форма на проводе, конфликт с
единственным сериализатором. Модуль в пакете генератора остался, но
похудел до одной работы — печатать контракт машинно; событий он не собирает.

Порченые сообщения теперь делаются мутацией настоящего события, а не
собираются с нуля. Это и честнее: так ломается настоящая выгрузка.

Из #41 приехали три утверждения — день доезжает до ods.event через обе
ноды, *_errors пуст на честном прогоне, повтор не меняет счёт после
дедупа. Они про хранилище, а не про сериализатор. Оформлены разовой
приёмкой в теле PR, а не постоянной проверкой: день в полсотни тысяч
событий в повторяемую цель не ставится.

Холодное ревью, линия дефектов. Снят хвост «именно clean» — он
противоречил обрамлению пачки, которое сам же и отменяет. Обрамление ODS
офсетами убрано: метаданные доставки живут у stg.*_raw, ods.event их не
несёт (раздел 7 спеки), а положи их туда — покраснеет договор со схемой.
Рецепт опыта с упавшей матвью теперь называет объект: удалять надо
распределённую ods.event_dist, а не локальную; на локальной вставка в
Distributed вернёт управление сразу, и утверждение показалось бы
опровергнутым — а на нём стоит правило «грязные записи не валят пайплайн».
Ссылка на образец цикла опроса исправлена: циклы есть в
scripts/stand-services.sh, я зря написал, что их нет. Названы _load_ts
в таблице ошибок, перенумерация проверок в скрипте и то, что Makefile
едет тем же коммитом, что и переименование скрипта.

Холодное ревью, линия уместности. Сторож массивов заменён на сверку
разобранного события по всем 47 колонкам. Довод ревьюера сильнее моего:
тихо пустой массив ничем не отличается от тихого нуля в остальных сорока
двух колонках — опечатка внутри JSONExtract даёт умолчание типа везде.
Ловится это тем, что источники у двух сторон разные: выражения сверки
собираются из контракта, выражения матвью пишутся руками по описанию
выгрузки.

Побочно вскрылась неточность в ADR 0005: там сказано, что опечатка в имени
скалярного поля уводит строки в брак и видна сразу. Верно только для пяти
опорных колонок с Nullable-разбором. Поправить тем же PR — записано
в «сначала прочитать».

Добавлен третий класс брака в проверку строгого приёма: key_field_unparsed
был объявлен и нигде не показан, а он самый жизненный из трёх. Сказано, что
отправка одна на все проверки — иначе трижды платим ожиданием сброса буфера
Kafka. Добавлен четвёртый учебный комментарий в DDL, про _load_ts как
колонку версии: постоянную проверку на это мы срезали, значит урок целиком
на комментарии. И сказано, что делать, если четыре SELECT ответят не так,
как ждёт постановка, — это вопрос владельцу, а не починка на месте.

Про цену make check-clickhouse: замер в карте целей перестанет быть верным,
и его надо перезамерить. Порога по времени в репозитории нет и заводить его
не следует — цена там замеренное число с датой, а не назначенный предел.

Четвёртая правка. Тикет переставлен **после** #41 и заметно упростился; заодно в него легли находки холодного ревью. **Перестановка.** Прежний порядок заставлял этот тикет выдумывать событие — своим модулем, печатающим JSON, вторым местом сериализации против правила раздела 4 спеки. Теперь события даёт проигрыватель, и всё, что вокруг фикстуры наросло, ушло: правило «тип ClickHouse → значение на проводе», дата фикстуры вне модельного календаря, форма на проводе, конфликт с единственным сериализатором. Модуль в пакете генератора остался, но похудел до одной работы — печатать контракт машинно; событий он не собирает. Порченые сообщения теперь делаются мутацией настоящего события, а не собираются с нуля. Это и честнее: так ломается настоящая выгрузка. **Из #41 приехали три утверждения** — день доезжает до `ods.event` через обе ноды, `*_errors` пуст на честном прогоне, повтор не меняет счёт после дедупа. Они про хранилище, а не про сериализатор. Оформлены разовой приёмкой в теле PR, а не постоянной проверкой: день в полсотни тысяч событий в повторяемую цель не ставится. **Холодное ревью, линия дефектов.** Снят хвост «именно `clean`» — он противоречил обрамлению пачки, которое сам же и отменяет. Обрамление ODS офсетами убрано: метаданные доставки живут у `stg.*_raw`, `ods.event` их не несёт (раздел 7 спеки), а положи их туда — покраснеет договор со схемой. Рецепт опыта с упавшей матвью теперь называет объект: удалять надо распределённую `ods.event_dist`, а не локальную; на локальной вставка в `Distributed` вернёт управление сразу, и утверждение показалось бы опровергнутым — а на нём стоит правило «грязные записи не валят пайплайн». Ссылка на образец цикла опроса исправлена: циклы есть в `scripts/stand-services.sh`, я зря написал, что их нет. Названы `_load_ts` в таблице ошибок, перенумерация проверок в скрипте и то, что `Makefile` едет тем же коммитом, что и переименование скрипта. **Холодное ревью, линия уместности.** Сторож массивов заменён на сверку разобранного события по всем 47 колонкам. Довод ревьюера сильнее моего: тихо пустой массив ничем не отличается от тихого нуля в остальных сорока двух колонках — опечатка внутри `JSONExtract` даёт умолчание типа везде. Ловится это тем, что источники у двух сторон разные: выражения сверки собираются из контракта, выражения матвью пишутся руками по описанию выгрузки. Побочно вскрылась неточность в ADR 0005: там сказано, что опечатка в имени скалярного поля уводит строки в брак и видна сразу. Верно только для пяти опорных колонок с `Nullable`-разбором. Поправить тем же PR — записано в «сначала прочитать». Добавлен третий класс брака в проверку строгого приёма: `key_field_unparsed` был объявлен и нигде не показан, а он самый жизненный из трёх. Сказано, что отправка одна на все проверки — иначе трижды платим ожиданием сброса буфера Kafka. Добавлен четвёртый учебный комментарий в DDL, про `_load_ts` как колонку версии: постоянную проверку на это мы срезали, значит урок целиком на комментарии. И сказано, что делать, если четыре `SELECT` ответят не так, как ждёт постановка, — это вопрос владельцу, а не починка на месте. Про цену `make check-clickhouse`: замер в карте целей перестанет быть верным, и его надо перезамерить. Порога по времени в репозитории нет и заводить его не следует — цена там замеренное число с датой, а не назначенный предел.
ddmitry changed title from Типизированный ODS: ods.event, строгий приём и contract-тест to Типизированный ODS: ods.event, строгий приём и таблица ошибок 2026-08-06 22:00:46 +03:00
Author
Owner

Contract-тест снят. Решение владельца после разбора: сверка system.columns
со списком из контракта ловила сверх строгого приёма ровно одно — смену типа
колонки. А смену типа ловит и сверка разобранного события, причём лучше: не на
объявлениях, а на живых данных. Сверх соседки сверка объявлений не давала
ничего.

Всё, что действительно ломается — поле пропало, появилось, переименовано, —
ловит строгий приём, и ловит громко: события уходят в *_errors все до
единого. Это часть продукта, а не проверка.

Понятие data contract на стенде остаётся видимым дважды: контракт есть
артефактом (schema.py и собранное из него описание выгрузки) и его соблюдение
видно работающим (*_errors с классом брака). Убрана церемония вокруг
экспоната, а не экспонат.

Довод «в бою так пишут» проверки не спас: в бою источник и хранилище живут в
разных репозиториях с разными релизами, оттуда такие сверки и растут. Здесь оба
списка правятся одним коммитом. И форма в бою другая — реестр схем у Kafka или
тесты на данные, а не рукописное сличение с питоновским модулем.

Проверок в make check-clickhouse осталось три: строгий приём, инвариант слоёв,
сверка разобранного события. Модуль в пакете генератора остался — он нужен
третьей проверке, чтобы собрать выражения сличения из контракта, а не списать
их с матвью.

Название тикета поправлено. Правка документов — мандатом в критерии: мастер-спека
1.4 и спека генератора 3 называют contract-тест одним из двух механизмов границы,
ADR 0005 упоминает дважды, карта проверок зовёт «договором со схемой событий».
Заодно снялся вопрос о термине: сверять контракт с объявлениями больше нечего.

Ранее в тот же заход по холодному ревью: снято «через обе ноды» из критерия про
настоящий день (в ODS нет метаданных доставки); записана ловушка PREWHERE до
FINAL при обрамлении по _load_ts; сказано, что матвью задним числом не
досыпают и прогон после создания ODS нужен новый; восстановлена зависимость
#43#42, которую разворот порядка порвал.

**Contract-тест снят.** Решение владельца после разбора: сверка `system.columns` со списком из контракта ловила сверх строгого приёма ровно одно — смену типа колонки. А смену типа ловит и сверка разобранного события, причём лучше: не на объявлениях, а на живых данных. Сверх соседки сверка объявлений не давала ничего. Всё, что действительно ломается — поле пропало, появилось, переименовано, — ловит строгий приём, и ловит громко: события уходят в `*_errors` все до единого. Это часть продукта, а не проверка. Понятие data contract на стенде остаётся видимым дважды: контракт есть артефактом (`schema.py` и собранное из него описание выгрузки) и его соблюдение видно работающим (`*_errors` с классом брака). Убрана церемония вокруг экспоната, а не экспонат. Довод «в бою так пишут» проверки не спас: в бою источник и хранилище живут в разных репозиториях с разными релизами, оттуда такие сверки и растут. Здесь оба списка правятся одним коммитом. И форма в бою другая — реестр схем у Kafka или тесты на данные, а не рукописное сличение с питоновским модулем. Проверок в `make check-clickhouse` осталось три: строгий приём, инвариант слоёв, сверка разобранного события. Модуль в пакете генератора остался — он нужен третьей проверке, чтобы собрать выражения сличения из контракта, а не списать их с матвью. Название тикета поправлено. Правка документов — мандатом в критерии: мастер-спека 1.4 и спека генератора 3 называют contract-тест одним из двух механизмов границы, ADR 0005 упоминает дважды, карта проверок зовёт «договором со схемой событий». Заодно снялся вопрос о термине: сверять контракт с объявлениями больше нечего. Ранее в тот же заход по холодному ревью: снято «через обе ноды» из критерия про настоящий день (в ODS нет метаданных доставки); записана ловушка PREWHERE до `FINAL` при обрамлении по `_load_ts`; сказано, что матвью задним числом не досыпают и прогон после создания ODS нужен новый; восстановлена зависимость #43 → #42, которую разворот порядка порвал.
Author
Owner

Три проверки становятся разовыми опытами. Решение владельца 7 августа
2026 года, по итогам холодного ревью связки #41/#42/#43.

Довод. Все три не спрашивают стенд, а кормят его: строгий приём вкладывает
в топик три порченых сообщения, инвариант слоёв — целую пачку, а сверка
разобранного события требует, чтобы у одного события разом нашлись строка в
сырье и строка в ODS. Последняя вдобавок хрупка по устройству: сырьё живёт трое
суток, и на стенде, поднятом неделю назад, она покраснела бы не от поломки.

Главное — вложенное остаётся. У ODS срока хранения нет, а make check-clickhouse
гоняют не задумываясь, семь секунд. После каждого прогона в мире менти
оседали бы события, которых мир не рождал, — и неотличимые от настоящих:
та же схема, тот же генератор. Менти считает конверсию и молча считает вместе
с ними. Интеграционная проверка — не часть регулярной жизни стенда, максимум
тема урока.

Что стережёт цепочку вместо них. Счётчики #42 против мини-манифеста: они
считают настоящий мир, ничего не вкладывают и ловят ту же поломку, ради которой
стоял инвариант слоёв, — сломалась матвью разбора, события поехали в брак, счёт
разошёлся. Причём на четырёхстах тысячах строк, а не на пачке из десятка.
Теряется честно одно: тихая подмена значения в одной колонке. Понадобится
сторож и на неё — дом у неё в паспорте манифеста, парой агрегатов по
детерминированному миру, а не в проверке, вкладывающей данные.

Уборка обязательна. Управляемая пачка играет день за границей оси мира:
её строки ложатся в собственную партицию EventDate, и партиция сносится по
окончании опыта. Партиция как единица уборки — тот же механизм, на котором
стоит переобработка дня X. Порченые сообщения остаются в *_errors: там
нарезка по дню загрузки, снос задел бы настоящие ошибки того же дня, а брак —
журнал, а не данные мира.

Попутно отпали два пункта: сквозная перенумерация проверок в скрипте
(их остаётся восемь) и расширение ensure_stand_running на Kafka — брокер был
нужен только этим проверкам. Переименование clickhouse-smoke.sh в
check-clickhouse.sh остаётся: это гигиена имени, а не проверка.

Из холодного ревью сюда же легли две правки документов. Список мест, где
спека упоминает снимаемый contract-тест, был неполон и неточен: упоминаний
шесть, ADR 0005 упоминает один раз, а не два, и не были названы три — включая
строку 37 спеки генератора («Целевая картина одним взглядом») и строку 286, где
обещано, что «каждый make up бесплатно прогоняет весь конвейер и
contract-тест на настоящих данных». Теперь велено собирать грепом, а не по
памяти. Второе: README сейчас говорит, что uv нужен только проверкам без
стенда, а этот тикет заводит модуль генератора, который зовут при опытах на
стенде.

Само правило вынесено в docs/architecture/testing.md — чтобы решалось по
карте, а не заново в каждом тикете.

**Три проверки становятся разовыми опытами. Решение владельца 7 августа 2026 года, по итогам холодного ревью связки #41/#42/#43.** **Довод.** Все три не спрашивают стенд, а кормят его: строгий приём вкладывает в топик три порченых сообщения, инвариант слоёв — целую пачку, а сверка разобранного события требует, чтобы у одного события разом нашлись строка в сырье и строка в ODS. Последняя вдобавок хрупка по устройству: сырьё живёт трое суток, и на стенде, поднятом неделю назад, она покраснела бы не от поломки. Главное — вложенное остаётся. У ODS срока хранения нет, а `make check-clickhouse` гоняют не задумываясь, семь секунд. После каждого прогона в мире менти оседали бы события, которых мир не рождал, — и неотличимые от настоящих: та же схема, тот же генератор. Менти считает конверсию и молча считает вместе с ними. Интеграционная проверка — не часть регулярной жизни стенда, максимум тема урока. **Что стережёт цепочку вместо них.** Счётчики #42 против мини-манифеста: они считают настоящий мир, ничего не вкладывают и ловят ту же поломку, ради которой стоял инвариант слоёв, — сломалась матвью разбора, события поехали в брак, счёт разошёлся. Причём на четырёхстах тысячах строк, а не на пачке из десятка. Теряется честно одно: тихая подмена значения в одной колонке. Понадобится сторож и на неё — дом у неё в паспорте манифеста, парой агрегатов по детерминированному миру, а не в проверке, вкладывающей данные. **Уборка обязательна.** Управляемая пачка играет день за границей оси мира: её строки ложатся в собственную партицию `EventDate`, и партиция сносится по окончании опыта. Партиция как единица уборки — тот же механизм, на котором стоит переобработка дня X. Порченые сообщения остаются в `*_errors`: там нарезка по дню загрузки, снос задел бы настоящие ошибки того же дня, а брак — журнал, а не данные мира. **Попутно отпали два пункта:** сквозная перенумерация проверок в скрипте (их остаётся восемь) и расширение `ensure_stand_running` на Kafka — брокер был нужен только этим проверкам. Переименование `clickhouse-smoke.sh` в `check-clickhouse.sh` остаётся: это гигиена имени, а не проверка. **Из холодного ревью сюда же легли две правки документов.** Список мест, где спека упоминает снимаемый contract-тест, был неполон и неточен: упоминаний шесть, ADR 0005 упоминает один раз, а не два, и не были названы три — включая строку 37 спеки генератора («Целевая картина одним взглядом») и строку 286, где обещано, что «каждый `make up` бесплатно прогоняет весь конвейер и contract-тест на настоящих данных». Теперь велено собирать грепом, а не по памяти. Второе: README сейчас говорит, что `uv` нужен только проверкам **без** стенда, а этот тикет заводит модуль генератора, который зовут при опытах на стенде. Само правило вынесено в `docs/architecture/testing.md` — чтобы решалось по карте, а не заново в каждом тикете.
Author
Owner

Проход на вычитание, 7 августа 2026 года. Тикет переписывался пять раз, и
каждая правка прибавляла; этот проход имел право только резать. Из тела ушло
восемь пунктов, ни одного утверждения о продукте среди них нет.

Модуль в пакете генератора, печатающий контракт машинно, — срезан. Он
попал сюда 6 августа с мотивом «нужен третьей проверке, чтобы собрать выражения
сличения из контракта». 7 августа третья проверка стала разовым опытом, а
модуль остался по инерции — постоянный код в репозитории ради работы, которая
проходит один раз. Свойство «источники у двух сторон разные» живёт в самом
действии, а не в закоммиченном файле: выражения собираются при исполнении
разовым cd generator && uv run python -c ... по schema.COLUMNS. Учить менти
модулю нечему — docs/formats/clickstream-event.md уже печатает все 47 колонок
с типами ClickHouse в порядке контракта, и собирает его schema_doc.py.

Следом ушли три пункта, стоявшие только на нём: критерий про README и uv
(без модуля утверждение README остаётся верным), строка make lint/typecheck/
test в разделе «Проверка» (тикет больше не трогает Python) и половина границы
про генератор.

Преамбула критериев ужата до двух абзацев. Довод «почему разовые опыты», что
стережёт вместо них и требование прибираться партицией 7 августа переехали в
docs/architecture/testing.md — ровно затем, чтобы решаться по карте, а не
заново в каждом тикете. В тикете осталась ссылка и та единственная фраза,
которой в карте нет: что честно теряется тихая подмена значения в одной колонке.

Обоснование отдельного коммита под переименование скрипта — срезано. Оно
опиралось на факт, которого больше нет: «этот же PR файл сильно правит, и в
одном коммите git перестанет видеть переименование». После 7 августа PR не
правит clickhouse-smoke.sh вообще. Само переименование осталось.

Пункт «нумерация проверок не меняется, ensure_stand_running не
расширяется» — срезан.
Это то же самое, что сказано в комментарии от
7 августа, и запрет на то, чего исполнитель нынешнего тикета не начнёт: в этот
скрипт он не заходит.

Мелкие: абзац про ключи в консольном продюсере (объяснён и тут же снят своей же
фразой «для этого тикета ключи не нужны»), оговорка про матвью, созданную на
несуществующую цель (рецепт опыта уже выбран), замер про пустое сообщение
до одной строки (полностью он лежит в доке хранилища), раздел «Проверка» до
одной команды (четыре пункта из шести дословно повторяли критерии приёмки).

Contract-тест: охват критерия расширен с документов на код. Мест в
документах действительно шесть, как и было сказано. Но в
generator/src/clickstream_generator/schema.py живут ещё три, и греп по
документам их не покажет: обещание «границу будет сторожить contract-тест» в
докстринге модуля, «от этой записи зависит будущий contract-тест» в пункте про
clickhouse_type и сам довод этого пункта — требование писать тип «ровно в той
записи, в какой его вернёт system.columns» держалось на снимаемой сверке.
Правило остаётся, довод у него теперь другой: по этой записи человек пишет DDL.

Три находки не пережили перепроверки и в тело не пошли.

Слияние опытов «строгий приём» и «инвариант слоёв» в один: отправка у них
действительно общая, но утверждения разные, и красное у них показывается
разными поломками. Слияние сэкономило бы абзац прозы ценой размытия двух
утверждений в одно.

Рез строки про PYTHONDONTWRITEBYTECODE: довод был «после реза модуля тикет не
трогает Python». Самый короткий способ показать красным сверку разобранного
события — поменять имя в schema.py, чтобы выражение сличения читало не тот
ключ. Это Python, и ловушка с устаревшим .pyc там ровно та.

Рез перечня мест с contract-тестом как «устаревшего»: перечень точен, устарел
был мой счёт.

Что не трогалось вовсе: обе таблицы ODS и обе матвью с их условиями,
строгий приём и три класса брака, четыре разведочных SELECT, сверка
разобранного события, опыт с упавшей матвью, четыре учебных комментария в DDL,
критерий про настоящий день.

Тело было 331 строку, стало 293.

**Проход на вычитание, 7 августа 2026 года.** Тикет переписывался пять раз, и каждая правка прибавляла; этот проход имел право только резать. Из тела ушло восемь пунктов, ни одного утверждения о продукте среди них нет. **Модуль в пакете генератора, печатающий контракт машинно, — срезан.** Он попал сюда 6 августа с мотивом «нужен третьей проверке, чтобы собрать выражения сличения из контракта». 7 августа третья проверка стала разовым опытом, а модуль остался по инерции — постоянный код в репозитории ради работы, которая проходит один раз. Свойство «источники у двух сторон разные» живёт в самом действии, а не в закоммиченном файле: выражения собираются при исполнении разовым `cd generator && uv run python -c ...` по `schema.COLUMNS`. Учить менти модулю нечему — `docs/formats/clickstream-event.md` уже печатает все 47 колонок с типами ClickHouse в порядке контракта, и собирает его `schema_doc.py`. Следом ушли три пункта, стоявшие только на нём: критерий про README и `uv` (без модуля утверждение README остаётся верным), строка `make lint`/`typecheck`/ `test` в разделе «Проверка» (тикет больше не трогает Python) и половина границы про генератор. **Преамбула критериев ужата до двух абзацев.** Довод «почему разовые опыты», что стережёт вместо них и требование прибираться партицией 7 августа переехали в `docs/architecture/testing.md` — ровно затем, чтобы решаться по карте, а не заново в каждом тикете. В тикете осталась ссылка и та единственная фраза, которой в карте нет: что честно теряется тихая подмена значения в одной колонке. **Обоснование отдельного коммита под переименование скрипта — срезано.** Оно опиралось на факт, которого больше нет: «этот же PR файл сильно правит, и в одном коммите `git` перестанет видеть переименование». После 7 августа PR не правит `clickhouse-smoke.sh` вообще. Само переименование осталось. **Пункт «нумерация проверок не меняется, `ensure_stand_running` не расширяется» — срезан.** Это то же самое, что сказано в комментарии от 7 августа, и запрет на то, чего исполнитель нынешнего тикета не начнёт: в этот скрипт он не заходит. Мелкие: абзац про ключи в консольном продюсере (объяснён и тут же снят своей же фразой «для этого тикета ключи не нужны»), оговорка про матвью, созданную на несуществующую цель (рецепт опыта уже выбран), замер про пустое сообщение до одной строки (полностью он лежит в доке хранилища), раздел «Проверка» до одной команды (четыре пункта из шести дословно повторяли критерии приёмки). **Contract-тест: охват критерия расширен с документов на код.** Мест в документах действительно шесть, как и было сказано. Но в `generator/src/clickstream_generator/schema.py` живут ещё три, и греп по документам их не покажет: обещание «границу будет сторожить contract-тест» в докстринге модуля, «от этой записи зависит будущий contract-тест» в пункте про `clickhouse_type` и сам довод этого пункта — требование писать тип «ровно в той записи, в какой его вернёт `system.columns`» держалось на снимаемой сверке. Правило остаётся, довод у него теперь другой: по этой записи человек пишет DDL. **Три находки не пережили перепроверки и в тело не пошли.** Слияние опытов «строгий приём» и «инвариант слоёв» в один: отправка у них действительно общая, но утверждения разные, и красное у них показывается разными поломками. Слияние сэкономило бы абзац прозы ценой размытия двух утверждений в одно. Рез строки про `PYTHONDONTWRITEBYTECODE`: довод был «после реза модуля тикет не трогает Python». Самый короткий способ показать красным сверку разобранного события — поменять имя в `schema.py`, чтобы выражение сличения читало не тот ключ. Это Python, и ловушка с устаревшим `.pyc` там ровно та. Рез перечня мест с contract-тестом как «устаревшего»: перечень точен, устарел был мой счёт. **Что не трогалось вовсе:** обе таблицы ODS и обе матвью с их условиями, строгий приём и три класса брака, четыре разведочных `SELECT`, сверка разобранного события, опыт с упавшей матвью, четыре учебных комментария в DDL, критерий про настоящий день. Тело было 331 строку, стало 293.
Author
Owner

Влито PR #62. Одно отклонение от буквы, чтобы оно не потерялось: критерий про карту целей велел править в docs/architecture/testing.md «только строку про переименованный скрипт», а правок вышло две — вместе с ней ушёл абзац «Ось: кого спрашивают», где договор со схемой событий стоял примером выбора дома для проверки. Без contract-теста пример перестал существовать. Постоянными проверками карта не пополнялась, как и велено.

Влито PR #62. Одно отклонение от буквы, чтобы оно не потерялось: критерий про карту целей велел править в `docs/architecture/testing.md` «только строку про переименованный скрипт», а правок вышло две — вместе с ней ушёл абзац «Ось: кого спрашивают», где договор со схемой событий стоял примером выбора дома для проверки. Без contract-теста пример перестал существовать. Постоянными проверками карта не пополнялась, как и велено.
Sign in to join this conversation.