diff --git a/AGENTS.md b/AGENTS.md
index 4b11367..594c75e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -81,6 +81,9 @@ Airflow) и названия из кода. Если для понятия ес
писал: простые слова, короткие фразы, сложную мысль поясняй при первом
упоминании. Если понятность и буквальная точность спорят — выбирай понятность.
+При создании или редактировании лабораторных следуй
+[правилам учебного текста](docs/specs/2026-09-01-labs-program.md#как-писать-задания).
+
Внутренние рассуждения и промежуточные пометки по ходу работы веди на
английском — он экономнее по токенам. На русском остаётся всё, что видит и
хранит проект: итоговые ответы, документы, комментарии в коде и SQL, сообщения
diff --git a/docs/labs/1-1-events-enriched.md b/docs/labs/1-1-events-enriched.md
index 3de404a..0a72405 100644
--- a/docs/labs/1-1-events-enriched.md
+++ b/docs/labs/1-1-events-enriched.md
@@ -1,7 +1,7 @@
# Задание 1: `dm.events_enriched_v`
-**Чему учит:** строить слой как договор о данных, а не как их копию.
-Оставлять витрину представлением, если зерно источника не меняется.
+**Чему учит:** подготовить удобный для анализа набор полей события
+и обойтись представлением, сохранив одну строку на событие.
## Сначала прочитать
@@ -9,7 +9,7 @@
- [Как применить свой DDL](README.md#как-применить-свой-ddl): файл для решения
и подключение с правами на создание витрины.
-## Роль витрины
+## Для чего нужна витрина
В `dds.event_v` 44 колонки, включая технические поля источника и массивы
товаров. Для анализа событий в Superset нужны деловые поля и три готовых
@@ -88,15 +88,10 @@ DDS читают из ODS без отдельной загрузки. Эти с
Отдельно сверьте `event_hour`, `page_path` и `referer_host` с теми же
выражениями, рассчитанными прямо по `dds.event_v`.
-Если строк в витрине в разы меньше, чем в источнике, и остались только
-события корзины и покупки, это признак развернутых массивов: `ARRAY JOIN`
-исключил все события с пустым массивом товаров.
+## Что должно получиться
-## Приемка
-
-- [ ] представление `dm.events_enriched_v` создано без физических таблиц;
-- [ ] список колонок в представлении задан явно;
-- [ ] число строк и набор `event_id` совпадают с `dds.event_v` за каждый
- прожитый день;
-- [ ] задача `events_enriched` в `homework_check` завершилась успешно;
-- [ ] задачи для этой витрины в `dm_transform` нет.
+- [ ] у вас есть `dm.events_enriched_v` с явно перечисленными колонками;
+- [ ] вы сверили число строк и набор `event_id` с `dds.event_v` за каждый
+ прожитый день; расхождений нет;
+- [ ] вы запустили `events_enriched` в `homework_check`, проверка успешна;
+- [ ] витрина работает без физической таблицы и задачи в `dm_transform`.
diff --git a/docs/labs/1-2-top-pages-daily.md b/docs/labs/1-2-top-pages-daily.md
index 5a1bd41..b936435 100644
--- a/docs/labs/1-2-top-pages-daily.md
+++ b/docs/labs/1-2-top-pages-daily.md
@@ -1,14 +1,14 @@
# Задание 2: `dm.top_pages_daily`
-**Чему учит:** создавать витрину с новым зерном: пять объектов, донор и
-замена дневных партиций. Включать ее пересборку в `dm_transform`.
+**Чему учит:** собрать дневные показатели страниц и настроить их
+обновление в `dm_transform` через замену партиций.
## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
-## Роль витрины
+## Для чего нужна витрина
По дневным показателям страниц аналитик сравнивает просмотры, посетителей и
действия с товарами. Добавление в корзину происходит на карточке товара,
@@ -98,13 +98,13 @@
Сумма `visitors` по страницам повторно учитывает посетителя, открывшего
несколько страниц. Число посетителей за день берите из `dm.daily_traffic_v`.
-## Приемка
+## Что должно получиться
-- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
- цели;
-- [ ] в `dm.top_pages_daily_v` явно перечислены колонки, включая `_load_id`
- и `_load_ts`;
-- [ ] объем прогона определяется по состоянию витрины без ручного задания;
-- [ ] задача `top_pages_daily` в `homework_check` завершилась успешно;
-- [ ] задача витрины добавлена в `dm_transform`, повторный прогон конвейера
- не меняет результат.
+- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
+ использует тот же ключ шардирования, что и целевая таблица;
+- [ ] вы явно перечислили колонки в `dm.top_pages_daily_v`, включая
+ `_load_id` и `_load_ts`;
+- [ ] ваш запрос сам определяет дни пересборки по состоянию витрины;
+- [ ] вы запустили `top_pages_daily` в `homework_check`, проверка успешна;
+- [ ] вы включили витрину в `dm_transform` и убедились, что повторный
+ прогон конвейера не меняет результат.
diff --git a/docs/labs/1-3-session-overview.md b/docs/labs/1-3-session-overview.md
index c2d2961..c3ee54f 100644
--- a/docs/labs/1-3-session-overview.md
+++ b/docs/labs/1-3-session-overview.md
@@ -1,24 +1,21 @@
# Задание 3: `dm.session_overview`
-**Чему учит:** хранить слагаемые, а не готовые средние и доли. Понимать,
-почему сумма `visitors` по строкам сейчас сходится, но начнет завышать
-аудиторию, если у одной куки появятся визиты с двух устройств.
+**Чему учит:** хранить слагаемые для расчета средних и долей и понимать,
+когда сложение числа посетителей по устройствам завышает аудиторию.
## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
-## Роль витрины
+## Для чего нужна витрина
Витрина нужна, чтобы по дням сравнивать визиты с телефона и компьютера: их
число, длительность, просмотры страниц и частоту покупок.
-На прошлом стенде витрина выделяла визиты сама. Теперь их рассчитывают в
-DDS, а повторять зерно сессий ради представления с переименованными
-колонками незачем. Здесь вы учитесь дневной агрегации, поэтому зерно — день
-и устройство. Выбор описан в разделе «Форма: по зерну, не по цене запроса»
-[справочника DM](../architecture/dm.md).
+Сессии уже рассчитаны в DDS. Из них вы соберете дневные показатели по
+устройствам. Почему такие итоги хранят в отдельной таблице, объясняет
+[справочник DM](../architecture/dm.md).
## Что сделать
@@ -107,12 +104,12 @@ DDS, а повторять зерно сессий ради представле
повторного учета визитов;
- сумма `events_total` равна сумме `events_total` по сессиям того же дня.
-## Приемка
+## Что должно получиться
-- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
- цели;
-- [ ] в `dm.session_overview_v` колонки перечислены явно;
-- [ ] источник — `dds.session_v`, повторного выделения визитов нет;
-- [ ] физическая таблица не хранит средние и доли;
-- [ ] задача `session_overview` в `homework_check` завершилась успешно;
-- [ ] задача витрины добавлена в `dm_transform`.
+- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
+ использует ключ шардирования целевой таблицы;
+- [ ] вы явно перечислили колонки в `dm.session_overview_v` и собрали
+ показатели из готовых сессий `dds.session_v`;
+- [ ] в таблице хранятся слагаемые для средних и долей;
+- [ ] вы запустили `session_overview` в `homework_check`, проверка успешна;
+- [ ] вы включили сборку витрины в `dm_transform`.
diff --git a/docs/labs/1-4-dq-errors-daily.md b/docs/labs/1-4-dq-errors-daily.md
index 13e2bff..ab5c35e 100644
--- a/docs/labs/1-4-dq-errors-daily.md
+++ b/docs/labs/1-4-dq-errors-daily.md
@@ -1,15 +1,14 @@
# Задание 4: `dm.dq_errors_daily`
**Чему учит:** считать брак по дню загрузки и сохранять сводку дольше
-исходных строк. Пересборка всех партиций после удаления источника по `TTL`
-стирает историю, которую уже не восстановить.
+исходных строк.
## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
-## Роль витрины
+## Для чего нужна витрина
Сводку открывают при росте брака в потоке: смотрят, каких ошибок стало
больше и сколько строк забраковано. Причину конкретной ошибки исследуют
@@ -97,17 +96,16 @@
`ods.event_errors_dist` и `ods.order_errors_dist`. Если она примерно вдвое
меньше, вероятная причина — чтение `_rep` вместо `_dist`.
-Пустая витрина на чистом стенде — верный ответ: обе стороны пусты,
-расхождений ноль, проверка успешна. Требование хотя бы одной строки дало бы
-постоянную ошибку, которую со временем стали бы игнорировать.
+Если в источниках нет брака, витрина должна быть пустой. В этом случае
+расхождений тоже ноль и проверка успешна.
-## Приемка
+## Что должно получиться
-- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
- цели;
-- [ ] источники — таблицы `_dist` обоих видов брака;
-- [ ] прогон заменяет партиции только дней, еще имеющихся в источниках, и
- сохраняет дни за пределами их срока хранения;
-- [ ] задача `dq_errors_daily` в `homework_check` завершилась успешно, в том
- числе на чистом стенде без брака;
-- [ ] задача витрины добавлена в `dm_transform`.
+- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
+ использует ключ шардирования целевой таблицы;
+- [ ] вы собираете сводку из таблиц `_dist` для ошибок событий и заказов;
+- [ ] ваш запрос пересобирает только дни, еще доступные в источниках,
+ сохраняя более старые итоги;
+- [ ] вы запустили `dq_errors_daily` в `homework_check`, проверка успешна;
+ отсутствие брака тоже дает успешный результат;
+- [ ] вы включили сборку витрины в `dm_transform`.
diff --git a/docs/labs/1-5-utm-effectiveness.md b/docs/labs/1-5-utm-effectiveness.md
index 3853828..4eeb157 100644
--- a/docs/labs/1-5-utm-effectiveness.md
+++ b/docs/labs/1-5-utm-effectiveness.md
@@ -1,15 +1,14 @@
# Задание 5: `dm.utm_effectiveness`
-**Чему учит:** понимать предупреждение в приставке `declared_`: это сумма со
-слов клиента, а не деньги отчетности. Самостоятельно находить причины
-расхождения двух сумм выручки.
+**Чему учит:** различать сумму покупки со слов клиента (`declared_`) и
+серверную выручку, находить причины их расхождения.
## Сначала прочитать
- [«Хранилище и его слои»](../handbook/storage-layers.md);
- [«Потоки Airflow и даги модельного времени»](../handbook/airflow-dags.md).
-## Роль витрины
+## Для чего нужна витрина
Аналитик сравнивает рекламные каналы по дням: трафик, добавления в корзину,
покупки и заявленные клиентом суммы.
@@ -44,7 +43,7 @@
| `_load_ts` | `DateTime64(3, 'UTC')` | Время сборки |
Для `declared_revenue` сложите `purchase_revenue[1]` событий `purchase`. В
-параллельных массивах `purchase*` по контракту хранятся один номер заказа и
+параллельных массивах `purchase*` хранятся один номер заказа и
одна сумма на событие. В сверке `purchase_vs_orders` используют тот же
индекс.
@@ -127,12 +126,12 @@
Сумма `visitors` по строкам повторно учитывает одного посетителя,
приходящего по разным меткам. Для числа посетителей дня она не подходит.
-## Приемка
+## Что должно получиться
-- [ ] созданы пять объектов, донор объявлен через `AS` и шардируется ключом
- цели;
-- [ ] сохранены строки без UTM-меток;
-- [ ] `declared_revenue` хранится в `Decimal(18, 2)`;
-- [ ] расхождение с `dm.revenue_daily_v` объяснено письменно;
-- [ ] задача `utm_effectiveness` в `homework_check` завершилась успешно;
-- [ ] задача витрины добавлена в `dm_transform`.
+- [ ] у вас есть пять объектов витрины; донор объявлен через `AS` и
+ использует ключ шардирования целевой таблицы;
+- [ ] вы сохранили строки без UTM-меток и используете `Decimal(18, 2)`
+ для `declared_revenue`;
+- [ ] вы записали, почему сумма отличается от `dm.revenue_daily_v`;
+- [ ] вы запустили `utm_effectiveness` в `homework_check`, проверка успешна;
+- [ ] вы включили сборку витрины в `dm_transform`.
diff --git a/docs/labs/2-1-superset-chart.md b/docs/labs/2-1-superset-chart.md
index c300da1..22da07d 100644
--- a/docs/labs/2-1-superset-chart.md
+++ b/docs/labs/2-1-superset-chart.md
@@ -8,7 +8,7 @@
- [«Немного Superset»](../handbook/superset.md);
- [задание 3: `dm.session_overview`](1-3-session-overview.md).
-## Роль графика
+## На какой вопрос ответит график
В задании 3 вы собрали витрину `dm.session_overview_v`. Теперь покажите, с
каких устройств чаще уходят после первой страницы. Постройте столбиковый
@@ -60,8 +60,8 @@ FROM dm.session_overview_v;
## Как проверить себя
-График проверяется глазами, поэтому задачи для него в `homework_check` нет.
-В SQL Lab выполните запрос:
+Автоматической проверки этого задания нет. Сверьте числа на графике
+с результатом запроса в SQL Lab:
```sql
SELECT device_category,
@@ -76,15 +76,16 @@ ORDER BY device_category;
проценты с одним знаком, как формат `.1%`, поэтому числа должны совпасть
один в один.
-## Приемка
+## Что должно получиться
-- [ ] создан физический датасет `dm.session_overview_v` в подключении
+- [ ] у вас есть физический датасет `dm.session_overview_v` в подключении
`ClickHouse`;
-- [ ] в датасете есть метрика «Доля отказов» с выражением
+- [ ] вы сохранили в нем метрику «Доля отказов» с выражением
`SUM(bounce_sessions) / SUM(sessions)` и форматом `.1%`;
-- [ ] для `device_category` и `report_date` заданы русские имена «Категория
+- [ ] вы назвали `device_category` и `report_date` по-русски: «Категория
устройства» и «Дата отчета»;
-- [ ] сохранен столбиковый график доли отказов по категориям устройств без
+- [ ] у вас сохранен столбиковый график доли отказов по категориям устройств без
относительного фильтра по времени;
-- [ ] график сверен с запросом из SQL Lab;
-- [ ] стендовые объекты не изменены, дашборд не создан.
+- [ ] вы сверили числа на графике с запросом из SQL Lab;
+- [ ] вы сохранили график отдельно, без создания дашборда и изменения
+ стендовых объектов.
diff --git a/docs/labs/3-1-sessions-by-hand.md b/docs/labs/3-1-sessions-by-hand.md
index f991ad5..58ca3b8 100644
--- a/docs/labs/3-1-sessions-by-hand.md
+++ b/docs/labs/3-1-sessions-by-hand.md
@@ -1,14 +1,14 @@
-# Нарежь сессии сам
+# Нарежьте сессии сами
-**Чему учит:** собрать сессии из событий и сверить их число с внешним
-эталоном. Найти причину расхождения в отдельных строках.
+**Чему учит:** собрать сессии из событий, сравнить их число с `VisitID`
+источника и найти события, из-за которых расчеты расходятся.
## Сначала прочитать
- [Хранилище и его слои, раздел DDS](../handbook/storage-layers.md#dds-язык-бизнеса);
- [формат события: Идентификаторы и время](../formats/clickstream-event.md#идентификаторы-и-время).
-## Роль сессии
+## Для чего нужны сессии
Визит, или сессия, объединяет действия посетителя за один заход на сайт.
По визитам аналитик считает длительность и глубину просмотра.
@@ -71,19 +71,22 @@ ORDER BY EventDate, ClientID;
## Как проверить себя
-Результат задания проверяется глазами, поэтому задачи в `homework_check`
-для него нет. Таблицы и представления создавать не нужно.
Сохраните исходный и уточненный расчеты, результаты сверки и найденные строки.
+Таблицы и представления создавать не нужно. Задачи в `homework_check`
+для этого задания нет: вы проверяете расчет своим запросом.
-**Предупреждение.** В `sql/dds/session_rebuild.sql` и
+В `sql/dds/session_rebuild.sql` и
`sql/dq/sessions_vs_reference.sql` лежат готовые запросы нарезки и сверки.
Откройте их после собственного решения и расследования. Сравните запросы
-со своими: копирование готового SQL пропускает расследование.
+со своими.
Сопоставление со стендовой нарезкой по `session_id` остается на ступень 4.
-## Приемка
+## Что должно получиться
-- [ ] запрос нарезает все события `ods.event_v` за все доступные дни;
-- [ ] сверка через `FULL OUTER JOIN` дает сводку по дням и список кук;
-- [ ] при разборе расхождения указаны кука, день, два события и причина;
-- [ ] уточненный расчет дает ноль расхождений во всех днях.
+- [ ] ваш запрос собирает сессии из всех событий `ods.event_v` за все
+ доступные дни;
+- [ ] у вас есть сводка расхождений по дням и список кук, полученные через
+ `FULL OUTER JOIN`;
+- [ ] вы объяснили причину одного расхождения и записали куку, день и два
+ события, на которых оно видно;
+- [ ] вы уточнили расчет и получили ноль расхождений во всех днях.
diff --git a/docs/labs/4-1-session-pipeline.md b/docs/labs/4-1-session-pipeline.md
index 61ce3c1..7671f2c 100644
--- a/docs/labs/4-1-session-pipeline.md
+++ b/docs/labs/4-1-session-pipeline.md
@@ -1,16 +1,16 @@
-# Перепиши нарезку стенда
+# Перепишите нарезку стенда
-**Чему учит:** изменить расчетную сущность внутри готового конвейера и
-проверить результат внешним эталоном.
+**Чему учит:** переписать расчет сессий в готовом конвейере и проверить
+результат по исходным событиям.
## Сначала прочитать
-- [задание «Нарежь сессии сам»](3-1-sessions-by-hand.md);
+- [задание «Нарежьте сессии сами»](3-1-sessions-by-hand.md);
- [модель сессии DDS](../architecture/dds/session.md);
- [дневной конвейер трансформаций](../architecture/etl.md#объём-прогона);
- [раскладка по шардам](../architecture/storage.md#раскладка-по-шардам).
-## Граница задания
+## Что предстоит сделать
Даг, таблицы сессий и проверка их числа уже созданы.
Перепишите только три файла:
@@ -24,7 +24,7 @@
служебная таблица, где день собирается перед заменой. В
`sessions_vs_reference.sql` число сессий сравнивается с `VisitID` из ODS.
-## Договор с дагом
+## Как даг запускает ваши запросы
Объем прогона — дни, которые соберет один запуск. Его вычисляет первый файл.
@@ -74,7 +74,7 @@ ClickHouse в `ALTER ... PARTITION` здесь не работает.
Состав и типы строки заданы в `dds.session_rep`. Заполните начало, конец и
длительность. Посчитайте все события, просмотры, добавления в корзину и
покупки. Возьмите входную и выходную страницы, UTM первого события, устройство
-и город. Служебные колонки тоже входят в договор таблицы.
+и город. Заполните также служебные колонки из схемы таблицы.
Пишите через `dds.session_stage_dist`: место строки определит ключ
`client_id`. При записи прямо в локальную таблицу оно зависело бы от ноды,
@@ -178,15 +178,14 @@ Distributed-таблицу и такую ошибку не заметит.
партицию. Расчет объема должен пропустить группу сессий: новых прожитых дней и
дыр в слое нет.
-## Приемка
+## Что должно получиться
-- [ ] переписаны только три файла `sql/dds/session_*.sql`, инфраструктура
- упражнения не изменена;
-- [ ] запуск после удаления партиции восстановил выбранный день и завершил
- `etl_pipeline` успешно;
-- [ ] сохраненные `session_id`, границы, длительность и счетчики десяти строк
- совпали после пересборки;
-- [ ] `sessions_vs_reference` дает ноль расхождений во всех прожитых днях;
-- [ ] для одной куки вручную сверены границы и атрибуты сессии;
-- [ ] пересчет ключа по `hostName()` не нашел строк на чужом шарде;
-- [ ] повторный прогон без новой дыры пропустил группу сессий.
+- [ ] вы переписали три файла `sql/dds/session_*.sql`, сохранив даг,
+ таблицы и проверку стенда;
+- [ ] ваш расчет восстановил удаленный день, `etl_pipeline` завершился успешно;
+- [ ] вы сверили десять сохраненных строк: `session_id`, границы,
+ длительность и счетчики совпали после пересборки;
+- [ ] вы получили ноль расхождений в `sessions_vs_reference` за все
+ прожитые дни и вручную сверили границы и атрибуты сессии одной куки;
+- [ ] ваш запрос с `hostName()` не нашел строк на чужом шарде;
+- [ ] вы повторили прогон без удаления данных: он пропустил группу сессий.
diff --git a/docs/labs/5-1-order-checks.md b/docs/labs/5-1-order-checks.md
index e73bcc2..48f0c9b 100644
--- a/docs/labs/5-1-order-checks.md
+++ b/docs/labs/5-1-order-checks.md
@@ -1,7 +1,7 @@
-# Сначала сторож, потом ломай: заказы
+# Проверьте заказы перед изменением обработки
-**Чему учит:** до рискованного изменения задавать проверке явный договор и
-испытывать его управляемой ошибкой.
+**Чему учит:** написать проверки заказов до изменения их обработки
+и убедиться на намеренной ошибке, что проверки ее замечают.
## Сначала прочитать
@@ -13,30 +13,30 @@
и на ее включение в [`dq_check`](../../dags/dq_check.py). Готового ответа для
двух проверок из этого задания в репозитории нет.
-## Граница задания
+## Что предстоит сделать
-Добавьте два сторожа:
+Добавьте две проверки и включите их в даг:
- `sql/dq/order_items_total.sql` проверяет сумму позиций заказа;
- `sql/dq/orders_vs_ods.sql` сверяет DDS с его источником;
- `dags/dq_check.py` запускает обе проверки, публикует их строки в
`dm.dq_summary` и создает для них отдельные задачи.
-На этом шаге не меняйте разбор ODS и сборку DDS. Даги, DDL, таблицы-доноры,
-Kafka и службы остаются несущей частью упражнения.
+Разбор ODS и сборку DDS вы перепишете в следующих заданиях. Здесь сохраните
+их, даги загрузки, DDL, таблицы-доноры, Kafka и службы как есть.
Перед опытами остановите автоматическое движение мира: поставьте `world_live`
-на паузу и дождитесь завершения активных прогонов. До чистой точки остановки не
+на паузу и дождитесь завершения активных прогонов. До завершения опытов не
запускайте руками `world_next_day`, `world_live_day`, `world_initialize` и
`world_recreate`. Это условие стабильного модельного времени: одной паузы
`world_live` для него мало.
-## Два договора качества
+## Что будут проверять ваши запросы
-Обе проверки следуют договору `dq_check`: запрос возвращает по одной строке
+Чтобы `dq_check` мог запустить проверку, ее запрос возвращает по одной строке
на день с колонками `data_date`, `reference_rows`, `actual_rows`,
`failed_rows`, `diagnostics`. Даг сохраняет в `dm.dq_summary` только счетчики,
-статус и координаты запуска. Ограниченные примеры из `diagnostics` ищите в
+статус и координаты запуска. Примеры из `diagnostics` ищите в
журнале упавшей задачи Airflow. Ограничьте их тем же малым числом, что и
существующие запросы в `sql/dq/`.
@@ -46,17 +46,18 @@ Kafka и службы остаются несущей частью упражн
посчитайте сумму `item_price * item_quantity` и сравните ее с `items_total`.
`reference_rows` считает ключи со стороны `items_total`, `actual_rows` - ключи
со стороны вычисленной суммы, а `failed_rows` - несовпавшие значения. В
-исправном наборе первые два счетчика равны по построению; сам инвариант выражен
-через `failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и
+исправном наборе первые два счетчика равны; несовпадения сумм считает
+`failed_rows`. Для диагностики достаточно `order_id`, ожидаемой суммы и
суммы позиций.
Проверка относится к типизированному разбору массива `items` на границе
-ODS -> DDS. Скидка, доставка и равенство `total` в этот договор не входят.
+ODS -> DDS. Скидку, доставку и равенство `total` здесь проверять не нужно.
### DDS против ODS
Имя проверки - `orders_vs_ods`. Источник модели DDS здесь - текущее состояние
-`ods.order_v`. Внутреннее состояние генератора в этот договор не входит.
+`ods.order_v`. С ним вы и сравниваете DDS; обращаться к внутреннему состоянию
+генератора для этой проверки не нужно.
Одним запросом соберите обе стороны на зерне дня заказа и сравните:
- число заказов;
@@ -131,18 +132,17 @@ ORDER BY check_name;
Проверка суммы должна охватывать каждый день `dds.order_v`. Сверка слоев
должна охватывать объединение дней ODS и DDS, включая дни только одной стороны.
-## Когда можно остановиться
+## Завершите опыт
-После двух восстановлений выбранный заказ снова совпадает с ODS. Все семь
-проверок дают зеленый результат. В таком состоянии можно закончить работу или
+После восстановления заказа и успешного `dq_check` можно закончить работу или
перейти к разбору заказов, оставив модельное время на паузе.
-## Приемка
+## Что должно получиться
-- [ ] две новые проверки встроены в `dq_check` и публикуют дневные строки по
- принятому договору;
-- [ ] каждая проверка дала красный результат на своей управляемой ошибке;
-- [ ] журналы упавших задач содержали ограниченную диагностику;
-- [ ] `etl_pipeline` восстановил обе измененные строки DDS;
-- [ ] `order_items_total`, `orders_vs_ods` и пять прежних DQ зелены;
-- [ ] SQL разбора ODS и сборки DDS на этом шаге не менялся.
+- [ ] вы включили две новые проверки в `dq_check`; их дневные результаты
+ появляются в `dm.dq_summary`;
+- [ ] вы увидели ошибку каждой проверки после соответствующего изменения
+ заказа, а в журнале нашли примеры заказов с расхождениями;
+- [ ] вы восстановили заказ через `etl_pipeline` после обоих опытов;
+- [ ] `order_items_total`, `orders_vs_ods` и пять прежних проверок успешны;
+- [ ] разбор ODS и сборка DDS остались прежними.
diff --git a/docs/labs/5-2-order-parsing.md b/docs/labs/5-2-order-parsing.md
index 1662017..fb20274 100644
--- a/docs/labs/5-2-order-parsing.md
+++ b/docs/labs/5-2-order-parsing.md
@@ -1,7 +1,7 @@
# Перепишите разбор заказов в ODS
-**Чему учит:** сначала измерить обе ветви разбора на управляемом срезе, затем
-переписать переход и повторить тот же опыт.
+**Чему учит:** переписать разбор заказов и на пробных сообщениях проверить,
+что годные строки и брак попадают в свои таблицы без потерь.
## Сначала прочитать
@@ -11,9 +11,9 @@
- [`orders_ingest`](../../dags/orders_ingest.py);
- [`order_parse.sql`](../../sql/ods/order_parse.sql).
-Готового запроса для сверки управляемого среза в репозитории нет.
+Запрос для проверки пробных строк вы напишете сами.
-## Граница задания
+## Что предстоит сделать
Сначала проведите опыт на готовом `order_parse.sql`. Затем удалите его SQL,
соберите разбор заново и повторите опыт с другим маркером.
@@ -72,7 +72,7 @@ jq -cn --arg marker "$LAB_MARKER" --arg day "$LAB_DAY" '
' > "$LAB_BATCH"
```
-Отправьте файл в обычный топик заказов штатным производителем контейнера:
+Отправьте файл в обычный топик заказов утилитой из контейнера Kafka:
```bash
docker compose exec -T kafka /opt/kafka/bin/kafka-console-producer.sh \
@@ -123,7 +123,7 @@ SETTINGS mutations_sync = 2;
В ClickHouse 26.3 мутации выполняются в фоне. С `mutations_sync = 2` команда
ждет их завершения на всех репликах. Запросами к трем Distributed-таблицам
-докажите, что строк маркера не осталось, затем удалите временный файл.
+убедитесь, что строк маркера не осталось, затем удалите временный файл.
Группа `clickstream_orders` уже подтвердила эти сообщения, поэтому обычный
повтор `orders_ingest` их не прочитает. В Kafka они остаются до удаления по
@@ -137,8 +137,8 @@ SETTINGS mutations_sync = 2;
## Перепишите разбор
-Удалите готовый SQL из `sql/ods/order_parse.sql` и сохраните его договор с
-`orders_ingest`:
+Удалите готовый SQL из `sql/ods/order_parse.sql` и напишите свой. Чтобы
+`orders_ingest` мог его использовать, сохраните следующие условия:
- вход - строки `stg.orders_raw_dist` одного `{load_id:String}`;
- каждая строка среза попадает ровно в один результат: годный заказ или брак;
@@ -161,21 +161,20 @@ SETTINGS mutations_sync = 2;
шард. Затем очистите второй срез теми же адресными командами и запустите
`dq_check`.
-## Когда можно остановиться
+## Завершите опыт
-Оба маркера отсутствуют в STG, ODS, таблице брака и DDS; временные файлы
-удалены. Семь DQ дают зеленый результат. В таком состоянии можно перейти к
+Убедитесь, что оба маркера отсутствуют в STG, ODS, таблице брака и DDS,
+временные файлы удалены, а семь проверок успешны. Теперь можно перейти к
сборке DDS, не снимая паузу с модельного времени. Если вы прерываетесь и до
опыта `world_live` работал, снимите его с паузы.
-## Приемка
+## Что должно получиться
-- [ ] до переписывания зафиксированы баланс 4 = 1 + 3, три класса брака,
- деловые поля текущей версии и фактический шард;
-- [ ] переписан только `sql/ods/order_parse.sql`, несущая инфраструктура не
- изменена;
-- [ ] после переписывания новый срез дал тот же результат;
-- [ ] обе годные строки различали `created_at` и `updated_at`;
-- [ ] проверки `hostName()` не нашли строки ODS на чужом шарде;
-- [ ] строки обоих опытов удалены только по своим маркерам;
-- [ ] после очистки общий конвейер и семь DQ находятся в зеленом состоянии.
+- [ ] у вас сохранены результаты до переписывания: четыре строки дали
+ один заказ и три класса брака; вы сверили поля текущей версии и шард;
+- [ ] вы переписали `sql/ods/order_parse.sql`, сохранив остальные части стенда;
+- [ ] ваш разбор дал тот же результат на новой пробной пачке;
+- [ ] вы проверили разные значения `created_at` и `updated_at` у годной
+ строки в обоих опытах и подтвердили правильный шард через `hostName()`;
+- [ ] вы удалили строки обоих опытов по их маркерам;
+- [ ] после очистки конвейер и все семь проверок успешны.
diff --git a/docs/labs/5-3-order-pipeline.md b/docs/labs/5-3-order-pipeline.md
index cd5b867..e45f340 100644
--- a/docs/labs/5-3-order-pipeline.md
+++ b/docs/labs/5-3-order-pipeline.md
@@ -1,7 +1,7 @@
# Перепишите сборку заказов в DDS
-**Чему учит:** переписать пересборку заказов по состоянию слоев и доказать ее
-результат на пропущенном закрытом дне.
+**Чему учит:** настроить пересборку заказов по состоянию ODS и DDS
+и проверить, что она восстанавливает пропущенный день за пределами текущего окна.
## Сначала прочитать
@@ -11,7 +11,7 @@
- [дневной конвейер](../architecture/etl.md#объём-прогона);
- [раскладку по шардам](../architecture/storage.md#раскладка-по-шардам).
-## Граница задания
+## Что предстоит сделать
Удалите готовый SQL и перепишите только три файла:
@@ -19,8 +19,8 @@
- `sql/dds/order_rebuild.sql`;
- `sql/dds/order_replace.sql`.
-Даг `dds_transform`, DDL, донор, ODS и обе проверки из первого задания уже
-задают границу работы. Перед началом убедитесь, что пробные строки предыдущего
+Даг `dds_transform`, DDL, донор, ODS и обе проверки из первого задания
+оставьте как есть. Перед началом убедитесь, что пробные строки предыдущего
задания удалены, а семь DQ дают зеленый результат.
Поставьте `world_live` на паузу и дождитесь завершения активных прогонов. До
@@ -28,7 +28,7 @@
`world_recreate`. `world_next_day` понадобится только в явно указанном ниже
случае.
-## Договор с дагом
+## Как даг запускает ваши запросы
Сохраните интерфейс `dds_transform`:
@@ -103,22 +103,22 @@ DROP PARTITION 'YYYY-MM-DD';
заказов снова должна пересобрать текущее изменяемое окно. Контрольные суммы
деловых полей остаются прежними, а все семь проверок дают зеленый результат.
-## Когда можно остановиться
+## Завершите опыт
-Удаленная партиция восстановлена, ее число строк и контрольная сумма совпали
-с исходными. На чужих шардах строк нет, повторный прогон не изменил деловые
-данные, семь DQ зелены. Если до упражнения `world_live` работал, теперь его
-можно снять с паузы.
+После восстановления партиции и успешных проверок снимите `world_live`
+с паузы, если он работал до упражнения.
-## Приемка
+## Что должно получиться
-- [ ] переписаны только три файла `sql/dds/order_*.sql`, инфраструктура
- упражнения не изменена;
-- [ ] удален ближайший к окну закрытый день, а красный DQ зафиксирован до
+- [ ] вы переписали три файла `sql/dds/order_*.sql`, сохранив остальные
+ части стенда;
+- [ ] вы удалили ближайший к окну закрытый день и увидели ошибку DQ до
восстановления;
-- [ ] `first_day` начался с удаленного дня, задачи замены покрыли непрерывный
- отрезок до конца окна;
-- [ ] число заказов и контрольная сумма восстановленного дня совпали;
-- [ ] `order_items_total`, `orders_vs_ods` и пять прежних DQ зелены;
-- [ ] проверка `hostName()` не нашла заказов DDS на чужом шарде;
-- [ ] повторный `etl_pipeline` сохранил деловые данные и зеленые проверки.
+- [ ] ваш расчет выбрал `first_day` с удаленного дня и заменил партиции
+ всего отрезка до конца окна;
+- [ ] вы сверили число заказов и контрольную сумму восстановленного дня
+ с сохраненными значениями; они совпали;
+- [ ] `order_items_total`, `orders_vs_ods` и пять прежних проверок успешны;
+- [ ] ваш запрос с `hostName()` не нашел заказов на чужом шарде;
+- [ ] вы повторили `etl_pipeline`: деловые данные остались прежними,
+ проверки прошли.
diff --git a/docs/labs/6-1-event-parsing.md b/docs/labs/6-1-event-parsing.md
index b817729..0fb0027 100644
--- a/docs/labs/6-1-event-parsing.md
+++ b/docs/labs/6-1-event-parsing.md
@@ -15,7 +15,7 @@
Готовый стендовый разбор находится в `sql/ddl/30-ods-views.sql`. Откройте его
после собственной сверки: в нем лежит ответ на основную часть задания.
-## Граница задания
+## Что предстоит сделать
Создайте в базе `default` одну свою таблицу годных событий и наполните ее одним
пакетным `INSERT SELECT` из `stg.hits_raw_dist`. Работайте с первой нодой под
@@ -72,11 +72,10 @@ Airflow. При включенном `world_live` посмотрите его т
`ods.event_rep`, а также `_load_ts`. Тип `purchaseRevenue` возьмите из ODS:
`Array(Decimal(18, 2))`.
-Схлопните повтор доставки по договору ODS. Возьмите
+Обработайте повторные доставки так же, как ODS. Возьмите
`ReplacingMergeTree(_load_ts)`, партицию `EventDate` и полный ключ замены:
`(CounterID, EventDate, intHash32(ClientID), WatchID)`. Достаточно одной
-локальной таблицы: отдельная Distributed-таблица не добавит урока к пакетному
-опыту на одной ноде.
+локальной таблицы: загрузка и сверка в этом опыте выполняются на одной ноде.
`WatchID` — логический идентификатор события, уникальный по
[договору генератора](../specs/2026-08-01-generator.md). Физический ключ ODS
@@ -85,8 +84,7 @@ Airflow. При включенном `world_live` посмотрите его т
## Разберите годные строки
-Для каждой строки среза примите решение по тому же строгому входу, что и
-стенд:
+Для каждой строки среза проверьте те же условия приема, что и на стенде:
1. корень `raw` — JSON-объект;
2. набор верхнеуровневых ключей в точности равен 47 ключам формата;
@@ -95,25 +93,25 @@ Airflow. При включенном `world_live` посмотрите его т
Напишите разбор, который не прерывает пакет из-за одной грязной строки. Сами
выберите способ безопасного извлечения пяти опорных полей, форму общих выражений
-и место фильтра. Остальные поля извлеките по договору формата.
+и место фильтра. Остальные поля извлеките по описанию формата.
Перенесите `_load_ts` из STG без пересчета: это время исходной доставки и
колонка версии. Время запуска вашего запроса сюда не входит. Вставьте в свою
таблицу только годные строки выбранного среза.
-Считайте отвергнутые строки прямо в исходном срезе: отдельная таблица брака в
-задание не входит. Проверьте работу строгого входа одним запросом. Посчитайте:
+Считайте отвергнутые строки прямо в исходном срезе, без отдельной таблицы
+брака. Одним запросом посчитайте:
- все доставки;
-- строки, прошедшие ваш предикат;
-- строки, отвергнутые его отрицанием.
+- строки, прошедшие ваше условие отбора;
+- строки, для которых это условие ложно.
Проверьте равенство общего числа строк сумме прошедших и отвергнутых. Так каждая
исходная строка учитывается ровно в одной группе. Если отвергнутые строки есть,
сохраните их `raw`, `_load_ts` и координаты Kafka. Найдите те же
координаты в `ods.event_errors_dist` и запишите стендовый `error_class`. Если
брака нет, так и зафиксируйте: опыт проверил успешную ветвь и выполнение
-предиката на этом срезе, но не классификатор ошибок. Подкладывать грязные
+условия отбора на этом срезе, но не классификатор ошибок. Подкладывать грязные
сообщения ради задания не требуется.
## Приведите стороны к одному зерну
@@ -126,17 +124,17 @@ Airflow. При включенном `world_live` посмотрите его т
полных ключей среди годных доставок. Разница между числом доставок и этим
счетом показывает повторы в исходном срезе.
-Область эталона постройте независимо от своего предиката и результата вставки.
-Снова возьмите весь исходный STG-срез. По координатам Kafka исключите доставки,
-которые есть в `ods.event_errors_dist`. У оставшихся доставок извлеките
-`WatchID` без риска прервать запрос и выберите эти события из свежего
+Выберите строки ODS для сравнения независимо от своего условия отбора и
+результата вставки. Снова возьмите весь исходный STG-срез. По координатам Kafka
+исключите доставки, которые есть в `ods.event_errors_dist`. У оставшихся
+доставок извлеките `WatchID` без риска прервать запрос и выберите эти события из свежего
`ods.event_v`.
Свяжите сырую доставку с ODS по `WatchID`: по договору генератора это уникальный
-идентификатор события. Он задает только область эталона. Дедупликация и
-последующее соединение используют полный физический ключ. Не берите область из
-своей таблицы и не применяйте к ней свой предикат. Иначе ошибочно отвергнутая
-строка исчезнет с обеих сторон сверки.
+идентификатор события. Здесь он нужен только для выбора событий ODS.
+Удаление повторов и последующее соединение используют полный физический ключ.
+Не берите список событий из своей таблицы и не применяйте к нему свое условие
+отбора. Иначе ошибочно отвергнутая строка исчезнет с обеих сторон сверки.
## Сверьте 47 полей
@@ -171,44 +169,46 @@ ClickHouse по умолчанию заполняет отсутствующую
Для измененной строки назовите поля, которые расходятся, и сравните каждое с
сырой строкой. Для пропавшей или лишней строки проверьте пять опорных полей и
-решение строгого предиката. Если координаты доставки есть в
+результат условия отбора. Если координаты доставки есть в
`ods.event_errors_dist`, посмотрите `error_class`.
Исправьте разбор, удалите и заново создайте только собственную таблицу, затем
повторите вставку того же среза. Остановитесь, когда счет исходных доставок по
-прежним границам не изменился, баланс строгого входа сошелся, а построчная
-сверка пуста.
+прежним границам не изменился, число годных и отвергнутых строк в сумме дает
+число исходных доставок, а построчная сверка пуста.
-## Где заканчивается проверка
+## Чего не подтверждает сравнение с ODS
`ods.event_v` — образец для сравнения. Оба разбора могли повторить одну
-ошибку. Для пяти опорных полей у вас есть проверка разбираемости и исходный
-JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако второго
-независимого источника для всех значений на стенде нет.
+ошибку. Для пяти опорных полей у вас есть проверка преобразования в нужный тип
+и исходный JSON. Остальные 42 поля можно вручную сопоставить с `raw`, однако
+второго независимого источника для всех значений на стенде нет.
## Уберите свой объект
-Сохраните DDL таблицы, запрос разбора, границы среза, баланс строгого входа и
-пустую итоговую сверку. Затем удалите свою таблицу командой `DROP TABLE` с ее
-точным именем. Убедитесь через `system.tables`, что объекта больше нет.
+Сохраните DDL таблицы, запрос разбора, границы среза, число исходных, годных
+и отвергнутых строк и итоговую сверку без расхождений. Запишите также, чего
+не подтверждает сравнение с ODS и как TTL ограничивает время для повторения
+опыта. Затем удалите свою таблицу командой `DROP TABLE` с ее точным именем.
+Убедитесь через `system.tables`, что объекта больше нет.
Прикладные строки стенда вы не меняли, поэтому удалять данные из STG, ODS или
Kafka после опыта не нужно.
-## Приемка
+## Что должно получиться
-- [ ] записаны границы малого завершенного среза, его счет и координаты
+- [ ] у вас сохранены границы выбранного среза, число строк и координаты
доставки по партициям Kafka;
-- [ ] создана одна собственная таблица со всеми 47 полями, перенесенным
- `_load_ts` и физическим договором дедупликации ODS;
-- [ ] пакетный разбор принимает только JSON-объекты с точным набором ключей и
- пятью разбираемыми опорными полями;
-- [ ] счет исходных строк равен сумме годных и отвергнутых, а найденный брак
- рассмотрен по `raw`, координатам и стендовому классу;
-- [ ] область ODS получена из всех координат исходного среза и стендовой
- таблицы ошибок, независимо от собственного предиката и таблицы;
-- [ ] обе стороны приведены к зерну полного физического ключа;
-- [ ] `FULL OUTER JOIN` сравнивает все 47 полей, а числа пропавших, лишних и
- измененных строк равны нулю;
-- [ ] отдельно подтвержден перенос `_load_ts` и учтены повторы доставки;
-- [ ] границы стендового оракула и TTL записаны вместе с результатом;
-- [ ] собственная таблица удалена, объекты и данные стенда не изменены.
+- [ ] вы собрали свою таблицу со всеми 47 полями и `_load_ts`, используя
+ те же правила удаления повторов, что и ODS;
+- [ ] ваш разбор принимает только JSON-объекты с точным набором ключей и
+ пятью опорными полями, которые преобразуются в нужные типы;
+- [ ] вы сверили число исходных строк с суммой годных и отвергнутых,
+ а найденный брак разобрали по `raw`, координатам и классу ошибки стенда;
+- [ ] вы выбрали события ODS по исходному срезу и таблице ошибок,
+ независимо от своей таблицы и условия отбора;
+- [ ] вы сравнили обе стороны по полному физическому ключу через
+ `FULL OUTER JOIN`: во всех 47 полях нет расхождений,
+ пропавших и лишних строк тоже нет;
+- [ ] вы отдельно проверили перенос `_load_ts` и учли повторные доставки;
+- [ ] вы записали ограничения сравнения с ODS и срок повторения опыта;
+- [ ] после опыта вы удалили свою таблицу, сохранив объекты и данные стенда.
diff --git a/docs/labs/7-1-monitoring.md b/docs/labs/7-1-monitoring.md
index 9f5dd9e..4c0ad11 100644
--- a/docs/labs/7-1-monitoring.md
+++ b/docs/labs/7-1-monitoring.md
@@ -99,21 +99,20 @@
[«Данные»](../architecture/monitoring/data.md). Устройство живого дня и
доставки слепка описано в [главе об Airflow](../handbook/airflow-dags.md).
-## Приемка
+## Что должно получиться
-- [ ] для каждого из трех дашбордов записан вопрос, на который он отвечает;
-- [ ] сохранены окно, текст и показатели верхнего запроса, определены его
- источник и назначение;
-- [ ] среди запросов открытых дашбордов найден тот, который прочитал больше
- всего строк, и соответствующая панель;
-- [ ] объяснено, как журнал собирают с двух нод и какие записи учитывают для
- счета запросов;
-- [ ] до наблюдения нарисован прогноз свежести событий и заказов, а форма
- линии объяснена через метку последней доставки;
-- [ ] записаны несколько наблюдений, на наброске видны рост и сброс свежести
- заказов, а доставка подтверждена в Airflow и на панели слепков;
-- [ ] модельная дата отделена от реального времени наблюдения;
-- [ ] объяснен выбор слоев для свежести, потока и доли брака, а также общее
- окно числителя и знаменателя;
-- [ ] службы не остановлены, данные вручную не правились, стендовые дашборды
- не изменены.
+- [ ] вы записали по одному вопросу для каждого из трех дашбордов;
+- [ ] у вас сохранены окно, текст и показатели верхнего запроса;
+ вы выяснили, кто его отправил и зачем;
+- [ ] вы нашли запрос открытых дашбордов, который прочитал больше всего
+ строк, и панель, которая его отправила;
+- [ ] вы можете объяснить, зачем журнал собирают с двух нод и какие записи
+ учитывают при подсчете запросов;
+- [ ] у вас есть прогноз свежести, сделанный до наблюдения, и несколько
+ замеров; рост и сброс на рисунке объяснены через время последней доставки;
+- [ ] вы подтвердили доставку заказов в Airflow и на панели слепков,
+ подписали модельную дату отдельно от реального времени наблюдений;
+- [ ] вы можете объяснить выбор слоев для свежести, потока и доли брака
+ и почему числитель и знаменатель берут за одно окно;
+- [ ] после наблюдений службы продолжают работать, данные и стендовые
+ дашборды остались без изменений.
diff --git a/docs/labs/README.md b/docs/labs/README.md
index 0ca33ee..05c69e2 100644
--- a/docs/labs/README.md
+++ b/docs/labs/README.md
@@ -10,28 +10,28 @@
|---|---|---|
| 1 | Пять витрин слоя DM | построить результат для потребителя хранилища; уроки каждой витрины — в таблице ниже |
| 2 | [Свой график в Superset](2-1-superset-chart.md) | превратить свой результат из хранилища в отчет в BI |
-| 3 | [Нарежь сессии сам](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится |
-| 4 | [Перепиши нарезку стенда](4-1-session-pipeline.md) | изменить расчетную сущность внутри готового конвейера и проверить ее внешним эталоном |
-| 5 | [Сначала сторож, потом ломай: заказы](5-1-order-checks.md)
[Перепишите разбор заказов в ODS](5-2-order-parsing.md)
[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов |
-| 6 | [Разбери сырые события и сверь результат со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом |
+| 3 | [Нарежьте сессии сами](3-1-sessions-by-hand.md) | сверить число сессий в своей нарезке с эталоном и найти события, из-за которых оно расходится |
+| 4 | [Перепишите нарезку стенда](4-1-session-pipeline.md) | переписать расчет сессий в готовом конвейере и проверить по исходным событиям |
+| 5 | [Проверьте заказы перед изменением обработки](5-1-order-checks.md)
[Перепишите разбор заказов в ODS](5-2-order-parsing.md)
[Перепишите сборку заказов в DDS](5-3-order-pipeline.md) | написать проверки качества до рискованного изменения, затем переписать разбор и сборку заказов |
+| 6 | [Разберите сырые события рядом со стендом](6-1-event-parsing.md) | изучить разбор потока отдельно от живого приема и сверить его со стендовым результатом |
| 7 | [Мониторинг: куда смотреть](7-1-monitoring.md) | выбрать, куда и на что смотреть в живой системе |
## Ступень 1: пять витрин слоя DM
Слой DM в стенде построен наполовину: витрины трех сюжетов дашборда и контроль
качества уже построены, остальные пять витрин строите вы. Каждая из них —
-самостоятельное ТЗ: в нем есть состав витрины, зерно, источники и способ
+самостоятельное задание: в нем есть состав витрины, зерно, источники и способ
проверки.
| № | Результат | Чему учит |
|---|---|---|
-| 1 | [`events_enriched_v`](1-1-events-enriched.md) | строить слой как договор о данных, а не как их копию |
+| 1 | [`events_enriched_v`](1-1-events-enriched.md) | подготовить удобный для анализа набор полей, сохранив одну строку на событие |
| 2 | [`top_pages_daily`](1-2-top-pages-daily.md) | создавать материализованную витрину с дневной пересборкой в конвейере |
| 3 | [`session_overview`](1-3-session-overview.md) | хранить слагаемые для расчета средних и долей |
| 4 | [`dq_errors_daily`](1-4-dq-errors-daily.md) | считать брак по дню загрузки и хранить итоги дольше источника |
| 5 | [`utm_effectiveness`](1-5-utm-effectiveness.md) | читать `declared_` как предупреждение о сумме со слов клиента |
-Общее устройство слоя — [дока DM](../architecture/dm.md). Она отвечает на
+Общее устройство слоя — [справочник DM](../architecture/dm.md). Он отвечает на
вопросы «почему так», задание отвечает на вопрос «что сделать».
## Как принято на ступени 1
@@ -99,9 +99,8 @@ DM](../architecture/dm.md).
собрано верно. В сводку качества конвейера `dm.dq_summary` он не пишет: это
инструмент лабы, а не история качества стенда.
-Проверка пяти заданий по витринам устроена одинаково и золотых констант не
-содержит: рядом с витриной независимым запросом собирается тот же ответ —
-прямо из источника витрины, минуя ее саму. У четырех заданий источник
+Для каждой витрины проверка независимо рассчитывает ожидаемый результат
+прямо по ее источнику. У четырех заданий источник
эталона тот же, что у витрины, `dds.session_v` или `dds.event_v`; у задания
4 это таблицы брака ODS, потому что в DDS нет сущности брака. Обе стороны
соединяются по деловому ключу через `FULL OUTER JOIN`, и считается число
@@ -109,16 +108,13 @@ DM](../architecture/dm.md).
соединение сохраняет ключи обеих сторон: при внутреннем пропавший день или
ключ не попал бы в результат, и потеря осталась бы незамеченной.
-Раздел «Как проверить себя» каждого ТЗ по витрине называет деловой ключ и
+Раздел «Как проверить себя» каждого задания по витрине называет деловой ключ и
сравниваемые величины. Тот же прием в стендовом исполнении — [даг
`dq_check`](../../dags/dq_check.py) и запросы в [`sql/dq/`](../../sql/dq).
-**Предупреждение.** Запросы самой проверки лежат в `sql/dq/` открыто, и
-имена называют витрину и роль сверки. Проверка на инвариантах неизбежно
-близка к ответу: чтобы сверить витрину, надо ее пересчитать. Если открыть
-эти файлы до выполнения задания, можно скопировать готовый SQL, не
-разобравшись в расчете самостоятельно. Смотреть туда стоит после своей
-сборки: сравнить два решения полезно, списать одно — нет.
+Запросы проверок лежат в `sql/dq/`. Откройте их после собственного решения:
+они заново рассчитывают показатели витрины, поэтому содержат ответ на
+существенную часть задания. Сравните свой способ расчета с готовым.
## Факультативы
@@ -152,12 +148,12 @@ DM](../architecture/dm.md).
### Что делать, когда стенд сломался
-Открывается после ступени 7. Поломки стенда и runbook по итогам
-расследований; весь раздел — повышенная сложность.
+Открывается после ступени 7. Поломки стенда и инструкции по восстановлению
+по итогам расследований; весь раздел — повышенная сложность.
## Уборка
-Черновики и опыты за собой сносить: правило проверок стенда —
-[дока тестирования](../architecture/testing.md). Снесенная витрина обязана
-снова покраснить свою задачу в `homework_check` — если не покраснила,
-проверка ничего и не проверяла.
+После опытов удаляйте свои временные объекты. Если удалили учебную витрину,
+ее задача в `homework_check` должна снова завершиться ошибкой: отсутствие
+витрины тоже проверяется. Подробнее — в
+[справочнике проверок](../architecture/testing.md).
diff --git a/docs/specs/2026-09-01-labs-program.md b/docs/specs/2026-09-01-labs-program.md
index a0bb100..bec0091 100644
--- a/docs/specs/2026-09-01-labs-program.md
+++ b/docs/specs/2026-09-01-labs-program.md
@@ -40,6 +40,35 @@
- *расследование* — данные уже лежат на стенде или стенд намеренно ломается, а
менти ищет причину и объясняет ее.
+### Как писать задания
+
+Задание обращается к учащемуся на «вы». Начните с «Чему учит»: назовите
+умение, которое он получит. Затем объясните, для чего нужен результат, и
+ведите читателя по действиям. Термины раскрывайте там, где без них нельзя
+понять следующий шаг. Историю разработки оставляйте в рабочих справочниках.
+
+Раздел «Как проверить себя» описывает способ проверки: что выполнить,
+с чем сравнить и где искать причину расхождения. Если проверка и есть ход
+исследования, сохраните ее в шагах, без повторного раздела. В конце задания
+«Что должно получиться» помогает учащемуся оценить результат: что у него
+работает, что он проверил и что может объяснить. Чекбоксы обращены к нему:
+«вы сверили числа», «у вас сохранен запрос». Подробный порядок действий
+остается в основной части.
+
+Заголовки остальных разделов выбирайте по содержанию: «Для чего нужна
+витрина», «Найдите причину», «Как даг запускает ваши запросы». Число разделов
+и пунктов определяется упражнением. Ограничения объясняйте рядом с действием,
+которого они касаются; завершение опыта включает восстановление данных или
+удаление временных объектов, когда это требуется.
+
+При вычитке сохраняйте состав результата, технические условия, способы
+проверки и самостоятельную часть работы. Новое упражнение или готовое решение
+под видом пояснения меняет учебный смысл и требует отдельного обсуждения.
+Формат инженерных issues из правил трекера относится к разработке стенда;
+учебные задания пишутся по этим правилам. Образцы:
+[дашборд Superset](../labs/optional/superset-dashboard.md) и
+[расследование подключения](../labs/optional/superset-missing-object.md).
+
## 3. Основная программа — семь ступеней
### Ступень 1. Построй пять витрин слоя DM
@@ -64,7 +93,7 @@
ступени предстоит написать при реализации программы. Полный собственный дашборд
остается факультативом в разделе «Superset и BI».
-### Ступень 3. Нарежь сессии сам
+### Ступень 3. Нарежьте сессии сами
До того как открыть стендовый SQL нарезки сессий в DDS, менти самостоятельно
делит события на визиты. Сначала он сравнивает число своих сессий с эталоном
@@ -75,7 +104,7 @@
Форма — построить свое по ТЗ и провести расследование. Обратная связь состоит
из счетной сверки с `VisitID` и ручного сопоставления границ.
-### Ступень 4. Перепиши нарезку стенда
+### Ступень 4. Перепишите нарезку стенда
Менти удаляет и заново пишет SQL нарезки сессий DDS: расчет объема, пересборку
и замену. Урок — изменить расчетную сущность внутри готового конвейера и
@@ -86,7 +115,7 @@
Форма — вычитание. Обратную связь дают `dq_check` и поштучная сверка,
построенная на третьей ступени.
-### Ступень 5. Сначала сторож, потом ломай: заказы
+### Ступень 5. Проверьте заказы перед изменением обработки
Сначала менти строит две недостающие DQ-проверки. Первая утверждает равенство
«цена × количество = items_total». Вторая сверяет слой с источником по дням. Их