Commit Graph
99 Commits
Author SHA1 Message Date
ddadminandClaude Opus 5 d67e697821 feat(ddl): пояс назван явно — в типах колонок и в разборе строки
- Зачем:
  - конвенция #63 записана, а код её не достиг: колонки времени стояли без
    пояса, и сходилось всё лишь потому, что пояс сервера — UTC.
- Что:
  - UTCEventTime объявлен DateTime('UTC'), служебные метки _load_ts и
    kafka_timestamp — DateTime64(3, 'UTC') в STG и ODS.
  - parseDateTimeOrNull получил третьим аргументом 'UTC': маска сверяет
    суффикс Z как букву, зоны из строки не берёт вовсе.
  - контракт схемы и описание выгрузки несут тип с поясом; имя пояса
    Europe/Samara встало рядом со смещением в world.py, сходимость сверяет
    тест.
  - учебный комментарий о линзе — у первой колонки с явным поясом.
- Проверка:
  - make lint, make typecheck, make test (408 тестов)
  - make clean && make up && make check-clickhouse — 9 из 9
  - замер тикета повторён: под session_timezone='Europe/Samara' колонка
    показана 2026-05-31 23:37:00, как и без настроек

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-08 19:30:50 +03:00
ddadmin e6f300a66e docs(storage): правки конвенции по двум холодным ревью
- Зачем:
  - раздел «Часовые пояса» прошёл два холодных ревью — по дефектам и по
    уместности. Первое поймало ложный замер и три расхождения с живым
    стендом, второе — материал не своей зоны и дубли (#63).
- Что:
  - замер «расхождение живёт по HTTP» отозван: мерил toString(UTCEventTime)
    в родном клиенте против голой колонки по HTTP, а это разные вещи.
    Перемерено — клиенты ведут себя одинаково; записан верный факт: вывод
    колонки идёт по поясу сессии, функция — по поясу типа.
  - «по поясу сервера» заменено на «по поясу сессии, а тот по умолчанию
    серверный» — в разделе и в ADR 0005; утверждение в ледгере переписано
    под измеренный раскол вывода и типа.
  - PARTITION BY toDate(_load_ts) больше не выдаётся за уже соблюдённое
    правило: _load_ts сегодня DateTime64(3) без пояса.
  - абзац ADR 0005 больше не спорит с цитатой вызова строкой выше.
  - вырезано: веер отклонённых вариантов под заголовком (живые отказы
    разложены прозой по своим абзацам, как принято в этом документе),
    ссылка на несуществующую связку в world.py, осиротевшая строка про
    Grafana, абзац про пояс показа — он уехал комментарием в #63.
- Проверка:
  - make lint
  - замеры повторены на живом стенде 8 августа 2026 года
  - DDL к конвенции по-прежнему не приведён: документы описывают цель
2026-08-08 19:05:39 +03:00
ddadmin a93a3bf3a3 docs(storage): конвенция часовых поясов принята и записана
- Зачем:
  - пояс в стенде нигде не назван: числа верны только потому, что сервер
    ClickHouse стоит в UTC, а правило понадобится в dds и витринах —
    воронки, удержание, «покупки по дням» (#63).
- Что:
  - раздел «Часовые пояса»: пояс — линза, называется в типе колонки либо в
    вызове; какая именно — решает слой (ODS на языке выгрузки, DDS и витрины
    на языке бизнеса); день берётся из EventDate.
  - названы оба перехода, где пояс выбирается, включая разбор строки в
    матвью — он берёт пояс у сервера и тип колонки этого не чинит.
  - отвергнутые варианты прозой: умолчание сервера, TZ серверу, ODS в поясе
    счётчика, хранение местного времени.
  - три замера ушли в «Что проверено», сверка с документацией — от 8 августа.
- Проверка:
  - make lint
  - DDL к конвенции ещё не приведён: документ описывает цель, код идёт
    следом тем же тикетом.
2026-08-08 18:36:29 +03:00
ddmitry c382b0390f Merge pull request 'docs: приёмка этапа 2 и порог, который оказался гигиеной' (#67) from docs/priyomka-etapa-2 into main
Reviewed-on: #67
2026-08-07 20:56:34 +03:00
ddadmin 2951359e6c docs(spec): порог приёмки — гигиена, и живёт он в карте проверок
Зачем: формулировка в спеке дублировала строку карты проверок и делала это
хуже оригинала. «make up работает» следует из зелёных проверок и потому
пусто; настоящее условие — «с нуля» — в спеке не проговаривалось. Заодно
гигиеническая планка носила имя приёмки этапа, хотя про предмет этапа она
молчит: дашборд может быть не нарисован, а все три цели зелены. Тот же промах
разобран в ADR 0004 — там он случился с числовым порогом.

Что: раздел 9 спеки отсылает к карте проверок вместо своей формулировки и
называет вещь своим именем — гигиена, а не приёмка. Копия строки убрана из
семи карт этапов и карты #1 в трекере: одна вещь — одно место.

Проверка: правка документная. grep по docs, README и AGENTS — других копий
формулировки нет.
2026-08-07 20:26:29 +03:00
ddadmin 6f5f76fafb docs(stage-2): приёмка этапа 2 — снятые замеры и решение по дублям Kafka
Зачем: этап 2 принят прогоном с нуля, и два его следа должны остаться в
доках — иначе решение «не проверяем» станет забытым долгом, а цена цели
разойдётся с замером.

Что:
- в спеке v2 раздел 11 больше не держит Kafka Engine на двух нодах: дубли и
  раскладку партиций между прогонами решено не проверять — дубль возможен по
  устройству движка, окно разобрано в доке хранилища, в ODS его схлопывает
  ReplacingMergeTree. Половина, которую показал #37, названа;
- в карте проверок цена check-services 44 с -> 59 с и абзац с замерами
  приёмки: clean+up 2 м 58 с, смоук 9 с, check-clickhouse 8 с, test 71 с.
  Причина подорожания не выдумывается — названа неизвестной.

Проверка: make clean && make up, затем make smoke, make check-clickhouse,
make check-services, make lint, make typecheck, make test — всё зелёное
7 августа 2026 года.
2026-08-07 20:14:35 +03:00
ddmitry 600bd74b04 Merge pull request 'feat(stand): стартовый мир при make up и опись, которая его сторожит' (#64) from feat/42-world-manifest into main
Reviewed-on: #64
2026-08-07 19:34:14 +03:00
ddadmin c696fce40b fix(stand): находки ревью — диагноз не утверждает причину, комментарии не врут
Зачем
Холодное ревью нашло три места, где написанное сильнее сделанного.

Что
- Непустая таблица брака больше не выдаётся за доказательство сломанного
  разбора. Модельного дня у брака нет, обрамить его нечем, и строки прежних
  уроков лежат в нём месяц: после первого же урока с мусором проверка
  давала бы неверный диагноз навсегда. Теперь она даёт признак, по которому
  причину отличают, — сошлась недостача с числом брака или нет.
- Комментарий у world-init обещал, что расхождение числа дней с
  STARTING_DAYS поймают счётчики. Это неправда в одну сторону: лишний день
  ложится за рамкой дат описи. Обещание убрано, дыра названа.
- Довод «даг next_day этапа 5 продолжит ось» опирался на несуществующий
  этап; заменён настоящей причиной — заливка замыкает цепь разовых служб.
- Потолок ожидания 300 с получил обоснование замером с кратностью, а сам
  скрипт — честную оговорку: его обещание работает на пустом стенде, на
  живом ждать нечего.
- Третья, пропущенная ссылка на снятый порог скорости дня убрана из спеки.
- Даты замеров в карте целей разведены: #42 менял три цели, а не шесть.

Проверка
Обе ветви диагноза сняты заново на живом стенде: без брака — «не доехали»,
с браком — признак различения. Стенд восстановлен, все 9 проверок зелёные,
брака 0. make lint, typecheck, config-test, test (407 тестов) зелёные.

Ссылка: #42
2026-08-07 18:47:13 +03:00
ddadmin 7c9eeedc40 feat(stand): make up наполняет стенд стартовым миром, опись сторожит его
Зачем
Стенд поднимался пустым, и всякая приёмка следующих этапов начиналась с
ручной заливки данных. Теперь `make up` сам приводит стенд к одному и тому
же состоянию, а в git лежит то, чем это состояние проверяется.

Что
- Опись мира `data/world-inventory.json`: паспорт (зерно, версия
  генератора, хеш каталога) и по строке на каждый из восьми дней — дата,
  число событий, хеш байтов. Собирается `make inventory`, свежесть сторожит
  `test_inventory.py` — тем же способом, что свежесть описания выгрузки.
- Разовая служба `world-init` вышла из-под профиля и играет в топик восемь
  дней при каждом подъёме; зависимый у неё — `airflow-init`, иначе `--wait`
  считает успешно отработавшую службу упавшей.
- `scripts/wait-for-world.sh` — вторая половина `make up`: приём
  асинхронный, поэтому ждать надо доезда до `ods.event`, а не завершения
  заливки. Ограниченный цикл опроса, не пауза наугад.
- Девятая проверка `make check-clickhouse`: подневный счёт событий против
  описи, рамка по датам стартового мира, счёт через `FINAL`. При
  расхождении называет, где искать, — в событиях или в браке.
- Порог «день ≤ 30 с» снят из спеки генератора в обоих местах: замер дал
  1,7 с, порог был выше факта в восемнадцать раз. На его месте — замеры с
  датой. Раздел 9 спеки закрыт: открытых вопросов не осталось.
- Слова: «манифест» стал описью мира, «зерновой мир» — стартовым миром
  (решение владельца). Оба заведены в словарь CONTEXT.md.

Проверка
`make clean && make up` с нуля — 2 м 50 с, доехало ровно 401 185 событий.
`make check-clickhouse` зелёный (8 с), `make smoke` зелёный (9 с),
`make test` — 407 тестов за 71 с, `make lint`, `make typecheck`,
`make config-test` зелёные.

Что проверка умеет краснеть, снято двумя поломками: снос партиции
2026-06-03 дал диагноз «не доехали до ODS», негодная строка в сырье —
«сломан разбор». Строки опыта убраны, день переигран, счёт вернулся.
Тест свежести проверен молчаливой правкой цены в каталоге: покраснел.

Ссылка: #42
2026-08-07 18:37:01 +03:00
ddadminandClaude Opus 5 30a1e9e567 docs(testing): корректность процессов — в даги DQ, не в цели make
- Зачем:
  - находка «это стоило бы проверять регулярно» решалась заново в каждом
    тикете и каждый раз тянулась в make check-clickhouse. У неё есть
    назначенный дом: даги качества данных, которые придут со следующими
    уровнями хранилища.
- Что:
  - добавлен раздел «Корректность процессов живёт в дагах DQ, а не в целях
    make»: цели make отвечают «стенд собран», свойства данных — работа дага.
  - назван фильтр: про полноту дня, свежесть слоя, сходимость витрины с
    источником — это даг, а не цель.
  - названа учебная сторона: даг идёт по расписанию, пишет историю проверок
    и разбирается как обычная задача Airflow — так качество данных устроено
    в бою.
- Проверка:
  - make config-test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 17:39:49 +03:00
ddmitry f3ebc115ba Merge pull request 'feat(ods): типизированное событие, строгий приём и таблица ошибок' (#62) from feat/43-ods-event into main
Reviewed-on: #62
2026-08-07 16:50:44 +03:00
ddadminandClaude Opus 5 a534f3cc94 fix(ods): метка времени разбирается по названному формату, а не best-effort
Зачем: parseDateTimeBestEffort на непонятной строке не краснеет, а достраивает
недостающее — обрезанное «20:00:21» становится первым января текущего года.
Такое сообщение проходило строгий приём с тихо неверным временем, то есть с
той самой порчей, ради которой класс key_field_unparsed и заведён.

Что:
- В обеих матвью разбор метки идёт parseDateTimeOrNull по формату
  '%Y-%m-%dT%H:%i:%SZ'. Форма на проводе одна и каноническая, поэтому широта
  best-effort не нужна вовсе, а платится за неё отключённой проверкой.
- Замеры в ADR 0005: три записи, которые best-effort достраивает; проверка,
  что настройка cast_string_to_date_time_mode не спасает JSONExtract; сверка
  на настоящих данных — по всем 101 252 строкам сырья модельного дня точный
  формат разобрал метку у каждой и ни на одной не разошёлся с best-effort.
- Записано наблюдение стенда: пересозданная на живом чтеце матвью пропускает
  ближайшее сообщение мимо ODS, через минуту то же сообщение разбирается.
  Воспроизведено дважды; на нём я сам споткнулся при проверке этой правки.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:48:37 +03:00
ddadminandClaude Opus 5 68f789ba91 docs(ods): находки ревью — опыт с _load_ts, точность формулировок, рез повторов
Зачем: холодное ревью по двум линиям нашло дыру в следе опытов и три места,
где текст утверждает не то, что построено.

Что:
- Опыт «_load_ts переносится из сырья» прогнан и записан: у двух тысяч
  событий метка совпала с меткой одной из доставок, случаев «метки нет среди
  доставок» ноль. Туда же — ответ про форму ключа ODS: вопрос раздела 11
  спеки закрывался молча.
- Дока хранилища говорила, что предикат собран из функций, не возвращающих
  NULL; построено иначе — обнуляемый разбор есть, но кончается IS NOT NULL.
- Записана гарантия на JSONType: на не-JSON и пустой строке она отдаёт Null и
  не бросает, то есть годится в предикат. Раньше первый класс брака стоял на
  замере соседней функции.
- ttl_only_drop_parts у таблицы ошибок назван в доке хранилища.
- Комментарий матвью ужат: три вопроса строгого приёма пересказывали ADR 0005
  целиком. Осталось то, чего по коду не видно, — запрет трогать arraySort и
  замер про ISO-8601. Убрано неверное «в полусотне строк» и упоминание имени
  таблицы хранилища в докстринге контракта генератора.

Проверка: DDL применяется на живом кластере; make lint, typecheck, docs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:16:09 +03:00
ddadminandClaude Opus 5 6910440400 feat(ods): типизированное событие, строгий приём и таблица ошибок
Зачем: цепочка Kafka → STG → ODS достраивается последним этажом. Сырьё уже
доезжает (#37), настоящие события в топике есть (#41), а типизированного слоя
не было — событие негде было прочитать колонками, а брак негде увидеть.

Что:
- sql/ddl/20-ods-tables.sql — ods.event_rep/_dist на ReplacingMergeTree с
  версией _load_ts, партиция по EventDate, ключ по разделу 1.3 спеки,
  шардирование cityHash64(ClientID); ods.event_errors_rep/_dist с классом
  брака, своими ключами и сроком жизни в месяц.
- sql/ddl/30-ods-views.sql — две матвью над stg.hits_raw_dist. Годность
  считает предикат из трёх частей, вторая матвью берёт его дословное
  отрицание, класс брака пишется первым совпавшим из трёх.
- Метку времени разбирает parseDateTimeBestEffortOrNull, а не JSONExtract:
  ISO-8601 с суффиксом Z JSONExtract не берёт вовсе. Спека генератора
  обещала обратное — обещание поправлено, форма на проводе не менялась.
- Сверка объявлений (contract-тест) снята из документов и из докстрингов
  schema.py: сверх строгого приёма она ловила только смену типа.
- Документация приведена в соответствие: ADR 0005, дока хранилища и обе
  спеки; группа «сказано по памяти» в доке хранилища опустела.

Проверка: make up && make check-clickhouse (8 проверок, 7,5 с); make lint,
make typecheck, make test (406), make docs без диффа. Разовые опыты при
исполнении — в теле PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:06:46 +03:00
ddadminandClaude Opus 5 d7485217d8 refactor(scripts): clickhouse-smoke.sh переименован в check-clickhouse.sh
Зачем: имя файла осталось от прежней цели smoke-cluster, которой больше нет,
и читатель ищет проверку ClickHouse не там, где она лежит.

Что: git mv scripts/clickhouse-smoke.sh scripts/check-clickhouse.sh, тем же
коммитом — вызов в Makefile и строка в карте целей. Абзац-объяснение в
docs/architecture/testing.md снят: он обещал переименование, которое здесь и
случилось.

Проверка: make check-clickhouse.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 15:33:44 +03:00
ddmitry 77fa2905a2 Merge pull request 'feat(generator): сериализатор, приёмники, проигрыватель и запуск контейнером' (#61) from feat/41-serializer-sinks-cli into main
Reviewed-on: #61
2026-08-07 14:37:22 +03:00
ddadminandClaude Opus 5 a61f7934ec feat(generator): сериализатор, приёмники, проигрыватель и запуск контейнером
- Зачем:
  - до сих пор генератор умел собирать день, но не умел его отдать: топик
    hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
    события до стенда и закрывает форму на проводе, на которую обопрётся
    типизированный ODS (#43).
  - сериализатор один по решению спеки: второе место, печатающее событие в
    JSON, разошлось бы с первым молча.
- Что:
  - serialize.py — канонический сериализатор на orjson: единственное место,
    где событие целиком становится JSON; 47 ключей всегда, «пусто» это
    пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
    ecommerce в commerce.py вторым сериализатором не считается — правило
    про событие, а не про блок внутри него.
  - sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
    событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
    ключом он был бы ключом лишь на вид.
  - player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
    live (темп ×60), несколько дней одним запуском, ограниченная пачка,
    раздельные тайминги генерации и доставки, лаг в логе.
  - день на оси и имя топика умолчаний не имеют: параметр, описывающий
    среду или позицию, приходит от зовущего, иначе отказ до генерации.
    Умолчания зерна, числа дней и темпа остаются — они описывают мир.
  - generator/Dockerfile — свой образ: зависимости из uv.lock, база
    закреплена до патча, раскладка репозитория сохранена ради каталога
    товаров. Образ Airflow не тронут.
  - разовая служба compose под профилем, цели generate-batch и
    generate-live, .dockerignore, tmp/ в .gitignore.
  - решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
  - make test 406 passed, make lint, make typecheck, make config-test.
  - побайтовый детерминизм: два прогона дня в независимых процессах дают
    один sha256; день в контейнере совпадает с днём на машине.
  - на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
    Distributed сошёлся — отправлено 50626, в таблице 50626.
  - топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
    clickhouse-02 раздел 1 (24258).
  - живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
    темп ×60, лаг печатается.
  - форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
    строкой.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:05:11 +03:00
ddadminandClaude Opus 5 8903b6a054 docs(agents): усложнение не бесплатно — платит учебная ценность
- Зачем:
  - у вопроса «чему на этом научится менти?» не хватало второй половины:
    он отсеивал бесполезное, но не взвешивал цену полезного. Стенд
    учебный, и чем он сложнее, тем хуже как учебный материал.
  - прогон #41 показал механизм: две слепые линии ревью дали 15 находок,
    саморевью ещё 7, и каждая по устройству триажа превратилась в правку.
    Шага, на котором кто-нибудь вычитает, в конвейере не было ни разу —
    только воронка. Проход на вычитание потом срезал 149 строк и 6 тестов,
    и всё срезанное появилось после ревью, а не при замысле.
- Что:
  - в раздел «Цель репозитория» добавлены три абзаца: внимание менти как
    конечная валюта и вопрос «что он платит и что получает»; почему
    оборонительный код дороже прочего и врёт про опасность; проход на
    вычитание как обязательный шаг перед приёмкой заметной работы.
  - записано частное правило, выведенное владельцем на двух резах подряд:
    не сторожить ошибку, которую человек делает сам себе и тут же видит —
    разбирательство с ней и есть урок.
- Проверка:
  - правка текстовая, целей make не задевает; make config-test зелёный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 12:04:43 +03:00
ddadminandClaude Opus 5 b8816466d9 docs(testing): интеграционная проверка не живёт постоянной целью
- Зачем:
  - проверка, которая кормит стенд данными, оставляет их в мире менти
    навсегда: у ODS срока хранения нет, и события, которых мир не рождал,
    неотличимы в лабах от настоящих. Решение принято при разборе постановок
    #41 и #43, чтобы оно решалось по карте, а не заново в каждом тикете.
- Что:
  - добавлен раздел «Интеграционная проверка постоянной целью не становится»:
    такой прогон делается один раз при исполнении, след — запись в теле PR.
  - названо требование прибираться за собой и дешёвый способ это сделать:
    модельный день за границей оси мира и снос его партиции.
  - названо, что стережёт цепочку постоянно вместо неё — проверки на
    настоящих данных, которые стенд произвёл сам.
- Проверка:
  - make config-test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 09:22:56 +03:00
ddmitry 896c846cbc Merge pull request 'refactor(config-test): срезан разбор Bash — он не ловил ничего' (#59) from refactor/config-test-bez-razbora-bash into main
Reviewed-on: #59
2026-08-06 17:23:58 +03:00
ddadminandClaude Opus 5 5492e6ff3d refactor(config-test): срезан разбор Bash — он не ловил ничего
- Зачем:
  - проверка была сломана с рождения цели: bash -n со списком файлов
    разбирает только первый, остальные уходят ему в аргументы. Из пяти
    скриптов проверялся один, и за всё время этого никто не заметил.
  - чинить незачем: скрипты стенда запускают с той же машины, и
    синтаксическая ошибка вылезает при первом же запуске с номером строки.
    Учебной ценности в проверке нет — из неё не узнаёшь ничего, кроме того,
    что у bash есть ключ -n.
  - держалась она не строчкой, а двенадцатью: обход репозитория, временный
    файл со списком, mapfile и две ветки на пустой список.
- Что:
  - из scripts/config-test.sh убраны разбор Bash и весь аппарат сбора
    списка файлов; 53 строки стали 40.
  - разбор файлов DAG остался и получил комментарий с основанием: их на
    машине не запускает никто, обработчик разбирает их внутри контейнера, и
    ошибка всплывает не сообщением, а молча пропавшим DAG.
  - README и карта проверок больше не обещают проверку синтаксиса Bash.
  - в карте записано, почему проверку не стоит заводить заново.
- Проверка:
  - make config-test зелёный.
  - оставшийся разбор DAG краснеет: незакрытая скобка в dags/test_kafka.py
    роняет цель с SyntaxError и ненулевым кодом; файл восстановлен.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 16:59:44 +03:00
ddmitry 4245daf3f1 Merge pull request 'refactor(smoke): цели проверки по назначению — смоук, ClickHouse, службы' (#58) from refactor/54-proverki-po-naznacheniyu into main
Reviewed-on: #58
2026-08-06 16:04:38 +03:00
ddadminandClaude Opus 5 7e4d5815a3 feat(smoke): смоук спрашивает Kafka снаружи — по объявленному адресу
- Зачем:
  - деление целей оставило дыру: про Airflow, Superset, Prometheus и Grafana
    смоук стучится с машины в отображённый порт, а про Kafka после переезда
    check_kafka_from_host знал только «контейнер здоров».
  - вердикт этот приходит из healthcheck в compose.yaml, а тот спрашивает
    брокер изнутри и по внутреннему слушателю: объявленный наружу адрес может
    вести не туда, и Kafka всё равно останется здоровой.
  - поломка популярная и показательная: клиент подключается, получает
    метаданные и молча виснет на адресе, которого с его стороны нет. Менти
    узнаёт, что у брокера два слушателя и зачем нужен advertised.listeners.
    Генератор будет писать в Kafka именно с машины.
- Что:
  - check_kafka_external_listener в make smoke: запрос списка топиков с машины
    через отображённый порт, ответ приходит только если объявленный адрес ведёт
    туда же. Комментарий у проверки объясняет, от чего она заведена.
  - ожидание ответа ограничено 15 секундами при замеренных 2,6 — впятеро
    больше, чем стоит зелёный прогон.
  - README и карта проверок: новая проверка названа, доводы записаны, цена
    смоука обновлена с 6 до 8 секунд.
- Проверка:
  - make config-test, make smoke (20 проверок, 8 с), make check-clickhouse,
    make check-services — зелёные.
  - краснеет на своей поломке: брокеру объявлен адрес kafka-nowhere:29092 при
    целом внутреннем слушателе — проверка состояния контейнера осталась
    зелёной, смоук покраснел именно на этой строке. После проверки Kafka
    возвращена в исходное состояние, посторонних контейнеров не осталось.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 15:20:27 +03:00
ddadminandClaude Opus 5 bbbe17cfb0 refactor(smoke): цели проверки по назначению — смоук, ClickHouse, службы
- Зачем:
  - смоук перестал быть быстрым: 42 секунды из 48 съедали шесть проверок,
    которые ждут службу — запуск DAG, вход в Superset, Kafka с машины.
  - имена целей врали: смоуком звались и глубокая проверка кластера, и
    интеграционные проверки; префикс достался им от общего происхождения.
  - нигде не было записано, зачем в репозитории каждая цель и куда класть
    новую проверку, — без записи скрипт дорастёт снова.
- Что:
  - ось деления — кого спрашивают, а не сколько стоит: make smoke (стенд
    собран), make check-clickhouse (спрашивают у ClickHouse), новая
    make check-services (службы работают).
  - шесть тяжёлых проверок переехали в scripts/stand-services.sh; общее —
    счёт, обращение к Compose, зависимости машины и check_containers_survived
    — вынесено в scripts/stand-common.sh, копипасты нет.
  - smoke-cluster переименована в check-clickhouse; имя файла скрипта не
    тронуто (в него встраивается проверка договора со схемой), расхождение
    названо в карте.
  - смоук и check-services печатают своё время в строке ИТОГ; порога по
    времени нет — по доводу ADR 0004.
  - docs/architecture/testing.md: карта всех семи целей, правило быстрого
    смоука словами, лесенка по частоте и правило про краснеющую проверку,
    переехавшее из README; указатель из AGENTS.md.
  - README: описания целей сокращены, карта не дублируется; быстрый старт
    показывает работающий стенд, а не только собранный.
  - планка приёмки этапа в спеке названа поимённо: три цели вместо
    «smoke-проверки».
- Проверка:
  - make config-test, make smoke (19 проверок, 6 с), make check-clickhouse
    (8 проверок, 7 с), make check-services (7 проверок, 44 с) — зелёные.
  - 19 + 7 = 25 разных проверок, как и до деления: check_containers_survived
    считается дважды намеренно.
  - краснеют обе разделённые цели: со снятым prometheus смоук дал три ошибки,
    с подменённым UUID подключения Superset покраснел check-services.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 14:34:02 +03:00
ddmitry f84f8c416f Merge pull request 'refactor(smoke): срезаны проверки стенда, не служащие менти' (#57) from refactor/56-srezat-proverki-stenda into main
Reviewed-on: #57
2026-08-06 14:10:44 +03:00
ddadmin a2f5b27d89 refactor(smoke): ограничен по времени опрос keeper, уточнены формулировки
- Зачем:
  - горячее ревью: перевезённая проверка keeper потеряла привычку файла
    ограничивать обращения к контейнерам по времени и глушить их ошибки.
- Что:
  - опрос keeper идёт через keeper_exec с timeout 20s и тихим stderr,
    в отчёте об отказе пустой ответ назван словами.
  - комментарий к проверке и абзац README переписаны на проверяемое
    утверждение: настройки объявлены в compose.yaml, смоук спрашивает,
    дошли ли они до процесса.
  - в пробнике ClickHouse ожидаемой строке возвращено имя expected_rows.
- Проверка:
  - make config-test, make smoke, make smoke-cluster; отдельно проверено,
    что на паузе keeper проверка краснеет, а не виснет.
2026-08-06 14:01:45 +03:00
ddadmin 66490d124e refactor(smoke): срезаны проверки стенда, не служащие менти
- Зачем:
  - проверок стало больше, чем продукта, и росли они из критериев приёмки,
    а не из учебной ценности (#56).
- Что:
  - удалены tests/dag-probes-unit.py, tests/stand-smoke-guards.sh,
    tests/stand-smoke-static.sh и tests/smoke-guards.sh вместе с целью
    make smoke-guards и запуском юнит-тестов в scripts/config-test.sh.
  - из scripts/stand-smoke.sh убран check_env_consistency, туда же переехал
    check_keeper_runtime; счёт проверок остался 25.
  - в пробниках свёрнуты функции _assert_*, комментарий про отложенный импорт
    переписан на причину из документации Airflow и продублирован в test_kafka.
- Проверка:
  - make config-test, make up, make smoke, make smoke-cluster.
2026-08-06 13:06:44 +03:00
ddadminandClaude Opus 5 6afa26b7be docs(agents): вопрос об учебной ценности как шлагбаум
Зачем: учебная ценность в AGENTS.md заявлена дважды, но обе строки —
утверждения о ценности, а не действие в момент письма. При них проверок
в репозитории выросло больше, чем продукта.

Что: в раздел «Цель репозитория» добавлен проверяемый вопрос к любой
доработке — чему на ней научится менти — с ветвью: не складывается ответ,
значит это вопрос владельцу, а не строчка кода.

Проверка: не требуется, правка документа.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 12:37:06 +03:00
ddmitry 18200fb97f Merge pull request 'docs(agents): правка тела issue идёт через API, а не через tea' (#55) from docs/issue-tracker-api-recipes into main
Reviewed-on: #55
2026-08-06 09:28:04 +03:00
ddadminandClaude Opus 5 dd6d117d49 docs(agents): правка тела issue идёт через API, а не через tea
Зачем: тикнуть чекбокс в критериях приёмки нужно при закрытии каждой задачи,
а рецепта в доке не было. Наступили на это при закрытии #37: `tea issues edit
--description` требует тело целиком строкой, но взять её неоткуда — вывод
`tea issues <номер>` обёрнут и разрисован для терминала.

Что: раздел «Правка тела issue — только через API» с рабочим рецептом
(забрать сырой JSON, поправить body, вернуть через `-X PATCH -d @файл`) и
разбором флагов `tea api`. Оттуда же общее правило: CLI удобен, пока команда
создаёт объект или меняет его свойство, и мешает, как только надо изменить
уже написанный текст. Ссылка из раздела про автозакрытие и запись в «Что
проверено и когда».

Проверка: рецепт снят живыми запросами при закрытии #37 — так проставлены
чекбоксы тикета и пункт в чек-листе карты #4. `make config-test` зелёный.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 09:24:53 +03:00
ddmitry c99e853fd3 Merge pull request 'feat(stg): DDL-бутстрап, топик hits и приём сырья обеими нодами' (#53) from feat/37-ddl-bootstrap-stg-ingest into main
Reviewed-on: #53
2026-08-06 09:21:18 +03:00
ddadminandClaude Opus 5 daf13384a8 feat(stg): DDL-бутстрап, топик hits и приём сырья обеими нодами
Зачем: стенду нужен воспроизводимый холодный старт, при котором схема
хранилища и топик появляются сами, а сырьё из Kafka доезжает в STG обеими
нодами кластера — без ручных шагов между `make clean` и рабочим приёмом.

Что:
- `sql/ddl/` — три файла, применяются по порядку имён: базы `stg` и `ods`,
  Kafka-чтец `hits_raw_kafka` формата RawBLOB, реплицируемая `hits_raw_rep`
  с окном TTL в трое суток, распределённая `hits_raw_dist` и матвью
  `hits_raw_mv`, переносящая сырьё вместе с метаданными доставки.
- `compose.yaml` — службы `kafka-init` (топик `hits` на две партиции, с
  ремонтом уже созданного однопартиционного) и `clickhouse-init` (применяет
  `/ddl/*.sql`); `hostname:` у обеих нод, чтобы `hostName()` отдавал имя узла,
  а не идентификатор контейнера; `airflow-init` зависит от `clickhouse-init` —
  без зависимого успешный одноразовый сервис считается упавшим для `--wait`.
- Доки: конвенции и раздел «Что проверено» в справочнике хранилища, указатели
  и границы обещаний в ADR 0005, снятые пункты в разделе 11 спеки.

Проверка: `make lint`, `make typecheck`, `make config-test`, `make smoke`
(25 проверок), `make smoke-guards` — зелёные. Приёмочный прогон с чистого
тома подтвердил все пять критериев #37: холодный старт и идемпотентный
повтор, две партиции у `hits`, метаданные доставки у доехавшего сообщения,
обе партиции на обеих потребляющих нодах в одном прогоне, некорректный JSON
лежит сырым и приём не встаёт.

Известная граница: RawBLOB молча теряет запись с пустым значением и
запись-надгробие; принято как свойство, замер и довод — в справочнике
хранилища.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 08:04:25 +03:00
ddmitry a0144e0fff Merge pull request 'docs(storage): конвенции хранилища и приём событий перед этапом 2' (#52) from docs/37-storage-conventions into main
Reviewed-on: #52
2026-08-05 22:09:39 +03:00
ddadminandClaude Opus 5 923ebad80e docs(storage): связность восстановлена, форма дат на проводе задана
- Зачем:
  - холодное ревью связности нашло девять мест, где вставленный текст спорит с
    соседним; отдельно вскрылось, что представление дат в JSON не зафиксировано
    нигде, а #43 обязан его знать раньше, чем #41 напишет сериализатор.
- Что:
  - гарантия приёма переписана: после снятия синхронной вставки «хотя бы один
    раз» стало неправдой — есть и окно потери, и окно дубля.
  - критерий выбора пяти опорных колонок приведён к списку, который он
    порождает; `CounterID` оговорён отдельно.
  - «переобработки у ODS нет вовсе» смягчено до пакетной: ручная вставка из
    сырья в пределах окна возможна.
  - в спеку генератора добавлена форма дат на проводе — ISO-8601, с доводом от
    читаемости слоя сырья.
  - убраны осиротевшая фраза про порядок сервисов, дубль порядка классов брака,
    устаревшая датировка сверки и ещё три следа вставок.
- Проверка:
  - make config-test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:53:03 +03:00
ddadminandClaude Opus 5 f1254ce79d docs(handoff): передача контекста удалена как отработавшая
- Зачем:
  - список «чего не переоткрывать» устарел за день: снятая настройка
    синхронной вставки и прежняя раскладка DDL остались в нём как принятые
    решения, а документ, утверждающий решённым переехавшее, — ловушка.
- Что:
  - удалён .scratch/handoffs/2026-08-04-storage-conventions.md; содержание
    живёт в ADR 0005 и 0006, доке хранилища и тикетах #37 и #43.
- Проверка:
  - make config-test

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:31:32 +03:00
ddadminandClaude Opus 5 31b274175a docs(storage): конвенции и приём событий выправлены после ревью
- Зачем:
  - три холодных ревью и сверка с документацией ClickHouse нашли противоречия
    между докой, ADR и спекой: исполнитель #37 получал два разных ответа на
    один вопрос, а два утверждения о движке оказались неверными.
- Что:
  - раскладка файлов DDL перестроена — сначала таблицы, матвью приёма
    последней: иначе часть событий тихо минует ODS.
  - синхронная вставка снята с пути приёма: настройка недостижима для потока
    Kafka-движка и связывает шарды; на ETL-вставках осталась.
  - у таблицы ошибок появился класс брака с порядком проверки, у сырья и
    ошибок названы движки и ключи сортировки.
  - в доку добавлен раздел «Что проверено»: сверенное с документацией,
    проверяемое на стенде и сказанное по памяти разведены.
  - в спеке выправлены источник матвью разбора, пять опорных колонок, имена
    четырёх витрин и ссылка на несуществующую цель make.
- Проверка:
  - make config-test
  - grep по устаревшим именам файлов DDL и витрин — пусто

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 21:25:35 +03:00
ddadmin dedb6c6739 docs(handoff): передача контекста по конвенциям хранилища
- Зачем:
  - работа над #37 продолжится в новой сессии, а решения сессии разбросаны по
    двум ADR, доке хранилища, двум спекам и двум тикетам; свежему агенту нужен
    один вход с указателями и списком того, что переоткрывать не надо.
- Что:
  - добавлен .scratch/handoffs/2026-08-04-storage-conventions.md: указатели на
    артефакты, перечень принятых развилок, открытые хвосты и особенности
    работы с трекером.
- Проверка:
  - make config-test
2026-08-04 00:21:43 +03:00
ddadmin 319db308bf docs(storage): приняты решения по приёму событий и именам
- Зачем:
  - тикет #37 молча опирался на конвенции хранилища, которых в проекте не
    было; без них #43 и следующие этапы разъехались бы в именах, служебных
    колонках и механике приёма.
- Что:
  - ADR 0005: топик читается байтами в STG, разбор идёт функциями в матвью
    ODS; строгий приём — сверка набора ключей плюс Nullable на пяти опорных
    колонках.
  - ADR 0006: суффикс вида в именах объектов (_rep, _dist, _kafka, _mv, _v).
  - docs/architecture/storage.md: конвенции имён и служебных колонок, путь в
    keeper, раскладка по шардам, срок жизни сырья, свойства приёма, раскладка
    файлов DDL и карта таблиц.
  - спеки приведены в соответствие: механизм строгого приёма, имена объектов,
    контракт транспорта «одно событие — одно сообщение Kafka», три проверки
    при исполнении.
- Проверка:
  - make config-test
2026-08-04 00:14:13 +03:00
ddmitry a18e6b7a80 Merge pull request 'Торговые события генератора: корзина, покупка и уровень спроса товара' (#51) from feat/40-trade-events into main
Reviewed-on: #51
2026-08-02 22:59:26 +03:00
ddadmin 4243aa2c88 docs(generator): вилка конверсии в корзину — ориентир, а не граница
- Зачем:
  - после #50 доля просмотров, доходящих до корзины, подошла к краю вилки
    5–9%, и следующий исполнитель стал бы калибровать поведение под это
    число. Владелец решил: пока доля правдоподобна, она ничего не сторожит —
    у живых магазинов она гуляет широко.
- Что:
  - в блок «Решено при исполнении #50» добавлено решение владельца: вилка —
    ориентир; настоящий предел здесь дневной бюджет событий, на котором
    стоят манифест (#42) и порог скорости дня.
- Проверка:
  - git show --stat; следующий исполнитель читает решение, не переспрашивая.
2026-08-02 22:54:31 +03:00
ddadmin c0f4d21df9 feat(generator): корзина отвязана от страницы, у товара — уровень спроса
- Зачем:
  - приёмка #40 нашла в торговых данных два точных равенства, каких в живом
    магазине не бывает: событие корзины случалось ровно у визитов со
    страницей /cart, а внутри такого визита в корзину уходили все открытые
    карточки. Привлекательность товара было нечем измерить, а аналитик читал
    бы эти равенства как склейку в разметке.
- Что:
  - класть в корзину может любой визит, открывший карточку; страница /cart
    осталась шагом воронки, а у визита, дошедшего до неё, корзина непуста.
  - у товара появился уровень спроса — колонка каталога с тремя значениями
    и два ряда вероятностей в числах мира: намерение визита берётся из шага
    воронки, а не из метки покупателя, которая уже действует через неё.
  - уровни рассыпаны по каталогу одной колодой: одинакового расклада по
    категориям нет, связи с ценой нет, и то и другое сторожится тестами.
  - числа мира перемерены: конверсия карточки в корзину 8,62% (по уровням
    13,1 / 9,2 / 5,8), кладут без открытия корзины 41,4% таких визитов,
    средний день 49 834 события, конверсия визита в покупку 2,43%.
  - спека генератора и мастер-спека приведены к новой форме каталога.
- Проверка:
  - make lint && make typecheck && make test — 394 passed (было 383).
  - трафиковая половина побайтово та же: sha256 по (URL, времени, WatchID)
    всех просмотров за 14 канонических дней совпадает со снимком до правки.
2026-08-02 22:46:30 +03:00
ddadmin 4165f10cdd docs(generator): числа блока грилинга — с канонического зерна
- Зачем:
  - блок «Решено при исполнении #40 после приёмки» цитировал замер на зерне 0,
    а канонический мир, на котором стоят все тесты, — CANONICAL_SEED
    (20260601); числа относились к другому миру.
- Что:
  - замер перемерен на каноническом зерне за 14 дней: тождество «событие
    корзины ↔ страница /cart» держится каждый день, на дне 2 это 828 и 828.
  - уточнены средний день (49 509 событий), конверсия визита (2,43%), состав
    заказа (1,87 позиции) и доля брошенных позиций (число мира 15%, замер по
    покупающим корзинам 12%).
- Проверка:
  - скрипт замера на 14 днях CANONICAL_SEED; git show --stat.
2026-08-02 21:21:43 +03:00
ddadmin fbffeda5ea docs(generator): решения грилинга — корзина и уровень спроса товара
- Зачем:
  - приёмка #40 нашла в торговых данных два точных равенства, каких в живом
    магазине не бывает: событие корзины возникало ровно у визитов со
    страницей /cart, а внутри такого визита в корзину ложились все открытые
    карточки.
- Что:
  - в спеку генератора, раздел 9, добавлен блок «Решено при исполнении #40
    после приёмки»: корзина отвязана от страницы корзины, уровень спроса
    становится свойством товара, границы правки — вилки раздела 5.
  - записаны отклонённые варианты: свой процент у каждого sku и вывод
    склонности из цены.
  - в CONTEXT.md заведён термин «уровень спроса».
- Проверка:
  - git show --stat; текст решений читается без обращения к переписке.
2026-08-02 21:06:51 +03:00
ddadminandClaude Opus 5 722dbe22b7 feat(generator): торговые события — корзина, покупка, сырой ecommerce
- Зачем:
  - клиентская сторона мира становится целой: без add_to_cart и purchase
    в данных нет ни таксономии событий, ни вложенного JSON, ни денег,
    а метка «покупатель» из плана состава ни на что не влияла (#40).
- Что:
  - добавлен модуль commerce: корзина шире заказа, номер заказа вида
    ГГГГММДД-NNNN, промокод без скидки в сумме, сырой ecommerce через orjson;
  - метка покупателя получила два рычага — долгую жизнь куки в плане и
    свою воронку в дне; CART_PERCENT опущен с 8 до 6, чтобы конверсия
    мира осталась около 2%;
  - часть цен каталога получила копейки: productPrice округляется форматом,
    purchaseRevenue несёт точную сумму — расхождение живёт внутри события;
  - граница суток забирает страницу подтверждения вместе с её покупкой:
    потерь на клиентской стороне этот этап не заводит;
  - решения и перемеренные числа мира записаны в спеку генератора, §9.
- Проверка:
  - make lint && make typecheck && make test — 383 passed (было 353).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 19:13:30 +03:00
ddadmin bfbaa96696 docs(generator): форма эффекта метки покупателя — оба шага воронки
- Зачем:
  - холодное ревью отметило, что это решение, а не калибровка: от выбора
    шага зависит, как «покупатель» читается в данных, и оставлять выбор
    реализации нельзя.
- Что:
  - в блоке решений #40 записано, что помеченный отличается на обоих шагах
    воронки — и до корзины доходит чаще, и бросает её реже; числа под
    каждый шаг остаются калибровкой.
- Проверка:
  - чтением: docs/specs/2026-08-01-generator.md, раздел 9, блок #40.
2026-08-02 17:42:48 +03:00
ddadmin 2cfda7180f docs(generator): решения #40 после холодного ревью
- Зачем:
  - холодное ревью нашло две дыры уровня решений: лифт конверсии не делает
    покупателя видимым в данных, а цены каталога кратны рублю — от этого
    урок про Float64 остаётся без материала.
- Что:
  - метка покупателя получает второй рычаг: помеченные дольше живут и чаще
    возвращаются; границы правки заданы вилками раздела 5, база до правок
    записана числами.
  - часть цен каталога получит копейки; хвост этапу 4 переписан честнее.
  - дописаны нерешённые места: количество штук и повторная карточка,
    множество нумерации заказов, таблица «код — скидка», состав сырого
    ecommerce, резерв времени вместо зажима к полуночи.
  - два расхождения внесены в мастер-спеку и записаны в раздел 7: длина
    массивов purchase* и product*, механика класса amount_delta.
- Проверка:
  - чтением: docs/specs/2026-08-01-generator.md, разделы 7-9;
    docs/specs/2026-07-30-stand-v2-realism.md, разделы 1, 4, 7.
2026-08-02 17:37:48 +03:00
ddadmin a7442765ff docs(generator): решения #40 — торговые события
- Зачем:
  - перед реализацией торговых событий грилинг закрыл шесть развилок; без
    записи решения и отклонённые варианты потерялись бы, а часть из них
    выходит за границы тикета и меняет постановку.
- Что:
  - в раздел 9 спеки генератора добавлен блок «Решено при исполнении #40»:
    корзина шире заказа, метка покупателя становится значимой, место
    торговых событий, номер заказа, промокод, единицы денег, разная длина
    массивов purchase* и product*, orjson, хвост покупки у границы суток.
  - в раздел 8 добавлены хвосты этапам 3 и 4: скидка по промокоду и
    намеренное расхождение сумм.
  - в словарь добавлены «покупатель» и «торговое событие».
- Проверка:
  - чтением: docs/specs/2026-08-01-generator.md, разделы 8 и 9; CONTEXT.md.
2026-08-02 17:17:31 +03:00
ddmitry 898e00474a Merge pull request 'feat(generator): день-функция — трафик, визиты и просмотры страниц' (#49) from feat/39-day-function into main
Reviewed-on: #49
2026-08-02 16:32:47 +03:00
ddadminandClaude Opus 5 7dcd1c9ba3 docs(generator): README — черновой раздел «Как это работает»
Зачем: код генератора компактен, но нетривиален: многое в нём — соглашения, а
не конструкции, и чтением они не выводятся. Ближайшая опасность конкретна —
следующий этап трогает и план, и день, а вставка броска в середину функции
выглядит безобидно и молча меняет весь мир.

Что: цепочка от корневого зерна до строк событий; ленивость мира — когорта по
требованию и предыстория до D0, отсюда «любой день собирается сам по себе»;
правило порядка бросков внутри подпотока с механикой и способом обнаружения
промаха; причина целочисленной случайности со ссылкой на спеку. Раздел помечен
черновым: по сути верен, но на понятность читателем со стороны не выверен —
выверка вместе с полным путеводителем, тикет #48.

Проверка: `make lint` чист, `make test` — 353 passed; правка только в README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 16:28:14 +03:00
ddadminandClaude Opus 5 eb433ad023 feat(generator): день-функция — трафик, визиты и просмотры страниц
Зачем: план состава отдаёт дневную аудиторию, но событий у мира ещё не было.
День-функция превращает аудиторию в поток просмотров — на нём стоят лабы про
сборку визитов и про витрины, а следующий этап вешает на него торговые события.

Что:
- `day.py` — день как чистая функция зерна и номера дня: суточная волна в
  местном времени посетителя, визиты по документированным правилам нарезки,
  все 47 колонок выгрузки; шов для торговых событий — ряды `page` и `product`;
- `reference.py` — справочники-литералы: профили устройств, города Поволжья с
  настоящими гео-id Яндекса, источники трафика, карта сайта;
- `catalog.py` и `data/catalog/products.csv` — каталог на 180 позиций, общий у
  генератора и будущего словаря ClickHouse;
- `weights.py` — выбор по целым весам, один на план и на день;
- паспорт куки (устройство и город) переехал в план состава; броски приписаны
  последними, поэтому измеренные числа канонического мира не сдвинулись;
- словарь: «визит» закреплён за сессией, одноимённое понятие плана стало
  «днём активности»; статьи в `CONTEXT.md`;
- решения по ходу — в спеку генератора, раздел 9; наполнение
  `ParsedParamsKey1` отложено тикетом #47.

Проверка: `make lint`, `make typecheck`, `make test` — 353 passed (было 297).
Счётчики плана после правки те же: приток 3827,64/день, дневная аудитория
6235–7124, 68 119 посетителей за 14 дней, 170 двухкуковых пар. День 0 —
45 810 событий за 0,6 с, снимок 14 дней — 5,9 с при пороге 30 с на день.
Две слепые линии ревью, десять находок, все закрыты и перепроверены.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 16:12:17 +03:00