Closes#41 (тикнуть чек-лист #4 и закрыть тикет руками после слияния —
русские ключевые слова Gitea не понимает).
Что сделано
Генератор доведён до стенда: топик hits наполняется настоящими данными в
обоих режимах.
Канонический сериализатор на orjson — единственное место, где событие
целиком становится JSON. 47 ключей всегда, «пусто» это пустое значение,
даты ISO-8601, ecommerce строкой. Вложенный блок ecommerce в commerce.py вторым сериализатором не считается: правило про событие, а не
про блок внутри него.
Приёмники — файл (одно событие — одна строка) и Kafka (одно событие —
одно сообщение). Ключа у сообщения нет: WatchID уникален у каждого
события, ключом он был бы ключом лишь на вид, а обещать «один ключ — один
раздел» на неповторяющихся ключах нечего.
Проигрыватель и CLI — режимы batch и live (темп ×60), несколько
дней одним запуском, ограниченная пачка, раздельные тайминги генерации и
доставки, лаг в логе.
Свой образ генератора — зависимости из uv.lock, база закреплена до
патча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow
не тронут ни строкой.
Обвязка запуска — разовая служба compose под профилем, цели generate-batch и generate-live, .dockerignore, tmp/ в .gitignore.
Вторым коммитом — принцип в AGENTS.md: усложнение оплачивается учебной
ценностью, и проход на вычитание обязателен перед приёмкой заметной работы.
Он выведен из этого же прогона, но к реализации отношения не имеет, поэтому
лежит отдельно.
Решения, принятые при исполнении
Размещение — генератор в своём контейнере; даг этапа 5 будет запускать
контейнер, а не импортировать пакет. Цена названа и принята: проброс сокета
докера в Airflow, на учебном стенде размен допустим.
Клиент Kafka — confluent-kafka, необязательной группой зависимостей.
Ключ сообщения — ключа нет. Умолчания librdkafka сняты не с рендера
документации (она их не называет), а с самой библиотеки через rd_kafka_topic_conf_dump: partitioner = consistent_random, sticky.partitioning.linger.ms = 10.
Умолчаний нет у того, что описывает среду или позицию. День на оси и
имя топика приходят от зовущего; не передали — отказ с кодом 2 до того, как
сгенерировано хоть одно событие. Умолчания зерна, числа дней и темпа
остаются: они описывают мир, а не среду.
База образа записана литералом, а не берётся из .env: она из того же
закреплённого набора, что и uv.lock, и в неотслеживаемом файле ей не
место. Двум существующим образам стенда это ещё предстоит — заведён #60.
След разовых критериев
Требуют прогона целого дня через стенд, постоянными целями make быть не
могут. Снято на чистом стенде, поднятом с нуля.
Пакетный день до stg.hits_raw_dist. Отправлено 50 626, счёт по
Distributed — 50 626. Сходится.
Топик прочитан обеими нодами.clickhouse-01 — раздел 0, 26 368
сообщений, офсеты 0–26367. clickhouse-02 — раздел 1, 24 258, офсеты
0–24257. Офсеты непрерывны по каждому разделу: ни одно сообщение не несло
двух событий.
Живой день ×60. Модельное время 01:00 на 60-й реальной секунде, 02:00 на 120-й; лаг печатается. Флаг ускорения проверен отдельно:
день на --speed 1000 уходит за 86 секунд, 49 722 события доставлены и
подтверждены запросом.
Форма на проводе. В колонке rawEventDate читается как 2026-06-01, UTCEventTime — как 2026-06-01T12:34:56Z, ecommerce
лежит строкой с экранированным JSON.
Побайтовый детерминизм. Два прогона дня в независимых процессах дают
один sha256; день, сыгранный в контейнере, совпал байт в байт с днём,
сыгранным на машине.
Убирать за прогонами не нужно: типизированного ODS ещё нет, день доехал
только до stg.hits_raw_dist, а у сырья срок жизни трое суток. Стенд снесён
вместе с томами.
Проверка
make test — 406 passed. make lint, make typecheck, make config-test —
зелёные. make up с нуля, make smoke (20/0), make check-services (7/0).
Как это ревьюировалось
Две слепые линии ревью разных родословных — 15 находок; саморевью
исполнителя — ещё 7. Все закрыты: исправлением либо аргументированным
отказом. Два довода отклонены осознанно (--speed nan и отказ на пустой
прогон): в обоих случаях ошибку совершает человек над собственной командой и
видит последствие сразу.
После триажа прошёл проход на вычитание с правом только резать: срезано
149 строк и 6 тестов. Всё срезанное появилось после ревью, а не при замысле —
сторожа на находки, проверки на сторожей и третьи пересказы уже записанных
решений. Обе линии перепроверили, что несущего не убрано.
Closes #41 (тикнуть чек-лист #4 и закрыть тикет руками после слияния —
русские ключевые слова Gitea не понимает).
## Что сделано
Генератор доведён до стенда: топик `hits` наполняется настоящими данными в
обоих режимах.
- **Канонический сериализатор** на orjson — единственное место, где событие
целиком становится JSON. 47 ключей всегда, «пусто» это пустое значение,
даты ISO-8601, `ecommerce` строкой. Вложенный блок `ecommerce` в
`commerce.py` вторым сериализатором не считается: правило про событие, а не
про блок внутри него.
- **Приёмники** — файл (одно событие — одна строка) и Kafka (одно событие —
одно сообщение). Ключа у сообщения нет: `WatchID` уникален у каждого
события, ключом он был бы ключом лишь на вид, а обещать «один ключ — один
раздел» на неповторяющихся ключах нечего.
- **Проигрыватель и CLI** — режимы `batch` и `live` (темп ×60), несколько
дней одним запуском, ограниченная пачка, раздельные тайминги генерации и
доставки, лаг в логе.
- **Свой образ генератора** — зависимости из `uv.lock`, база закреплена до
патча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow
не тронут ни строкой.
- **Обвязка запуска** — разовая служба compose под профилем, цели
`generate-batch` и `generate-live`, `.dockerignore`, `tmp/` в `.gitignore`.
- **Спека** пополнена решениями (разделы 4, 8, 9), README — быстрым стартом.
Вторым коммитом — принцип в `AGENTS.md`: усложнение оплачивается учебной
ценностью, и проход на вычитание обязателен перед приёмкой заметной работы.
Он выведен из этого же прогона, но к реализации отношения не имеет, поэтому
лежит отдельно.
## Решения, принятые при исполнении
- **Размещение** — генератор в своём контейнере; даг этапа 5 будет запускать
контейнер, а не импортировать пакет. Цена названа и принята: проброс сокета
докера в Airflow, на учебном стенде размен допустим.
- **Клиент Kafka** — `confluent-kafka`, необязательной группой зависимостей.
- **Ключ сообщения** — ключа нет. Умолчания librdkafka сняты не с рендера
документации (она их не называет), а с самой библиотеки через
`rd_kafka_topic_conf_dump`: `partitioner = consistent_random`,
`sticky.partitioning.linger.ms = 10`.
- **Умолчаний нет у того, что описывает среду или позицию.** День на оси и
имя топика приходят от зовущего; не передали — отказ с кодом 2 до того, как
сгенерировано хоть одно событие. Умолчания зерна, числа дней и темпа
остаются: они описывают мир, а не среду.
- **База образа** записана литералом, а не берётся из `.env`: она из того же
закреплённого набора, что и `uv.lock`, и в неотслеживаемом файле ей не
место. Двум существующим образам стенда это ещё предстоит — заведён #60.
## След разовых критериев
Требуют прогона целого дня через стенд, постоянными целями `make` быть не
могут. Снято на чистом стенде, поднятом с нуля.
- **Пакетный день до `stg.hits_raw_dist`.** Отправлено 50 626, счёт по
Distributed — 50 626. Сходится.
- **Топик прочитан обеими нодами.** `clickhouse-01` — раздел 0, 26 368
сообщений, офсеты 0–26367. `clickhouse-02` — раздел 1, 24 258, офсеты
0–24257. Офсеты непрерывны по каждому разделу: ни одно сообщение не несло
двух событий.
- **Живой день ×60.** Модельное время `01:00` на 60-й реальной секунде,
`02:00` на 120-й; лаг печатается. Флаг ускорения проверен отдельно:
день на `--speed 1000` уходит за 86 секунд, 49 722 события доставлены и
подтверждены запросом.
- **Форма на проводе.** В колонке `raw` `EventDate` читается как
`2026-06-01`, `UTCEventTime` — как `2026-06-01T12:34:56Z`, `ecommerce`
лежит строкой с экранированным JSON.
- **Побайтовый детерминизм.** Два прогона дня в независимых процессах дают
один `sha256`; день, сыгранный в контейнере, совпал байт в байт с днём,
сыгранным на машине.
Убирать за прогонами не нужно: типизированного ODS ещё нет, день доехал
только до `stg.hits_raw_dist`, а у сырья срок жизни трое суток. Стенд снесён
вместе с томами.
## Проверка
`make test` — 406 passed. `make lint`, `make typecheck`, `make config-test` —
зелёные. `make up` с нуля, `make smoke` (20/0), `make check-services` (7/0).
## Как это ревьюировалось
Две слепые линии ревью разных родословных — 15 находок; саморевью
исполнителя — ещё 7. Все закрыты: исправлением либо аргументированным
отказом. Два довода отклонены осознанно (`--speed nan` и отказ на пустой
прогон): в обоих случаях ошибку совершает человек над собственной командой и
видит последствие сразу.
После триажа прошёл **проход на вычитание** с правом только резать: срезано
149 строк и 6 тестов. Всё срезанное появилось после ревью, а не при замысле —
сторожа на находки, проверки на сторожей и третьи пересказы уже записанных
решений. Обе линии перепроверили, что несущего не убрано.
- Зачем:
- у вопроса «чему на этом научится менти?» не хватало второй половины:
он отсеивал бесполезное, но не взвешивал цену полезного. Стенд
учебный, и чем он сложнее, тем хуже как учебный материал.
- прогон #41 показал механизм: две слепые линии ревью дали 15 находок,
саморевью ещё 7, и каждая по устройству триажа превратилась в правку.
Шага, на котором кто-нибудь вычитает, в конвейере не было ни разу —
только воронка. Проход на вычитание потом срезал 149 строк и 6 тестов,
и всё срезанное появилось после ревью, а не при замысле.
- Что:
- в раздел «Цель репозитория» добавлены три абзаца: внимание менти как
конечная валюта и вопрос «что он платит и что получает»; почему
оборонительный код дороже прочего и врёт про опасность; проход на
вычитание как обязательный шаг перед приёмкой заметной работы.
- записано частное правило, выведенное владельцем на двух резах подряд:
не сторожить ошибку, которую человек делает сам себе и тут же видит —
разбирательство с ней и есть урок.
- Проверка:
- правка текстовая, целей make не задевает; make config-test зелёный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- до сих пор генератор умел собирать день, но не умел его отдать: топик
hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит
события до стенда и закрывает форму на проводе, на которую обопрётся
типизированный ODS (#43).
- сериализатор один по решению спеки: второе место, печатающее событие в
JSON, разошлось бы с первым молча.
- Что:
- serialize.py — канонический сериализатор на orjson: единственное место,
где событие целиком становится JSON; 47 ключей всегда, «пусто» это
пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок
ecommerce в commerce.py вторым сериализатором не считается — правило
про событие, а не про блок внутри него.
- sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно
событие — одно сообщение). Ключа у сообщения нет: WatchID уникален,
ключом он был бы ключом лишь на вид.
- player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и
live (темп ×60), несколько дней одним запуском, ограниченная пачка,
раздельные тайминги генерации и доставки, лаг в логе.
- день на оси и имя топика умолчаний не имеют: параметр, описывающий
среду или позицию, приходит от зовущего, иначе отказ до генерации.
Умолчания зерна, числа дней и темпа остаются — они описывают мир.
- generator/Dockerfile — свой образ: зависимости из uv.lock, база
закреплена до патча, раскладка репозитория сохранена ради каталога
товаров. Образ Airflow не тронут.
- разовая служба compose под профилем, цели generate-batch и
generate-live, .dockerignore, tmp/ в .gitignore.
- решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README.
- Проверка:
- make test 406 passed, make lint, make typecheck, make config-test.
- побайтовый детерминизм: два прогона дня в независимых процессах дают
один sha256; день в контейнере совпадает с днём на машине.
- на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по
Distributed сошёлся — отправлено 50626, в таблице 50626.
- топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368),
clickhouse-02 раздел 1 (24258).
- живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й —
темп ×60, лаг печатается.
- форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит
строкой.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ddmitry
merged commit 77fa2905a2 into main2026-08-07 14:37:25 +03:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #41 (тикнуть чек-лист #4 и закрыть тикет руками после слияния —
русские ключевые слова Gitea не понимает).
Что сделано
Генератор доведён до стенда: топик
hitsнаполняется настоящими данными вобоих режимах.
целиком становится JSON. 47 ключей всегда, «пусто» это пустое значение,
даты ISO-8601,
ecommerceстрокой. Вложенный блокecommerceвcommerce.pyвторым сериализатором не считается: правило про событие, а непро блок внутри него.
одно сообщение). Ключа у сообщения нет:
WatchIDуникален у каждогособытия, ключом он был бы ключом лишь на вид, а обещать «один ключ — один
раздел» на неповторяющихся ключах нечего.
batchиlive(темп ×60), несколькодней одним запуском, ограниченная пачка, раздельные тайминги генерации и
доставки, лаг в логе.
uv.lock, база закреплена допатча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow
не тронут ни строкой.
generate-batchиgenerate-live,.dockerignore,tmp/в.gitignore.Вторым коммитом — принцип в
AGENTS.md: усложнение оплачивается учебнойценностью, и проход на вычитание обязателен перед приёмкой заметной работы.
Он выведен из этого же прогона, но к реализации отношения не имеет, поэтому
лежит отдельно.
Решения, принятые при исполнении
контейнер, а не импортировать пакет. Цена названа и принята: проброс сокета
докера в Airflow, на учебном стенде размен допустим.
confluent-kafka, необязательной группой зависимостей.документации (она их не называет), а с самой библиотеки через
rd_kafka_topic_conf_dump:partitioner = consistent_random,sticky.partitioning.linger.ms = 10.имя топика приходят от зовущего; не передали — отказ с кодом 2 до того, как
сгенерировано хоть одно событие. Умолчания зерна, числа дней и темпа
остаются: они описывают мир, а не среду.
.env: она из того жезакреплённого набора, что и
uv.lock, и в неотслеживаемом файле ей неместо. Двум существующим образам стенда это ещё предстоит — заведён #60.
След разовых критериев
Требуют прогона целого дня через стенд, постоянными целями
makeбыть немогут. Снято на чистом стенде, поднятом с нуля.
stg.hits_raw_dist. Отправлено 50 626, счёт поDistributed — 50 626. Сходится.
clickhouse-01— раздел 0, 26 368сообщений, офсеты 0–26367.
clickhouse-02— раздел 1, 24 258, офсеты0–24257. Офсеты непрерывны по каждому разделу: ни одно сообщение не несло
двух событий.
01:00на 60-й реальной секунде,02:00на 120-й; лаг печатается. Флаг ускорения проверен отдельно:день на
--speed 1000уходит за 86 секунд, 49 722 события доставлены иподтверждены запросом.
rawEventDateчитается как2026-06-01,UTCEventTime— как2026-06-01T12:34:56Z,ecommerceлежит строкой с экранированным JSON.
один
sha256; день, сыгранный в контейнере, совпал байт в байт с днём,сыгранным на машине.
Убирать за прогонами не нужно: типизированного ODS ещё нет, день доехал
только до
stg.hits_raw_dist, а у сырья срок жизни трое суток. Стенд снесёнвместе с томами.
Проверка
make test— 406 passed.make lint,make typecheck,make config-test—зелёные.
make upс нуля,make smoke(20/0),make check-services(7/0).Как это ревьюировалось
Две слепые линии ревью разных родословных — 15 находок; саморевью
исполнителя — ещё 7. Все закрыты: исправлением либо аргументированным
отказом. Два довода отклонены осознанно (
--speed nanи отказ на пустойпрогон): в обоих случаях ошибку совершает человек над собственной командой и
видит последствие сразу.
После триажа прошёл проход на вычитание с правом только резать: срезано
149 строк и 6 тестов. Всё срезанное появилось после ревью, а не при замысле —
сторожа на находки, проверки на сторожей и третьи пересказы уже записанных
решений. Обе линии перепроверили, что несущего не убрано.
- Зачем: - у вопроса «чему на этом научится менти?» не хватало второй половины: он отсеивал бесполезное, но не взвешивал цену полезного. Стенд учебный, и чем он сложнее, тем хуже как учебный материал. - прогон #41 показал механизм: две слепые линии ревью дали 15 находок, саморевью ещё 7, и каждая по устройству триажа превратилась в правку. Шага, на котором кто-нибудь вычитает, в конвейере не было ни разу — только воронка. Проход на вычитание потом срезал 149 строк и 6 тестов, и всё срезанное появилось после ревью, а не при замысле. - Что: - в раздел «Цель репозитория» добавлены три абзаца: внимание менти как конечная валюта и вопрос «что он платит и что получает»; почему оборонительный код дороже прочего и врёт про опасность; проход на вычитание как обязательный шаг перед приёмкой заметной работы. - записано частное правило, выведенное владельцем на двух резах подряд: не сторожить ошибку, которую человек делает сам себе и тут же видит — разбирательство с ней и есть урок. - Проверка: - правка текстовая, целей make не задевает; make config-test зелёный. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>- Зачем: - до сих пор генератор умел собирать день, но не умел его отдать: топик hits наполнялся пробником, а не настоящими данными. Тикет #41 доводит события до стенда и закрывает форму на проводе, на которую обопрётся типизированный ODS (#43). - сериализатор один по решению спеки: второе место, печатающее событие в JSON, разошлось бы с первым молча. - Что: - serialize.py — канонический сериализатор на orjson: единственное место, где событие целиком становится JSON; 47 ключей всегда, «пусто» это пустое значение, даты ISO-8601, ecommerce строкой. Вложенный блок ecommerce в commerce.py вторым сериализатором не считается — правило про событие, а не про блок внутри него. - sinks.py — приёмники: файл (одно событие — одна строка) и Kafka (одно событие — одно сообщение). Ключа у сообщения нет: WatchID уникален, ключом он был бы ключом лишь на вид. - player.py, cli.py — проигрыватель и интерфейс запуска: режимы batch и live (темп ×60), несколько дней одним запуском, ограниченная пачка, раздельные тайминги генерации и доставки, лаг в логе. - день на оси и имя топика умолчаний не имеют: параметр, описывающий среду или позицию, приходит от зовущего, иначе отказ до генерации. Умолчания зерна, числа дней и темпа остаются — они описывают мир. - generator/Dockerfile — свой образ: зависимости из uv.lock, база закреплена до патча, раскладка репозитория сохранена ради каталога товаров. Образ Airflow не тронут. - разовая служба compose под профилем, цели generate-batch и generate-live, .dockerignore, tmp/ в .gitignore. - решения внесены в спеку (разделы 4, 8, 9), быстрый старт — в README. - Проверка: - make test 406 passed, make lint, make typecheck, make config-test. - побайтовый детерминизм: два прогона дня в независимых процессах дают один sha256; день в контейнере совпадает с днём на машине. - на стенде: пакетный день доехал до stg.hits_raw_dist, счёт по Distributed сошёлся — отправлено 50626, в таблице 50626. - топик прочитан обеими нодами: clickhouse-01 раздел 0 (26368), clickhouse-02 раздел 1 (24258). - живой день: модельное время 01:00 на 60-й секунде, 02:00 на 120-й — темп ×60, лаг печатается. - форма на проводе в колонке raw: даты читаются глазами, ecommerce лежит строкой. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>