- Зачем:
- на живом дашборде видно то, чего не показывает API: при нескольких
запросах в панели Grafana ставит перед именем ряда его букву — «A события»,
«B приземлилось в ClickHouse». Читателю панели это шум.
- Что:
- свежесть и поток собраны одним запросом на панель: два столбца вместо двух
запросов. У потока это заодно вернуло ряд заказов, который пропадал, когда
в окне не было ни одного слепка.
- у сходимости ряды из разных источников слить нельзя, поэтому ряду
ClickHouse имя задано переопределением поля.
- Проверка:
- make config-test; на живом стенде подписи прочитаны прямо со страницы
дашборда: «события», «заказы», «произведено в Kafka», «приземлилось в
ClickHouse»; все шесть панелей отвечают через /api/ds/query.
- Зачем:
- две линии ревью нашли, что колонки панели брака читаются как
непересекающиеся кучи, «вовремя ли» из ADR 0002 не считается вовсе, а
отказ от ряда «против вчерашнего» нигде не записан рядом с решением.
- Что:
- в панели брака столбец назван «Из них в брак»: брак — часть приехавшего,
а не соседняя куча.
- панель слепка считает паузу после прошлого слепка — это и есть «вовремя
ли» из ADR 0002; пустая пауза у самого раннего слепка держится
обнуляемым типом.
- отказ от ряда «против вчерашнего» записан в ADR 0002 и в monitoring.md с
доводом: сдвиг на модельные сутки прописал бы ускорение мира в JSON.
- в monitoring.md сказано, почему свежесть читает физическую _dist, а не
ods.*_v, и почему панель слепка не знает окна; довод про сломанный JSON
оставлен у кода, а не задвоен в доке.
- README больше не утверждает, что панелей нет.
- у разбора дашбордов убран флаг -e: он обещал утверждение, которого нет.
- Проверка:
- make config-test (краснеет на нарочно сломанном JSON дашборда), make lint,
make smoke 21/21, make check-clickhouse 10/10; все шесть панелей отвечают
через /api/ds/query. Опыты приёмки и их следы — monitoring.md,
«Что проверено».
- Зачем:
- главный дашборд ADR 0002 отвечает на вопрос «с потоком всё в порядке?»;
три его вопроса — свежесть, брак и сходимость — у предшественника не
заданы вовсе, и в этом весь смысл экрана.
- Что:
- добавлен infra/grafana/provisioning/dashboards/data.json: свежесть, поток,
брак, сходимость, отставание чтения из Kafka и дневной слепок заказов;
порогов и цветовых вердиктов на панелях нет.
- в monitoring.md записано устройство дашборда: почему слои у панелей разные
и как считается сходимость — с доводом и двумя отвергнутыми способами.
- make config-test разбирает JSON дашбордов тем же доводом, что и файлы
дагов: сломанный файл Grafana молча оставляет прежнюю версию.
- Проверка:
- make config-test; на живом стенде make smoke 21/21 и make check-clickhouse
10/10; все шесть панелей отвечают через /api/ds/query.
- Зачем:
- подготовлен пол мониторинга для SQL-панелей и метрик отставания Kafka.
- Что:
- добавлены отдельная роль ClickHouse, источник Grafana и Kafka exporter.
- образ Grafana собирается с закреплённым плагином вне тома данных.
- Prometheus, смоук и документация обновлены под четыре цели.
- Проверка:
- make config-test; make lint.
- make smoke; make check-clickhouse; make check-services.
- make -C generator lint typecheck test.
- Зачем:
- дашборды нужны точками наблюдения на приёмке этапов 4-8, а зависимость
у них формально только от этапа 1: «в хвосте» было выбором, не
необходимостью.
- Что:
- раздел 9: дашборды строятся параллельно этапам 4-8, состав не меняется.
- назван хвост этапа: runbook keeper/DDL, system.text_log, правила
оповещения, урок «посмотрели - сломали - вернули».
- записано правило: панели порогов и цветовых вердиктов не несут, красное
и зелёное живёт в проверках.
- Проверка:
- тикеты этапа нарезаны по этому раскрою: #114-#117 под #11.
- Зачем:
- локальные уточнения документации нужно опубликовать без потери слитых изменений #96 и #109.
- Что:
- актуальный `origin/main` объединён с двумя локальными коммитами документации.
- сохранены уточнения применения настроек ClickHouse и границ проектирования заказов.
- Проверка:
- `make config-test`.
- `make lint`.
- Зачем:
- ODS должен показывать сущность заказа, а не способ её доставки слепком.
- Что:
- физические таблицы и представление переведены на имена `order_*`.
- SQL загрузки, DAG и документация приведены к тому же словарю.
- Проверка:
- `make config-test`, `make lint`.
- `make clean && make up`, `make smoke`, `make check-clickhouse`, `make check-services`.
- Зачем:
- свежий стенд должен показывать отставание ночной выгрузки заказов от кликстрима.
- Что:
- после событий генератор отправляет семь слепков дней 0…6 в общий топик orders.
- make up запускает orders_ingest, ограниченно ждёт его завершения и оставляет день 7 первому ходу мира.
- README и карта проверок описывают второй источник и измеренную цену подъёма.
- Проверка:
- make clean && make up; make smoke; make check-clickhouse; make check-services.
- cd generator && make lint && make typecheck && make test.
- Зачем:
- состав полей DDS определяется вместе с моделью, поэтому отдельная задача заранее не нужна.
- Что:
- удалена задача #85 из списка открытых решений по заказам.
- убран устаревший указатель на неё из описания приёма.
- Проверка:
- выполнен `git diff --cached --check`.
- Зачем:
- перезапуск мог оставить прежнее окружение и старый inode файла, а документы обещали неподдерживаемое живое применение.
- Что:
- в README записано пересоздание нод с ожиданием их здоровья.
- ADR 0007 отделяет решение от процедуры и очищен от устаревших обещаний.
- Проверка:
- make config-test.
- git diff --cached --check.
- Зачем:
- слепок D7 содержал отрицательный итог заказа и попадал в брак ODS.
- Что:
- скидка пересчитана от корзины после удаления отсутствующей позиции.
- сериализатор отклоняет отрицательные деньги через ValueError.
- добавлены проверки восьми стартовых дней и обновлена опись мира.
- Проверка:
- make lint, make typecheck и make test в generator/.
- живой world_next_day: 1694 строки приняты, таблица ошибок пуста.
Зачем: ревью в три линии нашло настоящий дефект. Девятая проверка кластера
звала dictGet('dds.products', …) — объекта после переноса не существует, и
make check-clickhouse падал. Промах вырос из недосчёта: упоминания dds.products
я искал с фильтром по расширениям, .sh туда не попал, и в ADR уехало «четыре
упоминания» вместо семи.
Что: проверка переведена на dic.products и сверяет цену, умножив её обратно на
сто, — так утверждается ещё и точность приведения к Decimal. Раздел 3
мастер-спеки и список пользователей README догнали перенос: оба описывали
отменённое устройство. В ADR 0012 добавлено условие пересмотра для дома — для
него его не было, хотя ради дома тикет и заводился; недосчёт записан в
«Что проверено» как урок. Разнобой обозначений сведён: карта таблиц в доке
хранилища перешла на имена баз строчными, таблица мастер-спеки — на заголовок
«Где», зону всюду зовут зоной, а не слоем. Термин «Справочник» переписан без
метафоры и уложен в формат глоссария. Вычтено лишнее: комментарии DDL
сократились вдвое, из ADR 0006 и 0007 убраны самооправдание и дублирующие
абзацы, отраслевой образец получил честную оговорку о непроверяемости.
Проверка: make config-test, make lint, make smoke и make check-clickhouse —
все зелёные, десять проверок кластера из десяти.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: горячее ревью поймало ошибку в трёх местах сразу. Было записано, что
после правки каталога ноды расходятся до тридцати секунд — число взято из
ширины окна LIFETIME(MIN 60 MAX 90). Ширина тут ни при чём: каждая нода ждёт
правки от нуля до верхней границы окна, фазы у них независимы, поэтому
расходиться они могут почти на все полторы минуты.
Что: исправлены ADR 0012, опорная точка мастер-спеки и комментарий DDL. Заодно
уточнено, что LIFETIME(0) неатомарность не предотвращал, а сжимал до разброса
исполнения SYSTEM RELOAD DICTIONARY ON CLUSTER, и что витрина выручки пока
спроектирована, а не построена.
Проверка: рассуждением, замером не подтверждалось — прежнее число тоже было
выведено, а не измерено. Замер в ADR (116 секунд на окне MIN 60 MAX 120)
относится к другому утверждению: что словарь обновляется сам.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: каждое утверждение о словаре было записано трижды — в доке хранилища, в
комментариях DDL и в ADR 0012. Правка поведения стоила бы трёх согласованных
правок, а расхождение между копиями обнаружилось бы не сразу. Дока хранилища —
карта, а не пересказ реализации.
Что: раздел «Справочники» сведён к своему уровню — правило принадлежности зоны,
топология на кластере, единицы денег и указатели. Механика (движок, форма пути,
пользователь, окно обновления) осталась только в sql/ddl/05-dic-catalog.sql,
рядом с кодом, который она объясняет. Блок «Что проверено» отправлен к замерам
ADR 0012 вместо их пересказа — так же, как он уже поступает с ADR 0005.
Проверка: минус 30 строк; ссылок на убранный подзаголовок в репозитории нет,
три относительные ссылки нового текста разрешаются в существующие файлы.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: место словаря в dds было унаследовано от первого объекта, а не решено.
Справочник читают несколько слоёв, а производит его в хранилище ни один —
прописка внутри одного из потребителей приписывала DDS владение, которого у
него нет.
Что: заведена база dic вне цепочки STG → ODS → DDS → DM, dds.products переехал
в dic.products. Под словарём появилась подложка dic.products_file на движке
File — намеренное усложнение ради урока: в бою источник словаря приезжает
процессом, а не лежит файлом у сервера. Источник объявлен формой query и
приводит цену из целых копеек каталога в Decimal(18, 2), как у денег бэкенда.
Обновление — окном LIFETIME(MIN 60 MAX 90) вместо ручной перезагрузки, ценой
заявленной неатомарности между нодами. У словаря свой беспарольный
пользователь dict с единственным правом на чтение dic. Суффикс _file добавлен
в конвенцию имён ADR 0006. Решение, отвергнутые варианты и условия пересмотра
— ADR 0012.
Проверка: make clean && make up на собранном заново стенде — зелено, make smoke
20 проверок и 0 ошибок, make lint чисто. Словарь LOADED со 180 строками, цена
Decimal(18, 2) и точна на копейках: 188990 → 1889.90. Пользователь bi читает
словарь dictGet-ом со второй ноды, analyst — подложку соединением, база dds
пуста. Минимальное право на движок замерено тремя пользователями: достаточно
GRANT FILE ON *.*, хотя отказ называет TABLE ENGINE ON File.
Closes#105
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- витринам нужен общий с генератором каталог, а менти — пример штатного обновления словаря.
- Что:
- каталог смонтирован read-only в обе ноды и создан как dds.products ON CLUSTER.
- роли получили dictGet, а check-clickhouse сверяет ответ под etl с исходным CSV.
- источник, раскладка и явная политика обновления записаны в документации.
- Проверка:
- make up с чистыми томами; make lint; make config-test; make smoke; make check-clickhouse.
- в generator: make lint typecheck test — 427 тестов прошли.
- Зачем:
- преобразования хранилища должны читаться рядом с целевым слоем, а DAG должен показывать оркестрацию.
- Что:
- запросы забора и разбора заказов разложены по каталогам STG и ODS.
- общий контракт провода подключён в обе ветви штатным шаблонизатором Airflow.
- SQL смонтирован во все службы Airflow, а правило раскладки записано в архитектуре.
- Проверка:
- make lint config-test smoke check-services.
- airflow tasks render для pull_batch и parse_batch; airflow tasks test для parse_batch.
- Зачем:
- менти должен различать версию заказа, наблюдение источника и запуск загрузки.
- Что:
- добавлены таблицы версий и брака заказов, а также представление ods.order_v.
- даг orders_ingest дополнен строгим переходом одного среза STG в две цели ODS.
- результаты опытов записаны в документации, дефект генератора вынесен в #102.
- Проверка:
- make lint config-test smoke check-clickhouse check-services.
Зачем:
- связать проигрывание модельных дней с пакетным приёмом заказов
- показать на одном стенде различие потокового push и пакетного pull
Что:
- добавлен топик, Kafka-чтец и реплицированное сырьё заказов
- добавлен даг orders_ingest с одним прямым чтением и идентификатором загрузки
- работники мира отправляют слепок, ждут приём и только затем двигают позицию
- решения, границы отказа и проверки отражены в ADR и архитектурных документах
Проверка:
- make lint
- make config-test
- make smoke
- make check-clickhouse
- make check-services
- Зачем:
- задача #92 добавляет второй источник и показывает слепок как чистую функцию зерна и дня.
- Что:
- добавлены окно, состояние на границе суток и канонические байты с настоящими миллисекундами.
- добавлены команда snapshot, сдвиг D → D−1 и переиспользование проигранных дней.
- опись дополнена хешами слепков и поведенческими тестами.
- Проверка:
- в generator выполнены make lint, make typecheck и make test: 427 тестов.
- Зачем:
- issue #91 требует решать исход и расхождение заказа в день его рождения.
- Что:
- добавлены три исхода, моменты оплаты и отмены и дельта одной позиции.
- обновлены проверки повторяемости и архитектурное описание заказов.
- Проверка:
- cd generator && make lint && make typecheck && make test (418 passed).
- Зачем:
- холодное ревью Кодексом (линия дефектов) показало дыру в проверке
анонимности: личность, уехавшая строкой в сыром `ecommerce`, проходила
мимо сравнения числовых колонок, и тест оставался зелёным. Дыра
воспроизведена: user_id, дописанный в блок покупки, тест не замечал.
- Что:
- тест анонимности спрашивает двумя способами: числовые колонки дня — со
всеми личностями аудитории, строки покупок целиком — с личностями
покупателей. Сырой блок собирается руками, и лишнее поле в нём теперь
красит проверку.
- Проверка:
- из `generator/`: make lint, make typecheck, make test (417 тестов).
- мутация «дописать user_id в ecommerce покупки»: до правки тест зелёный,
после — красный.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- тикет #90: второй источник должен смотреть на торговую половину дня
глазами бэкенда — заказ есть проекция покупки, а не второе порождение;
учебный результат — два источника согласованы по построению, а не
сверкой.
- Что:
- `commerce` отдаёт вторым выходом покупки дня: номер заказа, корзину,
выручку клиента, промокод и человека за кукой; новых бросков в
подпоток `COMMERCE` не добавилось, события дня не сдвинулись.
- заведена заказная сторона: подпоток `ORDERS` на позиции 2 дерева зерна
(прежнее мёртвое имя `DISCREPANCIES`), ветвление — по дню рождения
заказа; `LATECOMERS` не тронут, его наполнит этап 6.
- новый модуль `orders.py`: деньги заказа целыми копейками —
`items_total` из корзины, `discount` по таблице «код → скидка»
(округление вниз), `delivery` броском по таблице весов на полную длину
дня, `total` = `items_total` − `discount` + `delivery`.
- план состава завёл `person_id` — последним броском когорты, после
паспортов: вторая кука пары повторяет ID своего человека, заказ
показывает его как `user_id`, в событие Метрики он не попадает.
- `world`: окно изменяемости `ORDER_WINDOW_DAYS` = 7 рядом с D0 и поясом,
черновая таблица стоимости доставки.
- слепок дня в тестах сторожит обе половины: заказы сравниваются наравне
с потоком событий.
- доки: имя подпотока заказной стороны и состав денег заказа записаны в
`docs/architecture/orders`, README генератора знает про новый модуль.
- Проверка:
- из `generator/`: make lint, make typecheck, make test (417 тестов);
в корне — make lint.
- опись мира не покраснела: байты восьми дней те же, заказная сторона
события не сдвинула.
Closes#90
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- Зачем:
- потребителю ODS нужна точная актуальная версия события без знания о FINAL.
- Что:
- создано ods.event_v над распределённой таблицей событий с FINAL.
- сверка стартового мира переведена на новое представление.
- документация различает физические версии ODS и бизнес-представление DDS.
- Проверка:
- make config-test.
- make check-clickhouse.
Зачем: черновик этапов держал DDS заказа в этапе 3, а принятый набор
docs/architecture/orders/ оставляет модель DDS проектированию этапа 4
(ingestion.md «Не входит», тикет #85 перевешен на #6) — расхождение
всплыло на холодном ревью нарезки этапа (#89).
Что: в разделе 9 этап 3 сужен до STG/ODS и дага приёма, модель DDS
заказа названа работой этапа 4.
Проверка: чтение; нарезка этапа 3 (#90–#96) согласована с этой строкой.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Зачем: датированная спека — событие истории, а устройство компонента — живой
документ; большое полотно плохо грузится и агентом, и человеком (ADR 0011).
Что: вычитание #84 слито с переустройством формы: набор
docs/architecture/orders/ — индекс README и семь файлов по частям устройства
(нарезка по правилу «семь плюс-минус два»); датированные файлы удалены,
ссылки перенацелены, AGENTS.md дополнен правилом подпапки. Приёмка
владельцем #88 пройдена, черновой статус снят из README.
Проверка: холодная сверка миграции свежим тредом — потерь решений нет;
обход относительных ссылок набора — битых нет.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Зачем: локальные рабочие файлы агентов (хэндоффы, обменные артефакты ревью)
живут в репозитории, чтобы переживать перезагрузку, но в git им не место.
Что: .scratch/ в .gitignore.
Проверка: git status не показывает .scratch/.
Зачем: решения развилок #70–#74, #80, #81 карты #69 разошлись по резолюциям
тикетов — цельная картина второго источника нужна в одном месте.
Что: черновик спеки заказов docs/specs/2026-08-16-orders.md и спека приёма
docs/specs/2026-08-16-order-ingestion.md; согласующие правки соседних
документов; правки горячего ревью и двух слепых холодных линий — дефекты
сборки закрыты, назван порядок строк внутри слепка.
Проверка: перепроверка находок теми же ревьюерами — ALL_CLOSED.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Зачем: определение описи разошлось с файлом, который называет. Формула
«счётчики закрытых окон с названным читателем» не покрывает счёт событий по
дням, а он в описи лежит и сегодня, и читатель у него самый живой: make
check-clickhouse сверяет по нему приход в ods.event.
Что: счёт событий вернулся в определение строкой дня; «паспорт» снова
раскрыт скобкой — двумя термами выше стоит «Паспорт куки», и голое слово с
ним сталкивалось; вернулось «единственное, что о мире хранится в git» —
определяющее свойство описи, а не следствие. Строка про слепок и правило
«счётчик только с названным читателем» сохранены.
Проверка: определение сверено с data/world-inventory.json и с резолюцией
«Места заказов в стартовом мире» (#74).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- документация не должна обещать постоянные числа для движущегося мира.
- Что:
- уточнено появление опоздавших заказов в слепках.
- опись ограничена неизменяемым выходом и счётчиками с читателем.
- счётчик пар оставлен плану, но исключён из описи.
- Проверка:
- CONTEXT.md перечитан целиком; git diff --check.
Зачем: взгляд владельца на генератор жил только в разговорах, и каждый
следующий агент восстанавливал его заново или не восстанавливал вовсе.
Без него решения по генератору принимаются не по тому критерию: внутренняя
стройность вместо правдоподобия данных на выходе.
Что: новый раздел «Чем меряется генератор» сразу после «Зачем» — пять
утверждений рамки. Генератор меряется выходом, а не устройством; менти
внутрь не смотрит, поэтому конструкция оправдана только наблюдаемым
эффектом; повторимы эффекты, а не числа, и хеши описи обещают видимость
сдвига, а не постоянство чисел; цена ошибки мала, поэтому оборона дороже
ошибки не заводится; сложность изолирована службой ради дешевизны правки.
Рамка шире этапа 2, поэтому спеки следующих этапов ссылаются на неё, а не
копируют.
Проверка: правка только текстовая, кода не касается.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Зачем:
- генератору нужен единый язык для скрытой личности и наблюдаемых идентификаторов.
- Что:
- определены человек, посетитель и пользователь магазина.
- закреплена граница между ClientID кликстрима и user_id заказа.
- Проверка:
- документ перечитан целиком.
Зачем: линия дефектов нашла опору на дефект провайдера, линия постановки —
незаписанный ответ на вопрос ADR 0009 и переменную образца, чьё значение на
чужой машине неверно, а узнаёт об этом читатель через двести строк.
Что: `auto_remove` у прогона генератора переведён с `success` на `force`.
Значение `success` тоже убирало контейнер в любом исходе, но случайно —
удаление у провайдера 4.5.7 стоит в `finally` вопреки собственной
документации; `force` то же поведение называет прямо и переживёт починку.
У выключателя назван `catchup=False`: умолчание Airflow 3 то же самое, но
у дага с тиком в 25 минут и `start_date` в январе это первый вопрос
читателя. «Быстрый старт» предупреждает про `DOCKER_GID` — единственное
значение образца, неверное вне этой машины. В образе Airflow записано, что
провайдер docker приходит с базой (4.5.7 к 3.3.0), — это ответ на вопрос,
который ADR 0009 оставил тикету. Список томов планировщика получил ту же
пометку «правя одно, правьте второе», что стоит у числа дней. Проход на
вычитание срезал три комментария, пересказывавших ADR.
Проверка: `make lint`, `make config-test`, `make smoke` (20/0),
`make check-clickhouse` (9/9) — зелёные. Живой пульт на чистом стенде:
снятый с паузы `world_live` сыграл два дня подряд без нажатия (24 м 07 с и
24 м 06 с, пауза между ними 55 секунд), дочерний прогон виден ссылкой из
задачи выключателя; пауза остановила мир на границе суток — тик прошёл,
третьего прогона нет. Контейнер после прогона с `force` не остался.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: модельный день прогонялся только руками, и владельцу нечем было
проверять процессы стенда вживую. Пульт нужен раньше этапа 3 и независимо
от него: он обкатывает то, что приёму заказов понадобится готовым — вызов
генератора из задачи Airflow.
Что: `dags/world_control.py` — три дага по ADR 0009. Работники
`world_next_day` (день пачкой, «сколько дней» параметром) и `world_live_day`
(день в темпе) живут без расписания и без паузы; выключатель `world_live`
создаётся на паузе, тикает раз в 25 минут и дёргает работника живого дня
с ожиданием конца. Генератор зовётся `DockerOperator` в каноническом
контейнере: сокет докера отдан планировщику, потому что при LocalExecutor
задачи исполняет он, а GID группы `docker` уехал в `.env` как локальная
настройка. Позицию на оси ведёт переменная `world_position` — её ставит
сыгравший день работник и только по успеху. Факты стенда — образ, сеть,
брокер, топик, размер стартового мира — даги получают окружением от compose;
внутри compose они названы по разу якорями, иначе разошлись бы с разовой
службой генератора. README получил раздел про пульт с названной вслух платой
за сокет.
Проверка: `make lint`, `make config-test`, `make smoke` (20/0), `make
check-services` (7/0), `make check-clickhouse` (9/9) — зелёные. На чистом
стенде: два прогона `world_next_day` подряд двигают позицию на два дня,
«дней = 3» — на три, все пять дней доехали в ODS; обрыв контейнера позицию
не двигает, повторный запуск играет тот же день с тем же счётом событий.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Зачем: модельный день прогоняется только руками, и владельцу нечем
проверять процессы стенда вживую. Этап 3 без дага не существует вовсе
(ADR 0008), поэтому форму пульта и способ вызова генератора надо решить
раньше кода.
Что: ADR 0009 — работники без расписания (`world_next_day` пачкой,
`world_live_day` в темпе) и выключатель `world_live` без своей работы,
ждущий дочерний прогон триггером, а не сенсором; генератор зовётся
`DockerOperator` в каноническом контейнере, потому что в образе Airflow ему
не жить (Python 3.14 против 3.13); позиция ставится как «последний сыгранный
+ 1» — правило вместо сторожа, и его мягкий худший исход назван. Плата за
сокет докера снята замером: доступ открывается добавлением GID в контейнер,
правка прав на хосте не нужна, а сам GID — локальная настройка в `.env`.
Пакетная автоматика отложена, не отвергнута. Расхождения внесены в спеку
генератора: позицию ставит сыгравший, у живого дня появился выключатель.
В словарь добавлен «пульт мира».
Проверка: `make config-test` зелёный; API Airflow 3.3 (пауза и ручной
запуск, ожидание триггера по идентификатору прогона, привязка сенсора к
логической дате, `DockerOperator`) сверено через MCP Context7; версия
Python в образе и права сокета — замером.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>