docs(glossary): четыре термина переименованы, world.md и stand.md вычитаны
- Зачем:
- справочники читает человек со стороны, а «пульт мира», «опись мира»,
«работник» и «сырьё» звучали шуткой или калькой времён разработки
(тикет #214, выбор владельца).
- Что:
- CONTEXT.md: статьи «Паспорт мира», «Даги модельного времени»,
«Шаговый даг», «Сырые данные», старые слова в строках «Избегать»;
пометки в ADR 0005, 0008, 0009 без переписывания решений.
- замены по README, справочникам, путеводителю, лабам, спекам,
комментариям дагов и SQL, тегу дага и панели Grafana; имена в коде
не тронуты.
- world.md и stand.md: 35 находок вычитки внесены, регистр выровнен.
- Проверка:
- make config-test; make lint в корне и в generator/; make test в
generator/ (437 passed); скрипт ссылок из #212 пуст; grep старых
слов вне ADR, research и CONTEXT.md пуст.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
This commit is contained in:
1 parent
86ad8afaf2
commit
59d8c83b17
54 files changed
+794
-776
No files matched your search
@@ -272,12 +272,13 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
Классы пересекаются — приоритет: `cancelled` > `lost_event` >
|
||||
`duplicate_event` > `amount_delta` > `match`.
|
||||
|
||||
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов
|
||||
впервые появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»),
|
||||
ориентир ~10%. Точные доли зафиксированы замером на каноническом мире
|
||||
(28 дней, 2026-08-30, #194) — запись в «Что проверено»
|
||||
[карты проверок](../architecture/testing.md); опись хранит счётчики наблюдаемых классов после приоритета — в заказах и
|
||||
только по дням с закрытым окном (раздел 8).
|
||||
Пятое — **опоздание** — бесплатно даёт формат доставки: часть заказов впервые
|
||||
появляется в слепке D+1/D+2 («вчера не сходилось, сегодня сошлось»), ориентир
|
||||
~10%. Точные доли зафиксированы замером на каноническом мире (28 дней,
|
||||
2026-08-30, #194) — запись в «Что проверено» [карты
|
||||
проверок](../architecture/testing.md); паспорт хранит счётчики наблюдаемых
|
||||
классов после приоритета — в заказах и только по дням с закрытым окном (раздел
|
||||
8).
|
||||
|
||||
Не берём: сироту-фрод (`purchase` есть, а заказа не будет никогда) —
|
||||
механически дублирует B.
|
||||
@@ -311,14 +312,13 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
запуск пишет полный текущий набор различных пар; предыдущий результат не
|
||||
служит источником следующего.
|
||||
- **N:1**: часть покупателей покупает с двух кук («телефон и ноутбук») —
|
||||
параметр мира; значение фиксирует эта спека: 15%, детерминировано
|
||||
от seed. Ядро лабы:
|
||||
`uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
|
||||
Константа мира: каждый двухкуковый покупатель делает минимум по одному
|
||||
заказу с каждой куки — иначе вторая кука не попадает в карту соответствий
|
||||
(она строится только из покупок) и лаба не воспроизводится. Число таких
|
||||
пар растёт, пока мир едет, и в опись не кладётся
|
||||
([что хранит опись](../architecture/orders/inventory.md)).
|
||||
параметр мира; значение фиксирует эта спека: 15%, детерминировано от seed.
|
||||
Ядро лабы: `uniq(посетителей) > uniq(людей)`, менти выводит расхождение сам.
|
||||
Константа мира: каждый двухкуковый покупатель делает минимум по одному заказу
|
||||
с каждой куки — иначе вторая кука не попадает в карту соответствий (она
|
||||
строится только из покупок) и лаба не воспроизводится. Число таких пар растёт,
|
||||
пока мир едет, и в паспорт не кладётся ([что хранит
|
||||
паспорт](../architecture/orders/inventory.md)).
|
||||
- Витрины разводят имена честно: **«посетители»** (`uniq(ClientID)`) и
|
||||
**«известные пользователи»** (после склейки) — оба числа рядом в дашборде.
|
||||
|
||||
@@ -339,16 +339,14 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
|
||||
ноду 2**. Это осознанная ловушка правильных ошибок: забытый ON CLUSTER или
|
||||
VIEW поверх локальной таблицы проявляются в дашборде сами.
|
||||
- **Приём Kafka**: у событий — Kafka-таблицы и MV на обеих нодах, одна
|
||||
consumer group, 2 партиции на топик `hits`; MV пишут в Distributed-цели.
|
||||
У заказов приём другого рода — пакетный забор без MV, чтец на одной ноде,
|
||||
топик `orders` в одну партицию ([ADR 0008](../adr/0008-order-ingestion.md)).
|
||||
Раскладку в обоих случаях решает ключ:
|
||||
события — по `cityHash64(ClientID)` (см. 1.3), заказы —
|
||||
`cityHash64(order_id)`, сырьё STG — `cityHash64(сырой строки)`; полный
|
||||
список и доводы — в [доке хранилища](../architecture/storage.md). Урок
|
||||
«какая нода читала топик — меняется между прогонами, куда легли данные —
|
||||
нет» живёт на событиях: при пакетном заборе читает та нода, которую
|
||||
спросили.
|
||||
consumer group, 2 партиции на топик `hits`; MV пишут в Distributed-цели. У
|
||||
заказов приём другого рода — пакетный забор без MV, чтец на одной ноде, топик
|
||||
`orders` в одну партицию ([ADR 0008](../adr/0008-order-ingestion.md)).
|
||||
Раскладку в обоих случаях решает ключ: события — по `cityHash64(ClientID)`
|
||||
(см. 1.3), заказы — `cityHash64(order_id)`, STG — `cityHash64(сырой строки)`;
|
||||
полный список и доводы — в [доке хранилища](../architecture/storage.md). Урок
|
||||
«какая нода читала топик — меняется между прогонами, куда легли данные — нет»
|
||||
живёт на событиях: при пакетном заборе читает та нода, которую спросили.
|
||||
- **Приём строгий**: пять опорных колонок — `WatchID`, `VisitID`, `ClientID`,
|
||||
`EventDate`, `UTCEventTime` — разбираются как `Nullable`, а набор ключей
|
||||
сообщения сверяется с контрактным; строка с NULL среди опорных колонок
|
||||
@@ -444,23 +442,24 @@ README.
|
||||
| DM | витрины `dm.*_v`, `dm.dq_summary` | см. ниже |
|
||||
|
||||
У каждой таблицы слоя — пара из локальной и распределённой, имена по конвенции
|
||||
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырья —
|
||||
см. [доку хранилища](../architecture/storage.md).
|
||||
Потребительские запросы и витрины читают сущности DDS через `_v` с явным
|
||||
списком колонок; `_rep` и `_dist` служат загрузке, диагностике и обслуживанию.
|
||||
Договор держится конвенцией, а не правами: модель доступа описана в
|
||||
[ADR 0007](../adr/0007-clickhouse-access.md), договор чтения — в
|
||||
[доке хранилища](../architecture/storage.md), «Имена объектов».
|
||||
суффиксов; она же задаёт служебные колонки, нарезку и срок хранения сырых данных
|
||||
— см. [доку хранилища](../architecture/storage.md). Потребительские запросы и
|
||||
витрины читают сущности DDS через `_v` с явным списком колонок; `_rep` и `_dist`
|
||||
служат загрузке, диагностике и обслуживанию. Договор держится конвенцией, а не
|
||||
правами: модель доступа описана в [ADR 0007](../adr/0007-clickhouse-access.md),
|
||||
договор чтения — в [доке хранилища](../architecture/storage.md), «Имена
|
||||
объектов».
|
||||
|
||||
Заказы принимаются **пакетным забором** ([ADR
|
||||
0008](../adr/0008-order-ingestion.md), [ADR
|
||||
0010](../adr/0010-order-versions-in-ods.md)): чтец топика байтовый, как у событий, но
|
||||
матвью к нему не привязана, и сырьё забирает шаг, которым управляет Airflow —
|
||||
одно прямое чтение вставляет порцию в `stg.orders_raw` с `_load_id` запуска.
|
||||
Один следующий `task_id` двумя последовательными запросами пишет годные строки
|
||||
и ошибки из того же среза. `ods.order` выбирает текущую версию по максимальной
|
||||
`snapshot_date` ([ADR 0014](../adr/0014-order-version-snapshot-date.md)). Слой
|
||||
сырья остаётся точкой повтора и разбора одной принятой порции.
|
||||
0010](../adr/0010-order-versions-in-ods.md)): чтец топика байтовый, как у
|
||||
событий, но матвью к нему не привязана, и порцию в STG забирает шаг под
|
||||
управлением Airflow — одно прямое чтение вставляет её в `stg.orders_raw` с
|
||||
`_load_id` запуска. Один следующий `task_id` двумя последовательными запросами
|
||||
пишет годные строки и ошибки из того же среза. `ods.order` выбирает текущую
|
||||
версию по максимальной `snapshot_date` ([ADR
|
||||
0014](../adr/0014-order-version-snapshot-date.md)). Слой Сырой слой остаётся
|
||||
точкой повтора и разбора одной принятой порции.
|
||||
|
||||
Состав служебных колонок задаёт дока хранилища. Спеке важны два следствия:
|
||||
`ods.event` и `ods.order` получают метку загрузки `_load_ts`, но версию
|
||||
@@ -531,31 +530,28 @@ Kafka день переигрывается генератором заново:
|
||||
пачками), Postgres остаётся только служебной базой Airflow. Смешанность
|
||||
ландшафта выражена режимами и частотами, а не второй трубой.
|
||||
|
||||
## 8. Эталонный мир и опись
|
||||
## 8. Эталонный мир и паспорт
|
||||
|
||||
В git хранится только опись эталонного мира; сам снимок генерируется на
|
||||
месте — при `make up` и при проверках (решение развилки «Производительность»
|
||||
карты #26, подробности — [спека генератора](2026-08-01-generator.md),
|
||||
раздел 5). Снимок — 8 дней, совпадает со стартовым миром: обещание в 14 дней
|
||||
снято на этапе 7. Опись несёт
|
||||
паспорт мира (каноническое зерно, версия генератора), контрольные счётчики
|
||||
и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N —
|
||||
сравни хеш» живут на ней. Политика версионирования артефакта (бывший туман
|
||||
карты #10) закрыта этим же ходом: версионируется опись.
|
||||
Контрольные числа описи:
|
||||
В git хранится только паспорт эталонного мира; сам снимок генерируется на месте
|
||||
— при `make up` и при проверках (решение развилки «Производительность» карты
|
||||
#26, подробности — [спека генератора](2026-08-01-generator.md), раздел 5).
|
||||
Снимок — 8 дней, совпадает со стартовым миром: обещание в 14 дней снято на этапе
|
||||
7. Паспорт несёт шапку с каноническим зерном и версией генератора, контрольные
|
||||
счётчики и хеши по дням; проверки «пустой git diff» и «пересгенерируй день N —
|
||||
сравни хеш» живут на нём. Политика версионирования артефакта (бывший туман карты
|
||||
#10) закрыта этим же ходом: версионируется паспорт. Контрольные числа паспорта:
|
||||
|
||||
- по строке на каждый отправленный слепок — хеш байтов: побайтовое обещание
|
||||
«слепок переснимается и даёт те же байты» опись сторожит наравне с днями;
|
||||
«слепок переснимается и даёт те же байты» паспорт сторожит наравне с днями;
|
||||
- заказная сторона: счётчики наблюдаемых классов расхождений после
|
||||
приоритета; каждый показывает число заказов и хранится только для дня с
|
||||
закрытым окном (читатель — техническая проверка `classes_vs_inventory`
|
||||
звена DQ; при N сыгранных днях таких дней N − 7). Отдельных полей для
|
||||
общего числа заказов и выручки в описи пока нет: для них не назван
|
||||
читатель;
|
||||
закрытым окном (читатель — техническая проверка `classes_vs_inventory` звена
|
||||
DQ; при N сыгранных днях таких дней N − 7). Отдельных полей для общего числа
|
||||
заказов и выручки в паспорте пока нет: для них не назван читатель;
|
||||
- контрольные числа идентичности не заводятся: uniq известных пользователей
|
||||
и число двухкуковых покупателей растут, пока мир едет, а счётчик без
|
||||
названного читателя в опись не кладётся
|
||||
([что хранит опись](../architecture/orders/inventory.md)).
|
||||
названного читателя в паспорт не кладётся ([что хранит
|
||||
паспорт](../architecture/orders/inventory.md)).
|
||||
|
||||
Размер снимка замерен на этапе 7 — запись в разделе «Что проверено»
|
||||
[карты проверок](../architecture/testing.md).
|
||||
@@ -572,7 +568,7 @@ v2 стартует пустым, поэтому объём ниже — это
|
||||
| SQL | DDL по слоям и ролям (ON CLUSTER, Replicated*, Distributed; раскладка файлов — в доке хранилища) + трансформации событий, заказов, identity, сверки + словарь | L — ~12–15 файлов, главная сложность |
|
||||
| Airflow | DAG'и по образцу v1: etl_pipeline (партиционная переобработка), world_init/next_day, helpers; первый даг — переливка слепка — приходит раньше, этапом 3 | M — ~5–6 файлов |
|
||||
| Superset | датасеты + дашборд с тремя новыми сюжетами | M — 2 файла |
|
||||
| Эталонный мир | пересборка снимка на месте, счётчики описи, чек-скрипты | M–L |
|
||||
| Эталонный мир | пересборка снимка на месте, счётчики паспорта, чек-скрипты | M–L |
|
||||
| Мониторинг | дашборды Grafana «данные», «кластер», «запросы»; ClickHouse источником данных, панели на SQL; Prometheus тонким полом (ADR 0002) | M — конфиги и дашборды |
|
||||
| Документация | доки v2 пишутся заново (см. раздел 12) | M, в тех же PR |
|
||||
|
||||
@@ -619,8 +615,8 @@ v2 стартует пустым, поэтому объём ниже — это
|
||||
4. Трансформации и витрины: модель DDS заказа, сессии, identity_map,
|
||||
выручка, сверка A+C.
|
||||
5. Airflow: `etl_pipeline` (партиционная переобработка).
|
||||
6. Расхождения B+D и опоздания; счётчики описи.
|
||||
7. Эталонный мир: опись и пересборка снимка, чек-скрипты. CI-генерация
|
||||
6. Расхождения B+D и опоздания; счётчики паспорта.
|
||||
7. Эталонный мир: паспорт и пересборка снимка, чек-скрипты. CI-генерация
|
||||
снята (спека генератора, раздел 5).
|
||||
8. Superset-дашборд v2.
|
||||
9. Мониторинг и runbook «keeper упал / DDL повис в очереди». Состав дашбордов
|
||||
@@ -658,15 +654,15 @@ ADR 0002 с приёмочными совпадают почти целиком.
|
||||
расхождение часов клиент/коллектор — туман карты, вернётся своим тикетом.
|
||||
- Лабы и курс: v2 — другой стенд, лабы для него пишутся с нуля отдельной
|
||||
работой после этой спеки; редизайн лаб v1 (#7) остаётся в v1 и сюда не
|
||||
переносится. Исключение — домашки слоя DM: ТЗ витрин менти и даг
|
||||
самопроверки забраны в этап 4 резолюцией #135, потому что витрина менти и
|
||||
конвейер, в который она встает, проектируются вместе. Сюда же —
|
||||
необязательная лаба «свой дашборд в Superset» (#197): она стоит на
|
||||
витринах домашек DM и продумана до готового ТЗ в этапе 8, тогда как
|
||||
остальные лабы пока на уровне идей. Единый стиль блока
|
||||
домашек — за работой учебного пути. Спека даёт будущим лабам только
|
||||
опорные точки — контрольные числа описи (сверка, идентичность). Явное
|
||||
следствие: после этапа 9 стенд работает, но учебного пути на нём ещё нет.
|
||||
переносится. Исключение — домашки слоя DM: ТЗ витрин менти и даг самопроверки
|
||||
забраны в этап 4 резолюцией #135, потому что витрина менти и конвейер, в
|
||||
который она встает, проектируются вместе. Сюда же — необязательная лаба «свой
|
||||
дашборд в Superset» (#197): она стоит на витринах домашек DM и продумана до
|
||||
готового ТЗ в этапе 8, тогда как остальные лабы пока на уровне идей. Единый
|
||||
стиль блока домашек — за работой учебного пути. Спека даёт будущим лабам
|
||||
только опорные точки — контрольные числа паспорта (сверка, идентичность).
|
||||
Явное следствие: после этапа 9 стенд работает, но учебного пути на нём ещё
|
||||
нет.
|
||||
- Инкрементальный ETL (#8) — свой issue.
|
||||
- Реплики (2×2), HAProxy, репликационная эксплуатация — в лекцию, не в стенд.
|
||||
- TLS между нодами, настоящее хранилище и ротация секретов, внешние учётные
|
||||
|
||||
+132
-139
@@ -41,10 +41,10 @@
|
||||
конструкция внутри оправдана только наблюдаемым эффектом: без него это
|
||||
сложность, за которую никто не заплатил.
|
||||
- **Повторимы должны быть эффекты, а не конкретные числа.** Правка свойств
|
||||
мира сдвигает почти все числа разом — генератор случайных чисел выдаёт
|
||||
другую последовательность. Опираться на конкретные числа поэтому нечем.
|
||||
Хеши описи обещают не постоянство чисел, а то, что их сдвиг не пройдёт
|
||||
молча: пересобрал опись — увидел diff.
|
||||
мира сдвигает почти все числа разом — генератор случайных чисел выдаёт другую
|
||||
последовательность. Опираться на конкретные числа поэтому нечем. Хеши паспорта
|
||||
обещают не постоянство чисел, а то, что их сдвиг не пройдёт молча: пересобрал
|
||||
паспорт — увидел diff.
|
||||
- **Цена ошибки здесь мала.** В худшем случае испорчен урок, и чинится он
|
||||
правкой генератора после взгляда на получившиеся распределения. Оборона,
|
||||
которая дороже ошибки, не заводится.
|
||||
@@ -59,15 +59,15 @@
|
||||
модельный день D — функция (зерно, D). Между прогонами живут только зерно
|
||||
и позиция на оси времени.
|
||||
- **Детерминизм до байта.** Одно зерно — побайтово тот же снимок; сверка —
|
||||
хешами описи. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||||
хешами паспорта. Транспорт (офсеты Kafka, темп) — вне обещания.
|
||||
- **Схема — контракт генератора.** Python-модуль с чистыми данными;
|
||||
хранилище строится по рендеренной документации, границу сторожит строгий
|
||||
приём на стороне хранилища.
|
||||
- **Один сериализатор, глупые приёмники.** День-функция выдаёт канонические
|
||||
байты; приёмники — файл, Kafka пачкой, Kafka с темпом.
|
||||
- **Числа.** Средний день ~50 тыс. событий; эталонный снимок совпадает со
|
||||
стартовым миром — 8 дней;
|
||||
в git — только опись мира; автоматических порогов по времени нет.
|
||||
стартовым миром — 8 дней; в git — только паспорт мира; автоматических порогов
|
||||
по времени нет.
|
||||
|
||||
## 1. Модель мира
|
||||
|
||||
@@ -109,23 +109,21 @@
|
||||
начинается в D0) — только дают, кому возвращаться в первые дни: дневная
|
||||
аудитория на полке с самого D0, разгона «пустого магазина» нет.
|
||||
- **Гарантия двухкуковых пар — назначенные заказы в плане.** Единица
|
||||
здесь — человек, не кука: план помечает часть людей покупателями
|
||||
(доля — раздел 9), и 15% покупателей (мастер-спека, раздел 5) получают
|
||||
вторую куку. Обе куки пары принадлежат одному человеку одной когорты;
|
||||
вторая рождается в пределах его окна активности, без фиксированного
|
||||
зазора — по тому же затухающему профилю, что и возвраты (уточнение при
|
||||
исполнении #38, 2026-08-02): куку человек заводит, пока ещё ходит.
|
||||
Равные шансы по всему окну означали бы вторую куку у давно ушедшего
|
||||
человека — и вчетверо меньше пар, реализованных внутри снимка.
|
||||
Каждой паре план назначает дни гарантированных заказов: по
|
||||
одному на куку, из дней активности этой куки, на оси от D0 и позже;
|
||||
человеку предыстории, чьё окно активности таких дней не оставляет,
|
||||
пара не назначается. День-функция обязана назначенные заказы
|
||||
реализовать; остальные покупки — вольные, их решает генератор торговых
|
||||
событий (#40). План считает пары, реализованные в запрошенном горизонте;
|
||||
опись это число не хранит — оно растёт вместе с миром. Условность в данных
|
||||
не видна: дни назначены той же случайностью, просто брошенной планом один
|
||||
раз.
|
||||
здесь — человек, не кука: план помечает часть людей покупателями (доля —
|
||||
раздел 9), и 15% покупателей (мастер-спека, раздел 5) получают вторую куку.
|
||||
Обе куки пары принадлежат одному человеку одной когорты; вторая рождается в
|
||||
пределах его окна активности, без фиксированного зазора — по тому же
|
||||
затухающему профилю, что и возвраты (уточнение при исполнении #38,
|
||||
2026-08-02): куку человек заводит, пока ещё ходит. Равные шансы по всему окну
|
||||
означали бы вторую куку у давно ушедшего человека — и вчетверо меньше пар,
|
||||
реализованных внутри снимка. Каждой паре план назначает дни гарантированных
|
||||
заказов: по одному на куку, из дней активности этой куки, на оси от D0 и
|
||||
позже; человеку предыстории, чьё окно активности таких дней не оставляет, пара
|
||||
не назначается. День-функция обязана назначенные заказы реализовать; остальные
|
||||
покупки — вольные, их решает генератор торговых событий (#40). План считает
|
||||
пары, реализованные в запрошенном горизонте; паспорт это число не хранит — оно
|
||||
растёт вместе с миром. Условность в данных не видна: дни назначены той же
|
||||
случайностью, просто брошенной планом один раз.
|
||||
- **Своя ось модельного времени.** Ось событий начинается в
|
||||
фиксированный день D0 (понедельник — см. раздел 5); реальный
|
||||
календарь в модели не участвует. В `EventDate`/`UTCEventTime` дни оси
|
||||
@@ -133,10 +131,10 @@
|
||||
зависящая (значение — раздел 9). Между прогонами живут только зерно и
|
||||
позиция на оси: выключенный ноутбук — мир замер, потом продолжил.
|
||||
- **Два режима движения по одной оси.** Пошаговый — базовый для лаб: старт
|
||||
с эталонного снимка, дальше «прожить следующий день» — явное действие.
|
||||
Живой день — текущий день проигрывается с ускорением, дашборд и мониторинг
|
||||
«дышат»; включается по требованию — постоянным фоном идёт, только пока
|
||||
включён выключатель пульта ([ADR 0009](../adr/0009-world-control.md)).
|
||||
с эталонного снимка, дальше «прожить следующий день» — явное действие. Живой
|
||||
день — текущий день проигрывается с ускорением, дашборд и мониторинг «дышат»;
|
||||
включается по требованию — постоянным фоном идёт, только пока включён
|
||||
выключатель `world_live` ([ADR 0009](../adr/0009-world-control.md)).
|
||||
- **Граница суток — единственный структурный шов.** Сессии режутся по ней,
|
||||
дневная партиция самодостаточна; в конце модельного дня — слепок заказов.
|
||||
День проживается целиком, полдня не бывает: недожитый из-за обрыва день
|
||||
@@ -148,14 +146,13 @@
|
||||
Отклонено с доводами:
|
||||
|
||||
- *Мутирующее состояние мира* («мир стареет»): ломает параллельность по
|
||||
дням, требует чекпоинтов, счётчики описи узнаваемы только постфактум;
|
||||
ни один урок стенда на старении не стоит.
|
||||
дням, требует чекпоинтов, счётчики паспорта узнаваемы только постфактум; ни
|
||||
один урок стенда на старении не стоит.
|
||||
- *Чистая функция без слоя состава*: глобальные инварианты пришлось бы
|
||||
выводить в каждом дне заново — тот же план мира, но неявный и размазанный.
|
||||
- *Привязка модельного времени к реальному календарю* (T-1 с догоном):
|
||||
конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает
|
||||
даты эталонного мира зависимыми от даты запуска, опись теряет
|
||||
воспроизводимость.
|
||||
конфликтует с ускорением ×60 — за вечер мир уезжает в будущее — и делает даты
|
||||
эталонного мира зависимыми от даты запуска, паспорт теряет воспроизводимость.
|
||||
|
||||
Отклонено при исполнении #38 (2026-08-02):
|
||||
|
||||
@@ -170,9 +167,9 @@
|
||||
константы — потолка одновременно живущих кук, которого в жизни нет и
|
||||
который менти нечем объяснить.
|
||||
- *Вероятностная гарантия пар* («почти наверняка купит с обеих кук»): не
|
||||
гарантия — однажды опись покраснеет, а чинить нечем, кроме смены
|
||||
зерна; при этом несклеенная пара в данных неотличима от двух незнакомцев,
|
||||
так что реализм этой лотереи невидим.
|
||||
гарантия — однажды паспорт покраснеет, а чинить нечем, кроме смены зерна; при
|
||||
этом несклеенная пара в данных неотличима от двух незнакомцев, так что реализм
|
||||
этой лотереи невидим.
|
||||
- *Покупка пары в первый визит куки*: гарантия железная и дешёвая, но узор
|
||||
«все пары покупают в первый день» виден в данных ровно там, куда лаба
|
||||
склейки смотрит пристальнее всего. Условность допустима, пока она не
|
||||
@@ -185,31 +182,29 @@
|
||||
- **Обещание — содержимое до байта.** Два прогона с одним зерном дают тот же
|
||||
набор событий: те же `WatchID`/`VisitID`, поля, метки модельного времени.
|
||||
Снимок при пересборке побайтово совпадает: канонический порядок ключей и
|
||||
строк; сверка — по хешам описи, а два локально пересобранных снимка
|
||||
сравнимы обычным diff — пустой означает «ничего не изменилось». Вне
|
||||
обещания — транспорт: офсеты и партиции Kafka, какая нода прочитала,
|
||||
`_load_ts`, темп живого дня.
|
||||
строк; сверка — по хешам паспорта, а два локально пересобранных снимка
|
||||
сравнимы обычным diff — пустой означает «ничего не изменилось». Вне обещания —
|
||||
транспорт: офсеты и партиции Kafka, какая нода прочитала, `_load_ts`, темп
|
||||
живого дня.
|
||||
- **Условия обещания.** Детерминизм держится при зафиксированном `uv.lock`
|
||||
и внутри канонического контейнера — то есть везде Linux, на маке и в WSL
|
||||
тоже; единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость любой машины проверяет
|
||||
скрипт «пересгенерируй день N — сравни хеш с описью». Расхождение на
|
||||
любой платформе — баг генератора, а не допуск.
|
||||
и внутри канонического контейнера — то есть везде Linux, на маке и в WSL тоже;
|
||||
единственная переменная — архитектура CPU. Истина — CI на Linux; сходимость
|
||||
любой машины проверяет скрипт «пересгенерируй день N — сравни хеш с
|
||||
паспортом». Расхождение на любой платформе — баг генератора, а не допуск.
|
||||
- **Раздача зерна — иерархией подпотоков.** Корневое зерно → подпоток
|
||||
состава мира, ветвящийся по номеру дня на когорты плана (состав
|
||||
спрашивается по дням — раздел 1; уточнение при исполнении #38).
|
||||
Дни предыстории отрицательны, а позиция в дереве — неотрицательное
|
||||
целое, поэтому у предыстории своя ветвь состава, отдельная от оси
|
||||
(уточнение при исполнении #38, 2026-08-02);
|
||||
(зерно, день) → подпоток дня → именованные подпотоки компонентов: трафик,
|
||||
торговые события, заказная и событийная стороны (судьба заказов и порча
|
||||
событийного потока) — в фиксированном порядке (уточнение [судьбой
|
||||
заказа](../architecture/orders/fate.md): прежние «расхождения» и
|
||||
«опоздания» были названы по классам витрины, а компонент называет часть
|
||||
мира).
|
||||
По построению: параллельный прогон равен последовательному; продление
|
||||
истории днём N+1 не трогает дни 1…N; правка одного компонента меняет
|
||||
только его часть снимка — в описи меняются хеши только затронутых
|
||||
дней, дифф двух локальных пересборок читаем.
|
||||
состава мира, ветвящийся по номеру дня на когорты плана (состав спрашивается
|
||||
по дням — раздел 1; уточнение при исполнении #38). Дни предыстории
|
||||
отрицательны, а позиция в дереве — неотрицательное целое, поэтому у
|
||||
предыстории своя ветвь состава, отдельная от оси (уточнение при исполнении
|
||||
#38, 2026-08-02); (зерно, день) → подпоток дня → именованные подпотоки
|
||||
компонентов: трафик, торговые события, заказная и событийная стороны (судьба
|
||||
заказов и порча событийного потока) — в фиксированном порядке (уточнение
|
||||
[судьбой заказа](../architecture/orders/fate.md): прежние «расхождения» и
|
||||
«опоздания» были названы по классам витрины, а компонент называет часть мира).
|
||||
По построению: параллельный прогон равен последовательному; продление истории
|
||||
днём N+1 не трогает дни 1…N; правка одного компонента меняет только его часть
|
||||
снимка — в паспорте меняются хеши только затронутых дней, дифф двух локальных
|
||||
пересборок читаем.
|
||||
- **Механизм подпотоков — `numpy.random.SeedSequence`.** Сверено через
|
||||
Context7 по документации numpy (2026-08-01): `spawn(n)` порождает детей
|
||||
расширением `spawn_key`, потомок полностью определяется парой
|
||||
@@ -223,8 +218,8 @@
|
||||
представление в клиентском `purchase` (урок мастер-спеки о расхождениях).
|
||||
- **Канонический seed и паспорт мира.** Эталонный мир собирается одним
|
||||
каноническим зерном — константой репозитория; свои зёрна менти крутит без
|
||||
гарантий описи. Опись хранит паспорт мира — зерно и версию
|
||||
генератора; чек-скрипты сверяют паспорт раньше счётчиков.
|
||||
гарантий паспорта. В шапке паспорта лежат зерно и версия генератора;
|
||||
чек-скрипты сверяют шапку раньше счётчиков.
|
||||
- **Суточный профиль интенсивности задаёт день-функция.** Форма — волны:
|
||||
ночной провал, обеденный и вечерний пики, различие будней и выходных;
|
||||
пики — до ~2× среднего. С детерминизмом профиль совместим: это часть
|
||||
@@ -241,8 +236,8 @@
|
||||
«параллельно равно последовательно» недостижимо.
|
||||
- *Обещание детерминизма поверх обновления зависимостей*: numpy сознательно
|
||||
улучшает алгоритмы распределений между версиями (NEP 19), Faker меняет
|
||||
словари. Фиксация — `uv.lock`; обновление зависимостей — осознанная
|
||||
пересборка описи одним PR.
|
||||
словари. Фиксация — `uv.lock`; обновление зависимостей — осознанная пересборка
|
||||
паспорта одним PR.
|
||||
|
||||
## 3. Контракт схемы
|
||||
|
||||
@@ -289,9 +284,9 @@
|
||||
|
||||
- **Один канонический сериализатор, глупые приёмники.** День-функция выдаёт
|
||||
упорядоченный поток канонических байтов — единственное место, где событие
|
||||
превращается в JSON. Приёмники не знают о содержимом: файл (локальный кэш
|
||||
для пересборки и проверок описи), Kafka пачкой — пакетный режим,
|
||||
Kafka с темпом ×60 — живой день. Новых топиков нет.
|
||||
превращается в JSON. Приёмники не знают о содержимом: файл (локальный кэш для
|
||||
пересборки и проверок паспорта), Kafka пачкой — пакетный режим, Kafka с темпом
|
||||
×60 — живой день. Новых топиков нет.
|
||||
- **Одно событие — одно сообщение Kafka.** «Пачкой» относится к темпу
|
||||
отправки, а не к упаковке: приёмник шлёт события подряд без пауз, но каждое
|
||||
отдельным сообщением. Контракт транспорта, не деталь реализации — сторона
|
||||
@@ -299,9 +294,9 @@
|
||||
([ADR 0005](../adr/0005-event-ingestion.md)), поэтому склейка нескольких
|
||||
событий в одно сообщение сломала бы разбор целиком.
|
||||
- **Даты и время на проводе — ISO-8601.** `EventDate` уезжает как `2026-06-01`,
|
||||
`UTCEventTime` — как `2026-06-01T12:34:56Z`. Довод — читаемость сырья: весь
|
||||
смысл слоя STG в том, что менти открывает колонку `raw` в обычном клиенте и
|
||||
разбирает событие глазами, а число эпохи этот урок убивает. Колонка
|
||||
`UTCEventTime` — как `2026-06-01T12:34:56Z`. Довод — читаемость сырых данных:
|
||||
весь смысл слоя STG в том, что менти открывает колонку `raw` в обычном клиенте
|
||||
и разбирает событие глазами, а число эпохи этот урок убивает. Колонка
|
||||
`ecommerce` — строка, внутри которой лежит экранированный JSON, как отдаёт
|
||||
Метрика.
|
||||
|
||||
@@ -321,9 +316,9 @@
|
||||
первым. Записана форма всё равно здесь — иначе каждый выберет своё, и
|
||||
разойдётся это уже после приёмки.
|
||||
- **Рабочий выбор сериализатора — orjson**: быстрее stdlib json в 5–14 раз,
|
||||
numpy-массивы и datetime сериализует нативно (заметка исследования #31).
|
||||
Смена библиотеки меняет канонические байты, поэтому проходит как
|
||||
обновление зависимости: осознанная пересборка описи одним PR.
|
||||
numpy-массивы и datetime сериализует нативно (заметка исследования #31). Смена
|
||||
библиотеки меняет канонические байты, поэтому проходит как обновление
|
||||
зависимости: осознанная пересборка паспорта одним PR.
|
||||
- **Промежуточные файлы не хранятся.** Файл дня — кэш чистой функции:
|
||||
потерял — пересчитал. В git снимок не попадает (раздел 5).
|
||||
- **Обрыв любого режима — переигровка дня целиком**; дедуп склеивает
|
||||
@@ -341,7 +336,7 @@
|
||||
|
||||
- *Отдельный топик / Kafka как хранилище дней*: офсеты и партиции вне
|
||||
обещания детерминизма, retention конечен, в git топик не положишь, хеш с
|
||||
описью не сверишь; Kafka на стенде — труба, не хранилище (раздел 7
|
||||
паспортом не сверишь; Kafka на стенде — труба, не хранилище (раздел 7
|
||||
мастер-спеки).
|
||||
- *Файл как обязательная станция доставки*: доигрывание обрыва уже решено
|
||||
через дедуп, канон держит единственный сериализатор, а не диск; файл
|
||||
@@ -360,27 +355,25 @@
|
||||
~2× среднего, будни/выходные; ≈8–12 тыс. сессий, 6–8 тыс. посетителей —
|
||||
правдоподобный средний магазин.
|
||||
- **Эталонный снимок совпадает со стартовым миром — 8 дней**: понедельник
|
||||
по понедельник, полная неделя и один закрытый день за окном K = 7.
|
||||
Обещание в 14 дней снято на этапе 7 (решение владельца 2026-08-30):
|
||||
у дней 8–13 не нашлось читателя — лабы проигрывают мир дальше без описи,
|
||||
транспорт байты с описью не сверяет, а проверке «пересгенерируй день N —
|
||||
сравни хеш» день 3 годится так же, как день 13. Замеры и тесты, меряющие
|
||||
мир на окне в две недели, не меняются: мир — функция без края, окну
|
||||
замера опись не нужна. Удлинение описи остается дешевым ходом
|
||||
(пересборка) и делается, когда читатель появится — например, если
|
||||
растяжке `classes_vs_inventory` понадобится сверять больше одного
|
||||
закрытого дня.
|
||||
- **В git — только опись, снимок не хранится.** Снимок генерируется при
|
||||
по понедельник, полная неделя и один закрытый день за окном K = 7. Обещание в
|
||||
14 дней снято на этапе 7 (решение владельца 2026-08-30): у дней 8–13 не
|
||||
нашлось читателя — лабы проигрывают мир дальше без паспорта, транспорт байты с
|
||||
паспортом не сверяет, а проверке «пересгенерируй день N — сравни хеш» день 3
|
||||
годится так же, как день 13. Замеры и тесты, меряющие мир на окне в две
|
||||
недели, не меняются: мир — функция без края, окну замера паспорт не нужен.
|
||||
Удлинение паспорта остаётся дешёвым ходом (пересборка) и делается, когда
|
||||
читатель появится — например, если растяжке `classes_vs_inventory` понадобится
|
||||
сверять больше одного закрытого дня.
|
||||
- **В git — только паспорт, снимок не хранится.** Снимок генерируется при
|
||||
создании мира и в проверках генератора: артефакт — кэш чистой функции, кэш в
|
||||
git не хранят. Опись несёт паспорт мира, счётчики и хеши по дням; проверки
|
||||
«пустой git diff» и «пересгенерируй день N — сравни хеш» живут на ней.
|
||||
Первый `make up` пустого стенда — живая демонстрация детерминизма; готовый мир
|
||||
повторный подъём не пересчитывает. Честная потеря —
|
||||
страховка на случай платформенного бага: раньше менти с расходящимися
|
||||
байтами мог взять готовый снимок из git, теперь он упрётся в красный чек
|
||||
описи; смягчение — дисциплина целочисленной случайности (раздел 2):
|
||||
расхождения amd64/arm64 сняты устройством, а не сторожем. CI ради одного
|
||||
прогона не заводится (решение владельца 2026-08-30).
|
||||
git не хранят. Паспорт несёт шапку, счётчики и хеши по дням; проверки «пустой
|
||||
git diff» и «пересгенерируй день N — сравни хеш» живут на нём. Первый `make
|
||||
up` пустого стенда — живая демонстрация детерминизма; готовый мир повторный
|
||||
подъём не пересчитывает. Честная потеря — страховка на случай платформенного
|
||||
бага: раньше менти с расходящимися байтами мог взять готовый снимок из git,
|
||||
теперь он упрётся в красный чек паспорта; смягчение — дисциплина целочисленной
|
||||
случайности (раздел 2): расхождения amd64/arm64 сняты устройством, а не
|
||||
сторожем. CI ради одного прогона не заводится (решение владельца 2026-08-30).
|
||||
- **Живой день — ×60 по умолчанию**: модельные сутки за 24 реальные минуты,
|
||||
суточная волна разворачивается на глазах; темп в среднем ~35 событий/с,
|
||||
в пиковые часы сильных дней — до ~100. Число —
|
||||
@@ -399,7 +392,7 @@
|
||||
| Операция | Ориентир |
|
||||
|---|---|
|
||||
| Генерация одного дня | 1,7–2,8 с (замер 7 августа 2026 года) |
|
||||
| Пересборка эталонного мира (8 дней + опись, без транспорта) | 20,5 с (замер 30 августа 2026 года) |
|
||||
| Пересборка эталонного мира (8 дней + паспорт, без транспорта) | 20,5 с (замер 30 августа 2026 года) |
|
||||
| Заливка стартового мира при `make up` (8 дней, Kafka → матвью → ODS) | 22,5 с (замер 7 августа 2026 года) |
|
||||
| Лаг живого дня | секунды |
|
||||
|
||||
@@ -460,9 +453,9 @@
|
||||
- **Раздел 1.4**: «из контракта выводятся DDL и валидация» заменено на data
|
||||
contract — хранилище пишется по документации, границу сторожит строгий
|
||||
приём (раздел 3 здесь).
|
||||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён описью;
|
||||
снимок генерируется на месте (раздел 5 здесь). Туман «политика
|
||||
версионирования артефакта» закрыт этим же ходом: версионируется опись.
|
||||
- **Раздел 8**: артефакт `data/startup_history/` в git заменён паспортом;
|
||||
снимок генерируется на месте (раздел 5 здесь). Туман «политика версионирования
|
||||
артефакта» закрыт этим же ходом: версионируется паспорт.
|
||||
- **Раздел 11**: пункт «до этапа 3 зафиксировать требования
|
||||
производительности» закрыт числами раздела 5.
|
||||
- Мелкие согласования там, где текст опирался на артефакт в git: источник
|
||||
@@ -491,24 +484,25 @@
|
||||
теперь есть.
|
||||
- **Этап 5 (Airflow)**: живой день со стороны хранилища — обычный ETL-даг
|
||||
по расписанию (~раз в 24 минуты); генератор не дорабатывается.
|
||||
- **Позиция на оси времени** (хвост отдан пульту мира раньше этапа 5 —
|
||||
- **Позиция на оси времени** (хвост отдан дагам модельного времени раньше этапа
|
||||
5 —
|
||||
[ADR 0009](../adr/0009-world-control.md))**.** Проигрыватель состояния не
|
||||
хранит (раздел 9), поэтому вести позицию — работа того, кто его зовёт. Живёт
|
||||
она переменной Airflow: отсутствие переменной означает, что стартовый мир
|
||||
ещё не создан. В обычной жизни её ставит даг пульта, который сыграл день, —
|
||||
она переменной Airflow: отсутствие переменной означает, что стартовый мир ещё
|
||||
не создан. В обычной жизни её ставит шаговый даг, который сыграл день, —
|
||||
значением «последний сыгранный + 1» и только по успеху. Даги жизненного цикла
|
||||
используют `-1` для незавершённой очистки, `0` для чистого мира до генерации
|
||||
и ставят первый несыгранный день только после приёма стартового мира
|
||||
([ADR 0013](../adr/0013-world-lifecycle.md)).
|
||||
Довод — генератор отдельная и заменяемая сущность, привязывать его к
|
||||
хранилищу незачем. Полный `make clean` сносит метаданные Airflow вместе с
|
||||
данными ClickHouse, а узкая пересборка явно сводит сохранённую позицию с
|
||||
заново залитым миром.
|
||||
Расхождение переменной с данными (менти почистил партицию руками) лечится
|
||||
документацией, а не сторожем: постоянная сверка была бы проверкой, красной
|
||||
в норме ([ADR 0002](../adr/0002-monitoring-scope.md)).
|
||||
- **Этап 7 (эталонный мир)**: пересборка — это опись, не артефакт.
|
||||
CI-генерация на amd64 и arm64 снята — резолюция в разделе 5.
|
||||
используют `-1` для незавершённой очистки, `0` для чистого мира до генерации и
|
||||
ставят первый несыгранный день только после приёма стартового мира ([ADR
|
||||
0013](../adr/0013-world-lifecycle.md)). Довод — генератор отдельная и
|
||||
заменяемая сущность, привязывать его к хранилищу незачем. Полный `make clean`
|
||||
сносит метаданные Airflow вместе с данными ClickHouse, а узкая пересборка явно
|
||||
сводит сохранённую позицию с заново залитым миром. Расхождение переменной с
|
||||
данными (менти почистил партицию руками) лечится документацией, а не сторожем:
|
||||
постоянная сверка была бы проверкой, красной в норме ([ADR
|
||||
0002](../adr/0002-monitoring-scope.md)).
|
||||
- **Этап 7 (эталонный мир)**: пересборка обновляет паспорт мира. Отдельного
|
||||
артефакта у неё нет. CI-генерация на amd64 и arm64 снята — резолюция в разделе
|
||||
5.
|
||||
- **Будущие лабы**: перезаливка дня X пакетным режимом проигрывателя —
|
||||
готовая демонстрация идемпотентности конвейера.
|
||||
|
||||
@@ -521,10 +515,10 @@
|
||||
[ADR 0017](../adr/0017-order-numbering.md). Новые решения генератора
|
||||
записывают в ADR и в спеке этапа, на котором их принимают.
|
||||
|
||||
Журнал собран по темам, но дата и тикет остались у каждого решения. По
|
||||
журналу видно, какие границы затрагивала каждая правка: иногда она меняла
|
||||
сразу план состава, воронку и каталог. При #40 это было дешево — опись еще
|
||||
не собрали; позже такой ход потребовал бы пересборки описи.
|
||||
Журнал собран по темам, но дата и тикет остались у каждого решения. По журналу
|
||||
видно, какие границы затрагивала каждая правка: иногда она меняла сразу план
|
||||
состава, воронку и каталог. При #40 это было дёшево — паспорт мира ещё не
|
||||
собрали; позже такой ход потребовал бы пересборки паспорта мира.
|
||||
|
||||
### План состава и числа мира
|
||||
|
||||
@@ -547,10 +541,10 @@
|
||||
покупателей ~190 в день, что дает конверсию ~2% на сессию.
|
||||
- **Конфигурация мира — модуль чистых данных** (#38, 2026-08-02) рядом с
|
||||
контрактом схемы: все числа мира собраны в одном месте, которое менти может
|
||||
крутить. Правка модуля меняет мир, и проверка описи честно краснеет: опись
|
||||
сторожит только канон. Вне модуля остается то, что мира не меняет, — свое
|
||||
зерно и транспортные флаги проигрывателя. Отклонено: внешний конфиг и
|
||||
переопределения окружением — переменная мира, которой нет в паспорте описи;
|
||||
крутить. Правка модуля меняет мир, и проверка паспорта честно краснеет:
|
||||
паспорт сторожит только канон. Вне модуля остается то, что мира не меняет, —
|
||||
свое зерно и транспортные флаги проигрывателя. Отклонено: внешний конфиг и
|
||||
переопределения окружением — переменная мира, которой нет в шапке паспорта;
|
||||
файл-конфиг в репозитории равен модулю по смыслу, но платит загрузчиком и
|
||||
валидацией (довод раздела 3 против YAML).
|
||||
- **Покупатель перестает быть словом — двумя рычагами сразу** (#40, решение
|
||||
@@ -749,8 +743,8 @@
|
||||
четвертям цены и по медиане, потому что одни только края списка обманываются
|
||||
четырьмя исключениями. Хвост соседям: словарь ClickHouse над этим файлом ещё
|
||||
не построен (#37/#43) — колонку надо взять в его описание сразу, а не догонять
|
||||
правкой; описи нужен хеш каталога (хвост #42) тем более, потому что теперь в
|
||||
файле живёт ещё и поведение.
|
||||
правкой; паспорту нужен хеш каталога (хвост #42) тем более, потому что теперь
|
||||
в файле живёт ещё и поведение.
|
||||
- **Доля просмотров карточек, доходящих до корзины, выросла с 7,3% до 8,6%**
|
||||
(#50, 2026-08-02) и подошла к потолку вилки. Визитам вне `/cart` нужны свои
|
||||
позиции, а визит с намерением должен класть почти все сравненное; обе границы
|
||||
@@ -764,7 +758,7 @@
|
||||
живых магазинов эта доля гуляет широко; пока она правдоподобна, сама по себе
|
||||
ничего не сторожит, и подгонять поведение под ее край не надо. Настоящий
|
||||
предел считается в другой валюте — дневной бюджет событий, на котором стоит
|
||||
опись #42. Если будущая правка упрется в него, двигать надо бюджет и его
|
||||
паспорт #42. Если будущая правка упрется в него, двигать надо бюджет и его
|
||||
причины, а не долю.
|
||||
|
||||
### Номер заказа, промокод и деньги
|
||||
@@ -933,28 +927,27 @@
|
||||
подъем указал бы внутрь `.venv`: каталог был бы в образе, но модуль его не
|
||||
нашел бы. Проверено в контейнере: день совпал побайтово с прогоном на машине.
|
||||
|
||||
### Опись мира и словарь
|
||||
### Паспорт мира и словарь
|
||||
|
||||
- **Опись мира одна** (#42, 2026-08-07; эталонная длина — 8 дней, этап 7,
|
||||
2026-08-30). Стартовый мир этапа 2 — первые восемь дней,
|
||||
понедельник по понедельник: полная неделя и первый замкнутый цикл K = 7.
|
||||
`data/world-inventory.json` — та самая опись, которую обещает раздел 5.
|
||||
Этап 7 оставил ее восьмидневной: у хвоста в шесть дней не нашлось читателя
|
||||
(резолюция — в разделе 5); вырастет она продолжением этого же файла, а не
|
||||
вторым. В JSON лежат
|
||||
паспорт мира — зерно, версия генератора и хеш каталога — и строки дней с
|
||||
датой, числом событий и хешем. Опись собирает `make inventory`, свежесть
|
||||
сторожит `test_inventory.py` тем же способом, что свежесть описания выгрузки.
|
||||
Отклонено: *два файла, младший и полный* — две правды об одном мире и лишнее
|
||||
слово в словаре.
|
||||
- **Паспорт мира один** (#42, 2026-08-07; эталонная длина — 8 дней, этап 7,
|
||||
2026-08-30). Стартовый мир этапа 2 — первые восемь дней, понедельник по
|
||||
понедельник: полная неделя и первый замкнутый цикл K = 7.
|
||||
`data/world-inventory.json` — тот самый паспорт, который обещает раздел 5.
|
||||
Этап 7 оставил его восьмидневным: у хвоста в шесть дней не нашлось читателя
|
||||
(резолюция — в разделе 5); вырастет он продолжением этого же файла, а не
|
||||
вторым. В JSON лежат шапка паспорта — зерно, версия генератора и хеш каталога
|
||||
— и строки дней с датой, числом событий и хешем. Паспорт собирает `make
|
||||
inventory`, свежесть сторожит `test_inventory.py` тем же способом, что
|
||||
свежесть описания выгрузки. Отклонено: *два файла, младший и полный* — две
|
||||
правды об одном мире и лишнее слово в словаре.
|
||||
- **Хеш каталога лежит в паспорте и объясняет расхождение** (#42, 2026-08-07).
|
||||
Правка цены меняет мир так же молча, как правка кода. Хеши дней и без
|
||||
каталожного хеша поймают смену мира; он отвечает на следующий вопрос — что
|
||||
менялось: разошлись дни и каталог — CSV, только дни — код.
|
||||
- **«Манифест» переименован в «опись мира», «зерновой мир» — в «стартовый мир»**
|
||||
(#42, решение владельца 2026-08-07). Обычные русские слова убирают пару
|
||||
«манифест / мини-манифест» и необходимость различать две сущности там, где
|
||||
вещь одна.
|
||||
(#42, решение владельца 2026-08-07; позднее тикет #214 сменил «опись мира» на
|
||||
«паспорт мира»). Обычные русские слова убирают пару «манифест / мини-манифест»
|
||||
и необходимость различать две сущности там, где вещь одна.
|
||||
|
||||
### Открытое на момент заморозки
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ DDL и двумя одноразовыми службами заливал во
|
||||
|
||||
Граница владения не совпадала с процессом, и это видно по следствиям. Успешная
|
||||
отправка в Kafka выдавалась за созданный мир: `world-init` завершался сразу
|
||||
после записи в топик, до приёма сообщений хранилищем.
|
||||
Повторный `make up` смешивал подъём служб с повторной заливкой данных. Узкая
|
||||
пересборка хранилища сохраняла в Kafka сообщения и офсеты прежнего мира, потому
|
||||
что чинила только ClickHouse. Позиция пульта правилась снаружи Airflow — тем,
|
||||
кто миром не управляет.
|
||||
после записи в топик, до приёма сообщений хранилищем. Повторный `make up`
|
||||
смешивал подъём служб с повторной заливкой данных. Узкая пересборка хранилища
|
||||
сохраняла в Kafka сообщения и офсеты прежнего мира, потому что чинила только
|
||||
ClickHouse. Позиция на оси правилась снаружи Airflow — тем, кто миром не
|
||||
управляет.
|
||||
|
||||
**Чему учит переезд.** Менти видит один наблюдаемый граф, которому принадлежат
|
||||
прикладная схема, данные мира и позиция модельного времени, — и Compose,
|
||||
@@ -63,7 +63,7 @@ Airflow держит прикладные топики Kafka, каноничес
|
||||
|
||||
Каталог `/opt/airflow/world` — то, что нужно для создания мира: `generator/`,
|
||||
`data/` и корневой `.dockerignore` смонтированы только для чтения. Из первого
|
||||
собирается канонический образ генератора, из второго читается опись мира, а
|
||||
собирается канонический образ генератора, из второго читается паспорт мира, а
|
||||
последний не пускает в контекст сборки локальные окружения и кэши. Запросы
|
||||
стенда приезжают прежним монтированием `sql/`.
|
||||
|
||||
@@ -101,7 +101,7 @@ Kafka, `SQLExecuteQueryOperator` для исполняемых SQL-файлов
|
||||
кода: сборкой образа при подъёме больше никто не занимается.
|
||||
5. Играет стартовые дни одним прогоном `batch` того же образа.
|
||||
6. Ждёт, пока события доедут до ODS. Условие ожидания то же, что у прежнего
|
||||
сценария, и оно точное: сумма строк `ods.event_dist` за даты описи и строк
|
||||
сценария, и оно точное: сумма строк `ods.event_dist` за даты паспорта и строк
|
||||
`ods.event_errors_dist` не меньше суммы событий из
|
||||
[`data/world-inventory.json`](../../data/world-inventory.json). «Что-то
|
||||
приехало» условием не считается.
|
||||
@@ -136,21 +136,21 @@ Kafka, `SQLExecuteQueryOperator` для исполняемых SQL-файлов
|
||||
третьим местом с той же последовательностью, а первое же расхождение между
|
||||
дагами пришлось бы прятать в его параметрах.
|
||||
|
||||
### Сторожа у работников мира
|
||||
### Сторожа у шаговых дагов
|
||||
|
||||
`world_next_day` и `world_live_day` краснеют, если позиции нет либо она меньше
|
||||
`WORLD_STARTING_DAYS`. Отказ называет прочитанное значение и следующий шаг:
|
||||
поднять стенд или пересобрать хранилище. Выключатель `world_live` своего
|
||||
сторожа не получает — он краснеет вслед за работником.
|
||||
поднять стенд или пересобрать хранилище. Выключатель `world_live` своего сторожа
|
||||
не получает — он краснеет вслед за шаговым дагом.
|
||||
|
||||
`orders_ingest` сторожа не получает вовсе: его зовёт сама инициализация, когда
|
||||
готового мира ещё нет.
|
||||
|
||||
## Состояния позиции
|
||||
|
||||
`world_position` остаётся единственным состоянием пульта.
|
||||
`world_position` остаётся единственным состоянием дагов модельного времени.
|
||||
|
||||
| Позиция | `world_initialize` | `world_recreate` | Работники |
|
||||
| Позиция | `world_initialize` | `world_recreate` | Шаговые даги |
|
||||
|---|---|---|---|
|
||||
| переменной нет | создаёт мир | очищает и создаёт | красный отказ |
|
||||
| `-1` | красный отказ: очистка не завершена, повторите `make rebuild-storage` | очищает и создаёт | красный отказ |
|
||||
@@ -187,9 +187,9 @@ Kafka, `SQLExecuteQueryOperator` для исполняемых SQL-файлов
|
||||
ClickHouse, они состоят в группах потребителей, и удаление группы недопустимо.
|
||||
Поэтому сначала удаляются базы, затем топики.
|
||||
|
||||
**Позицию удалили руками на живом мире.** Даг честно зальёт мир заново: сырьё
|
||||
в STG удвоится, ODS схлопнет повтор дедупликацией. Условие ожидания на непустом
|
||||
ODS истинно сразу — этим оно и ограничено, обещание «доехала именно эта
|
||||
**Позицию удалили руками на живом мире.** Даг честно зальёт мир заново: число
|
||||
строк STG удвоится, ODS схлопнет повтор дедупликацией. Условие ожидания на
|
||||
непустом ODS истинно сразу — этим оно и ограничено, обещание «доехала именно эта
|
||||
заливка» ни один счёт дать не может. Сторожа на этот случай нет: ошибку менти
|
||||
делает себе сам и тут же видит.
|
||||
|
||||
@@ -197,10 +197,10 @@ ODS истинно сразу — этим оно и ограничено, об
|
||||
`world_initialize`, тот ждет `etl_pipeline`, а конвейер ждет слой. В живом
|
||||
режиме такую же цепочку образуют `world_live`, `world_live_day`, `etl_pipeline`
|
||||
и слой. В обоих случаях три ожидающие задачи оставляют слою последний слот.
|
||||
Одновременная пересборка добавляет ожидания и загоняет стенд в тупик.
|
||||
Поэтому пересборка — явная операция обслуживания: оператор ставит `world_live`
|
||||
на паузу и даёт работникам доиграть. Предусловие остаётся в документации;
|
||||
механизм блокировок стоил бы дороже этой строки.
|
||||
Одновременная пересборка добавляет ожидания и загоняет стенд в тупик. Поэтому
|
||||
пересборка — явная операция обслуживания: оператор ставит `world_live` на паузу
|
||||
и даёт шаговым дагам доиграть. Предусловие остаётся в документации; механизм
|
||||
блокировок стоил бы дороже этой строки.
|
||||
|
||||
## Границы
|
||||
|
||||
@@ -226,9 +226,9 @@ ODS истинно сразу — этим оно и ограничено, об
|
||||
позиция та же, счётчики `check-clickhouse` те же, в журнале дага сказано, что
|
||||
мир уже есть.
|
||||
- **`make rebuild-storage`** возвращает канонический мир: позиция проходит
|
||||
`-1` → `0` → `WORLD_STARTING_DAYS`, в ODS ровно даты описи, топики созданы
|
||||
`-1` → `0` → `WORLD_STARTING_DAYS`, в ODS ровно даты паспорта, топики созданы
|
||||
заново, офсеты групп идут с нуля, DDS и DM собраны.
|
||||
- **Красные пути проверяются своими отказами.** Сторож работника при
|
||||
- **Красные пути проверяются своими отказами.** Сторож шагового дага при
|
||||
отсутствующей, нулевой и отрицательной позиции; `world_initialize` на `-1`;
|
||||
оборванная очистка, оставляющая `-1`. Проверка, не видевшая своей поломки,
|
||||
доказывает только то, что умеет печатать «ЗЕЛЁНО».
|
||||
|
||||
@@ -104,14 +104,14 @@
|
||||
|
||||
### Ступень 6. Разбери сырые события и сверь результат со стендом
|
||||
|
||||
Менти пакетно разбирает срез сырья событий в собственную таблицу и построчно
|
||||
Менти пакетно разбирает срез событий из STG в собственную таблицу и построчно
|
||||
сверяет результат с нетронутым стендовым ODS. Урок — изучить разбор потока
|
||||
отдельно от живого приема и использовать стендовый результат как образец для
|
||||
сверки. Матвью разбора остаются несущей стеной: их пересоздание молча теряет
|
||||
сообщения.
|
||||
|
||||
Форма — «собери рядом». Обратная связь — построчная сверка двух разборов. Ее
|
||||
границы называются прямо: срок работы ограничен окном хранения сырья, а
|
||||
границы называются прямо: срок работы ограничен окном хранения сырых данных, а
|
||||
содержимое неопорных колонок можно сравнить только со стендовым разбором, не с
|
||||
независимым эталоном.
|
||||
|
||||
@@ -150,7 +150,8 @@
|
||||
- **Какая позиция объясняет разницу в сумме.** Расхождение суммы нужно
|
||||
разложить до конкретной вычеркнутой позиции и объяснить до копейки.
|
||||
- **Был ли отмененный заказ оплачен.** Текущее состояние не заменяет историю, а
|
||||
некоторые различия после истечения срока сырья уже не восстановить.
|
||||
некоторые различия после истечения срока хранения сырых данных уже не
|
||||
восстановить.
|
||||
- **Когда отчет за день можно считать окончательным (#188).** Отчет за день
|
||||
становится окончательным лишь после того, как источник больше не может
|
||||
изменить этот день.
|
||||
@@ -191,7 +192,8 @@
|
||||
потребителей создает еще одну полную копию потока.
|
||||
- **Как заметить пропущенные сообщения по офсетам.** Разрыв в офсетах
|
||||
показывает потерю, которую трудно заметить по самим данным.
|
||||
- **Почему сырье удаляется по времени загрузки.** Очистка сырья живет по
|
||||
- **Почему сырые данные удаляются по времени загрузки.** Очистка сырых данных
|
||||
идёт по
|
||||
времени загрузки, а не по модельному дню.
|
||||
- **Куда попадают битые строки.** Годные строки и брак делят поток без
|
||||
пропусков и пересечений; менти подкладывает по строке каждого класса брака
|
||||
|
||||
Reference in new issue
Block a user