Написать ТЗ ступени 3 «Нарежь сессии сам» — файл docs/labs/3-1-sessions-by-hand.md — и связать его с индексом лаб: строка 3 таблицы основной программы в docs/labs/README.md получает ссылку, фраза «ТЗ для ступеней 3–7 появятся…» сдвигается на «4–7».
Спека («Программа учебных лаб», ступень 3): до того как открыть стендовый SQL нарезки сессий, учащийся сам делит события на визиты, сравнивает число своих сессий с эталоном VisitID, затем вручную соединяет свою сессию с эталонной и разбирает хотя бы одно расхождение границ. Урок — счет называет проблему, но причину приходится искать в отдельных строках. Форма — построить свое по ТЗ и провести расследование.
Чему научится: строить расчетную сущность, которой нет в потоке, и проверять ее внешним эталоном на зерне, которое показывает не «сколько разошлось», а «у кого именно»; после счета идти в строки.
Решения, принятые здесь
Это вкусовая часть; исполнитель их не пересматривает.
Факт, от которого отталкивается форма. Стендовая нарезка совпадает с VisitID один в один (проверено 2026-09-06 на десяти прожитых днях, расхождений ноль; ревью постановки подтвердило на 72 559 ключах кука × день). Правило генератора (generator/src/clickstream_generator/day.py:24-37): визит — одна кука; пауза дольше 30 минут между соседними событиями любого типа открывает новый визит; граница модельных суток режет визит. Единственное, в чем «кука + 30 минут» расходится с эталоном, — граница суток: визит, который эталон разрезал на границе модельного дня при паузе короче 30 минут. Таких случаев мало: 3 на 10 прожитых дней (по одному на границах 06-07/08, 06-08/09, 06-09/10); на свежем стенде с восемью днями — ровно один, на последнем дне. Ловушки «пауза только между просмотрами» и «равные метки времени» на этих данных не срабатывают (0 случаев). В UTCEventTime шов суток виден не как полночь, а как 20:00 (счетчик живет в поясе Europe/Samara, UTC+4): у двух соседних событий одна дата в UTC и разный EventDate. Слово «полночь» в ТЗ не употребляется — учащийся его не увидит.
Правило нарезки дается открыто как определение аналитика, без границы суток, и ТЗ честно говорит, что сверка разойдется. Первый шаг: визит — события одной куки (ClientID) подряд, где пауза между соседними событиями не дольше 30 минут; события всех типов. ТЗ прямо предупреждает: у стенда есть еще одна договоренность, и счет ее покажет; задача — найти по счету, у кого разошлось, а по строкам — на каком событии и почему. Скрывать правило суток смысла нет: его называет глоссарий CONTEXT.md, куда путеводитель ведет первым шагом. Урок от этого не страдает: даже знающий правило учащийся должен найти конкретные строки и увязать их с EventDate. Формулировка порога одна на весь текст: «пауза дольше 30 минут открывает новый визит».
Если расхождений нет. Их плотность около одного на три границы суток, поэтому на молодом стенде сверка может сойтись везде. ТЗ говорит об этом одним абзацем: расхождений будет единицы, а если их нет вовсе — мир прожил мало дней; продвинуть его на несколько дней (глава «Потоки Airflow», даги модельного времени) и повторить сверку. Отдельной второй ветки расследования нет: любой «визит, закрытый без паузы» — тот же самый случай границы суток.
Источник — ods.event_v, для обеих сторон. Своя нарезка и эталон считаются по одним и тем же строкам, VisitID лежит рядом с событием. dds.event_vVisitID скрывает намеренно, а склейка двух представлений по event_id — работа без урока. Колонки, которые нужны: ClientID, UTCEventTime, EventDate, EventType, WatchID, VisitID. Читать в SQL Lab или DBeaver под analyst (у роли есть SELECT ON ods.*).
Объем — все дни, что есть в ods.event_v (около 10 дней, полмиллиона событий; одному запросу с оконными функциями это дешево). Один день не подходит: ловушка на границе суток нуждается в соседних днях. Дат в ТЗ нет.
День своей сессии — EventDate ее первого события. Формат события (docs/formats/clickstream-event.md, строка EventDate) уже говорит: дату из UTCEventTime не выводить. ТЗ повторяет это одним предложением с той же причиной (у ночных событий дата в UTC на сутки другая), без запрета.
Зерно сверки — кука × день. Своя сторона: число сессий по (ClientID, день). Эталон: uniqExact(VisitID) по (ClientID, EventDate). Стороны соединяются FULL OUTER JOIN по паре ключей (без GLOBAL: обе стороны — готовые агрегаты, и слово ничего не меняет), как в самопроверке ступени 1; итог — сводка по дням (сколько кук разошлось) и список разошедшихся кук. Сумма по дню могла бы спрятать взаимную компенсацию, зерно кука × день — нет; на этом зерне видно, у кого именно искать. В ТЗ целиком приводится только запрос эталонной стороны. Свою нарезку и запрос сверки учащийся пишет сам; ТЗ описывает сверку прозой: ключи, полное соединение и почему оно (у реальных расхождений на своей стороне ключа нет совсем; чисел со стенда в ТЗ не переносить, как и дат), что вывести. Подсказка к нарезке — одно слово «оконные функции» (lagInFrame и накопительная сумма не называются).
Расследование. Соединение идет по событиям, а не «сессия к сессии»: у своей сессии и эталонной общего ключа нет, а событие несет оба признака — VisitID и номер своей сессии. Для одной разошедшейся куки: вывести ее события за два соседних дня с UTCEventTime, EventDate, VisitID и номером своей сессии; найти пару соседних событий, где решения разошлись; назвать причину одним предложением. Ответ — кука, день, время двух событий и причина. Затем уточнить правило и добиться нуля расхождений во всех днях. Дальше стендовый SQL остается закрытым: сравнение со стендовой нарезкой по session_id — материал ступени 4.
Ничего не создается. Ни таблиц, ни представлений, ни DDL, ни задачи в homework_check: результат — запрос и ответ расследования. Обратная связь ступени по спеке — счетная сверка с VisitID и ручное сопоставление границ, она встроена в задание. Проверка глазами наставника, как график ступени 2.
Стендовые ответы.sql/dds/session_rebuild.sql и sql/dq/sessions_vs_reference.sql содержат нарезку и сверку целиком. ТЗ говорит об этом как в README лаб («Предупреждение» в самопроверке ступени 1): открыть их стоит после своей нарезки, сравнить два решения полезно, списать одно — нет. Без слов «нельзя» и «запрещено».
Форма — по образцу docs/labs/2-1-superset-chart.md: заголовок «Нарежь сессии сам», «Чему учит» одной-двумя фразами, «Сначала прочитать» (глава «Хранилище и его слои», раздел DDS; формат события, таблица «Идентификаторы и время»), «Роль сессии» — зачем аналитику визит и почему его считает хранилище, «Что сделать» с подразделами «Нарезка», «Сверка с эталоном», «Расследование», «Как проверить себя», «Приемка» чекбоксами. Объем — до 120 строк (ТЗ ступени 2 — 90).
Язык. Обращение «вы» строчными. Без «ё», без «менти». Короткие фразы, одна мысль на фразу. Без олицетворений и калек. Термины — из путеводителя: кука, визит, сессия (то же понятие словами аналитики), эталон, модельные сутки. Без законов: договоренности стенда — выбор с доводом; прямое предупреждение уместно только там, где иначе технически не выйдет.
README лаб — только две точечные правки, названные в цели. Ничего в других разделах не меняется.
Границы
Не трогать: пять ТЗ ступени 1, ТЗ ступени 2, факультатив, путеводитель, справочники, CONTEXT.md, SQL и даги стенда.
Не создавать проверок, SQL-файлов и объектов в хранилище.
Спека не правится.
Сначала прочитать
docs/specs/2026-09-01-labs-program.md, разделы 2 и 3 (ступень 3).
docs/labs/2-1-superset-chart.md — форма и тон.
docs/labs/README.md — таблица программы, «Как принято на ступени 1», «Самопроверка ступени 1» (образец предупреждения).
docs/architecture/dds/session.md — зерно и сверка (для исполнителя; в ТЗ не ссылаться: это ответ на ступень 4).
docs/formats/clickstream-event.md — колонки.
sql/dq/sessions_vs_reference.sql — как сверяет стенд (для исполнителя, в ТЗ упоминается только как «ответ»).
Проверка
make lint в корне.
Все запросы ТЗ выполнены на стенде под analyst и возвращают то, что обещает текст.
Ссылки в ТЗ и README ведут на существующие файлы и якоря.
Приемка
docs/labs/3-1-sessions-by-hand.md написан по форме ТЗ ступени 2 и решениям выше;
правило нарезки в ТЗ — кука и «пауза дольше 30 минут», граница суток как правило не названа, но предупреждение о расхождении и абзац «если расхождений нет — продвинуть мир» есть; слова «полночь» нет;
запрос эталонной стороны приведен; своя нарезка и запрос сверки не приведены, сверка описана прозой;
сверка на зерне кука × день через FULL OUTER JOIN, сводка по дням и список кук;
раздел расследования требует куку, день, два события и причину;
в README лаб строка 3 ведет на ТЗ, фраза про будущие ТЗ — «4–7», остальное не тронуто;
слов-законов нет, обращение «вы», без «ё»;
запросы ТЗ выполнены на стенде, make lint зеленый.
Part of #206
## Цель
Написать ТЗ ступени 3 «Нарежь сессии сам» — файл `docs/labs/3-1-sessions-by-hand.md` — и связать его с индексом лаб: строка 3 таблицы основной программы в `docs/labs/README.md` получает ссылку, фраза «ТЗ для ступеней 3–7 появятся…» сдвигается на «4–7».
Спека («Программа учебных лаб», ступень 3): до того как открыть стендовый SQL нарезки сессий, учащийся сам делит события на визиты, сравнивает число своих сессий с эталоном `VisitID`, затем вручную соединяет свою сессию с эталонной и разбирает хотя бы одно расхождение границ. Урок — счет называет проблему, но причину приходится искать в отдельных строках. Форма — построить свое по ТЗ и провести расследование.
Чему научится: строить расчетную сущность, которой нет в потоке, и проверять ее внешним эталоном на зерне, которое показывает не «сколько разошлось», а «у кого именно»; после счета идти в строки.
## Решения, принятые здесь
Это вкусовая часть; исполнитель их не пересматривает.
**Факт, от которого отталкивается форма.** Стендовая нарезка совпадает с `VisitID` один в один (проверено 2026-09-06 на десяти прожитых днях, расхождений ноль; ревью постановки подтвердило на 72 559 ключах кука × день). Правило генератора (`generator/src/clickstream_generator/day.py:24-37`): визит — одна кука; пауза дольше 30 минут между соседними событиями любого типа открывает новый визит; граница модельных суток режет визит. Единственное, в чем «кука + 30 минут» расходится с эталоном, — граница суток: визит, который эталон разрезал на границе модельного дня при паузе короче 30 минут. Таких случаев мало: 3 на 10 прожитых дней (по одному на границах 06-07/08, 06-08/09, 06-09/10); на свежем стенде с восемью днями — ровно один, на последнем дне. Ловушки «пауза только между просмотрами» и «равные метки времени» на этих данных не срабатывают (0 случаев). В `UTCEventTime` шов суток виден не как полночь, а как 20:00 (счетчик живет в поясе Europe/Samara, UTC+4): у двух соседних событий одна дата в UTC и разный `EventDate`. Слово «полночь» в ТЗ не употребляется — учащийся его не увидит.
**Правило нарезки дается открыто как определение аналитика, без границы суток, и ТЗ честно говорит, что сверка разойдется.** Первый шаг: визит — события одной куки (`ClientID`) подряд, где пауза между соседними событиями не дольше 30 минут; события всех типов. ТЗ прямо предупреждает: у стенда есть еще одна договоренность, и счет ее покажет; задача — найти по счету, у кого разошлось, а по строкам — на каком событии и почему. Скрывать правило суток смысла нет: его называет глоссарий `CONTEXT.md`, куда путеводитель ведет первым шагом. Урок от этого не страдает: даже знающий правило учащийся должен найти конкретные строки и увязать их с `EventDate`. Формулировка порога одна на весь текст: «пауза дольше 30 минут открывает новый визит».
**Если расхождений нет.** Их плотность около одного на три границы суток, поэтому на молодом стенде сверка может сойтись везде. ТЗ говорит об этом одним абзацем: расхождений будет единицы, а если их нет вовсе — мир прожил мало дней; продвинуть его на несколько дней (глава [«Потоки Airflow»](docs/handbook/airflow-dags.md), даги модельного времени) и повторить сверку. Отдельной второй ветки расследования нет: любой «визит, закрытый без паузы» — тот же самый случай границы суток.
**Источник — `ods.event_v`, для обеих сторон.** Своя нарезка и эталон считаются по одним и тем же строкам, `VisitID` лежит рядом с событием. `dds.event_v` `VisitID` скрывает намеренно, а склейка двух представлений по `event_id` — работа без урока. Колонки, которые нужны: `ClientID`, `UTCEventTime`, `EventDate`, `EventType`, `WatchID`, `VisitID`. Читать в SQL Lab или DBeaver под `analyst` (у роли есть `SELECT ON ods.*`).
**Объем — все дни, что есть в `ods.event_v`** (около 10 дней, полмиллиона событий; одному запросу с оконными функциями это дешево). Один день не подходит: ловушка на границе суток нуждается в соседних днях. Дат в ТЗ нет.
**День своей сессии — `EventDate` ее первого события.** Формат события (`docs/formats/clickstream-event.md`, строка `EventDate`) уже говорит: дату из `UTCEventTime` не выводить. ТЗ повторяет это одним предложением с той же причиной (у ночных событий дата в UTC на сутки другая), без запрета.
**Зерно сверки — кука × день.** Своя сторона: число сессий по (`ClientID`, день). Эталон: `uniqExact(VisitID)` по (`ClientID`, `EventDate`). Стороны соединяются `FULL OUTER JOIN` по паре ключей (без `GLOBAL`: обе стороны — готовые агрегаты, и слово ничего не меняет), как в самопроверке ступени 1; итог — сводка по дням (сколько кук разошлось) и список разошедшихся кук. Сумма по дню могла бы спрятать взаимную компенсацию, зерно кука × день — нет; на этом зерне видно, у кого именно искать. В ТЗ целиком приводится только запрос эталонной стороны. Свою нарезку и запрос сверки учащийся пишет сам; ТЗ описывает сверку прозой: ключи, полное соединение и почему оно (у реальных расхождений на своей стороне ключа нет совсем; чисел со стенда в ТЗ не переносить, как и дат), что вывести. Подсказка к нарезке — одно слово «оконные функции» (`lagInFrame` и накопительная сумма не называются).
**Расследование.** Соединение идет по событиям, а не «сессия к сессии»: у своей сессии и эталонной общего ключа нет, а событие несет оба признака — `VisitID` и номер своей сессии. Для одной разошедшейся куки: вывести ее события за два соседних дня с `UTCEventTime`, `EventDate`, `VisitID` и номером своей сессии; найти пару соседних событий, где решения разошлись; назвать причину одним предложением. Ответ — кука, день, время двух событий и причина. Затем уточнить правило и добиться нуля расхождений во всех днях. Дальше стендовый SQL остается закрытым: сравнение со стендовой нарезкой по `session_id` — материал ступени 4.
**Ничего не создается.** Ни таблиц, ни представлений, ни DDL, ни задачи в `homework_check`: результат — запрос и ответ расследования. Обратная связь ступени по спеке — счетная сверка с `VisitID` и ручное сопоставление границ, она встроена в задание. Проверка глазами наставника, как график ступени 2.
**Стендовые ответы.** `sql/dds/session_rebuild.sql` и `sql/dq/sessions_vs_reference.sql` содержат нарезку и сверку целиком. ТЗ говорит об этом как в README лаб («Предупреждение» в самопроверке ступени 1): открыть их стоит после своей нарезки, сравнить два решения полезно, списать одно — нет. Без слов «нельзя» и «запрещено».
**Форма** — по образцу `docs/labs/2-1-superset-chart.md`: заголовок «Нарежь сессии сам», «Чему учит» одной-двумя фразами, «Сначала прочитать» (глава [«Хранилище и его слои»](docs/handbook/storage-layers.md), раздел DDS; [формат события](docs/formats/clickstream-event.md), таблица «Идентификаторы и время»), «Роль сессии» — зачем аналитику визит и почему его считает хранилище, «Что сделать» с подразделами «Нарезка», «Сверка с эталоном», «Расследование», «Как проверить себя», «Приемка» чекбоксами. Объем — до 120 строк (ТЗ ступени 2 — 90).
**Язык.** Обращение «вы» строчными. Без «ё», без «менти». Короткие фразы, одна мысль на фразу. Без олицетворений и калек. Термины — из путеводителя: кука, визит, сессия (то же понятие словами аналитики), эталон, модельные сутки. Без законов: договоренности стенда — выбор с доводом; прямое предупреждение уместно только там, где иначе технически не выйдет.
**README лаб** — только две точечные правки, названные в цели. Ничего в других разделах не меняется.
## Границы
- Не трогать: пять ТЗ ступени 1, ТЗ ступени 2, факультатив, путеводитель, справочники, `CONTEXT.md`, SQL и даги стенда.
- Не создавать проверок, SQL-файлов и объектов в хранилище.
- Спека не правится.
## Сначала прочитать
- `docs/specs/2026-09-01-labs-program.md`, разделы 2 и 3 (ступень 3).
- `docs/labs/2-1-superset-chart.md` — форма и тон.
- `docs/labs/README.md` — таблица программы, «Как принято на ступени 1», «Самопроверка ступени 1» (образец предупреждения).
- `docs/architecture/dds/session.md` — зерно и сверка (для исполнителя; в ТЗ не ссылаться: это ответ на ступень 4).
- `docs/formats/clickstream-event.md` — колонки.
- `sql/dq/sessions_vs_reference.sql` — как сверяет стенд (для исполнителя, в ТЗ упоминается только как «ответ»).
## Проверка
- `make lint` в корне.
- Все запросы ТЗ выполнены на стенде под `analyst` и возвращают то, что обещает текст.
- Ссылки в ТЗ и README ведут на существующие файлы и якоря.
## Приемка
- [x] `docs/labs/3-1-sessions-by-hand.md` написан по форме ТЗ ступени 2 и решениям выше;
- [x] правило нарезки в ТЗ — кука и «пауза дольше 30 минут», граница суток как правило не названа, но предупреждение о расхождении и абзац «если расхождений нет — продвинуть мир» есть; слова «полночь» нет;
- [x] запрос эталонной стороны приведен; своя нарезка и запрос сверки не приведены, сверка описана прозой;
- [x] сверка на зерне кука × день через `FULL OUTER JOIN`, сводка по дням и список кук;
- [x] раздел расследования требует куку, день, два события и причину;
- [x] в README лаб строка 3 ведет на ТЗ, фраза про будущие ТЗ — «4–7», остальное не тронуто;
- [x] слов-законов нет, обращение «вы», без «ё»;
- [x] запросы ТЗ выполнены на стенде, `make lint` зеленый.
Сделано в PR #233 (ветка 232-lab-sessions), слияние закроет тикет.
Что остается:
ТЗ ступени 3 — docs/labs/3-1-sessions-by-hand.md, 89 строк, по форме ТЗ ступени 2. Источник обеих сторон — ods.event_v (в dds.event_v эталонного VisitID нет намеренно). Нарезка: кука и «пауза дольше 30 минут открывает новый визит», события всех типов, день сессии — EventDate первого события; объем — все прожитые дни. Сверка на зерне кука × день: эталон uniqExact(VisitID), FULL OUTER JOIN, сводка по дням и список кук. Расследование — соединение по событиям (общего ключа у нарезок нет), ответ: кука, день, два события, причина; затем ноль расхождений везде.
Стендовая нарезка совпадает с VisitID один в один (десять дней, 72 559 ключей). Единственное расхождение «куки + 30 минут» с эталоном — граница модельных суток: 3 случая на 10 дней, на свежем стенде из восьми дней — один, на последнем дне. Поэтому ТЗ честно предупреждает, что сверка разойдется, а при нуле расхождений просит продвинуть мир. Скрывать правило суток не имело смысла: его называет глоссарий CONTEXT.md, куда путеводитель ведет первым шагом. Урок держится не на секрете, а на поиске строк.
В UTCEventTime шов суток виден как 20:00 (пояс счетчика Europe/Samara), поэтому слова «полночь» в ТЗ нет. Ловушки «пауза только между просмотрами» и «равные метки времени» на данных стенда не срабатывают, в ТЗ их нет.
README лаб: строка 3 ведет на ТЗ, «ТЗ для ступеней 4–7 появятся…». Ничего не создается в хранилище, задачи в homework_check нет: обратная связь ступени встроена в сверку.
Как делалось: постановка — Fable; ревью постановки — Opus (deep-reasoner): 8 находок, главная — правило суток скрыть невозможно, форма переделана на открытое предупреждение; все внесены решениями, перепроверены тем же ревьюером. Проза — Codex gpt-6-astra medium; саморевью — 0. Слепое ревью — Opus (craftsman) свежей сессией, одна линия: 3 находки («наставник» как несуществующее лицо, сортировка по WatchID как сторож без опасности, намек на «ночные» часы) плюс 1 при перепроверке, все закрыты. Вычитка — свежий тред Sol по ai-text-lint, 15 правок. Приемка глазами: две правки (словарь «мир прожил мало дней», убрана фраза, называющая ответ расследования).
Вне тикета: глава путеводителя «Хранилище и его слои» дает DDS три предложения и о сессии почти ничего — ссылка из ТЗ на нее ориентирует слабо; кандидат на доработку главы, когда до нее дойдет очередь.
Дальше по карте: ТЗ ступени 4 «Перепиши нарезку стенда» — вычитание sql/dds/session_rebuild.sql; тикет заводится при старте работы.
## Резолюция
Сделано в PR #233 (ветка 232-lab-sessions), слияние закроет тикет.
**Что остается:**
- ТЗ ступени 3 — `docs/labs/3-1-sessions-by-hand.md`, 89 строк, по форме ТЗ ступени 2. Источник обеих сторон — `ods.event_v` (в `dds.event_v` эталонного `VisitID` нет намеренно). Нарезка: кука и «пауза дольше 30 минут открывает новый визит», события всех типов, день сессии — `EventDate` первого события; объем — все прожитые дни. Сверка на зерне кука × день: эталон `uniqExact(VisitID)`, `FULL OUTER JOIN`, сводка по дням и список кук. Расследование — соединение по событиям (общего ключа у нарезок нет), ответ: кука, день, два события, причина; затем ноль расхождений везде.
- Стендовая нарезка совпадает с `VisitID` один в один (десять дней, 72 559 ключей). Единственное расхождение «куки + 30 минут» с эталоном — граница модельных суток: 3 случая на 10 дней, на свежем стенде из восьми дней — один, на последнем дне. Поэтому ТЗ честно предупреждает, что сверка разойдется, а при нуле расхождений просит продвинуть мир. Скрывать правило суток не имело смысла: его называет глоссарий `CONTEXT.md`, куда путеводитель ведет первым шагом. Урок держится не на секрете, а на поиске строк.
- В `UTCEventTime` шов суток виден как 20:00 (пояс счетчика Europe/Samara), поэтому слова «полночь» в ТЗ нет. Ловушки «пауза только между просмотрами» и «равные метки времени» на данных стенда не срабатывают, в ТЗ их нет.
- README лаб: строка 3 ведет на ТЗ, «ТЗ для ступеней 4–7 появятся…». Ничего не создается в хранилище, задачи в `homework_check` нет: обратная связь ступени встроена в сверку.
**Как делалось:** постановка — Fable; ревью постановки — Opus (deep-reasoner): 8 находок, главная — правило суток скрыть невозможно, форма переделана на открытое предупреждение; все внесены решениями, перепроверены тем же ревьюером. Проза — Codex gpt-6-astra medium; саморевью — 0. Слепое ревью — Opus (craftsman) свежей сессией, одна линия: 3 находки («наставник» как несуществующее лицо, сортировка по `WatchID` как сторож без опасности, намек на «ночные» часы) плюс 1 при перепроверке, все закрыты. Вычитка — свежий тред Sol по ai-text-lint, 15 правок. Приемка глазами: две правки (словарь «мир прожил мало дней», убрана фраза, называющая ответ расследования).
**Вне тикета:** глава путеводителя «Хранилище и его слои» дает DDS три предложения и о сессии почти ничего — ссылка из ТЗ на нее ориентирует слабо; кандидат на доработку главы, когда до нее дойдет очередь.
**Дальше по карте:** ТЗ ступени 4 «Перепиши нарезку стенда» — вычитание `sql/dds/session_rebuild.sql`; тикет заводится при старте работы.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #206
Цель
Написать ТЗ ступени 3 «Нарежь сессии сам» — файл
docs/labs/3-1-sessions-by-hand.md— и связать его с индексом лаб: строка 3 таблицы основной программы вdocs/labs/README.mdполучает ссылку, фраза «ТЗ для ступеней 3–7 появятся…» сдвигается на «4–7».Спека («Программа учебных лаб», ступень 3): до того как открыть стендовый SQL нарезки сессий, учащийся сам делит события на визиты, сравнивает число своих сессий с эталоном
VisitID, затем вручную соединяет свою сессию с эталонной и разбирает хотя бы одно расхождение границ. Урок — счет называет проблему, но причину приходится искать в отдельных строках. Форма — построить свое по ТЗ и провести расследование.Чему научится: строить расчетную сущность, которой нет в потоке, и проверять ее внешним эталоном на зерне, которое показывает не «сколько разошлось», а «у кого именно»; после счета идти в строки.
Решения, принятые здесь
Это вкусовая часть; исполнитель их не пересматривает.
Факт, от которого отталкивается форма. Стендовая нарезка совпадает с
VisitIDодин в один (проверено 2026-09-06 на десяти прожитых днях, расхождений ноль; ревью постановки подтвердило на 72 559 ключах кука × день). Правило генератора (generator/src/clickstream_generator/day.py:24-37): визит — одна кука; пауза дольше 30 минут между соседними событиями любого типа открывает новый визит; граница модельных суток режет визит. Единственное, в чем «кука + 30 минут» расходится с эталоном, — граница суток: визит, который эталон разрезал на границе модельного дня при паузе короче 30 минут. Таких случаев мало: 3 на 10 прожитых дней (по одному на границах 06-07/08, 06-08/09, 06-09/10); на свежем стенде с восемью днями — ровно один, на последнем дне. Ловушки «пауза только между просмотрами» и «равные метки времени» на этих данных не срабатывают (0 случаев). ВUTCEventTimeшов суток виден не как полночь, а как 20:00 (счетчик живет в поясе Europe/Samara, UTC+4): у двух соседних событий одна дата в UTC и разныйEventDate. Слово «полночь» в ТЗ не употребляется — учащийся его не увидит.Правило нарезки дается открыто как определение аналитика, без границы суток, и ТЗ честно говорит, что сверка разойдется. Первый шаг: визит — события одной куки (
ClientID) подряд, где пауза между соседними событиями не дольше 30 минут; события всех типов. ТЗ прямо предупреждает: у стенда есть еще одна договоренность, и счет ее покажет; задача — найти по счету, у кого разошлось, а по строкам — на каком событии и почему. Скрывать правило суток смысла нет: его называет глоссарийCONTEXT.md, куда путеводитель ведет первым шагом. Урок от этого не страдает: даже знающий правило учащийся должен найти конкретные строки и увязать их сEventDate. Формулировка порога одна на весь текст: «пауза дольше 30 минут открывает новый визит».Если расхождений нет. Их плотность около одного на три границы суток, поэтому на молодом стенде сверка может сойтись везде. ТЗ говорит об этом одним абзацем: расхождений будет единицы, а если их нет вовсе — мир прожил мало дней; продвинуть его на несколько дней (глава «Потоки Airflow», даги модельного времени) и повторить сверку. Отдельной второй ветки расследования нет: любой «визит, закрытый без паузы» — тот же самый случай границы суток.
Источник —
ods.event_v, для обеих сторон. Своя нарезка и эталон считаются по одним и тем же строкам,VisitIDлежит рядом с событием.dds.event_vVisitIDскрывает намеренно, а склейка двух представлений поevent_id— работа без урока. Колонки, которые нужны:ClientID,UTCEventTime,EventDate,EventType,WatchID,VisitID. Читать в SQL Lab или DBeaver подanalyst(у роли естьSELECT ON ods.*).Объем — все дни, что есть в
ods.event_v(около 10 дней, полмиллиона событий; одному запросу с оконными функциями это дешево). Один день не подходит: ловушка на границе суток нуждается в соседних днях. Дат в ТЗ нет.День своей сессии —
EventDateее первого события. Формат события (docs/formats/clickstream-event.md, строкаEventDate) уже говорит: дату изUTCEventTimeне выводить. ТЗ повторяет это одним предложением с той же причиной (у ночных событий дата в UTC на сутки другая), без запрета.Зерно сверки — кука × день. Своя сторона: число сессий по (
ClientID, день). Эталон:uniqExact(VisitID)по (ClientID,EventDate). Стороны соединяютсяFULL OUTER JOINпо паре ключей (безGLOBAL: обе стороны — готовые агрегаты, и слово ничего не меняет), как в самопроверке ступени 1; итог — сводка по дням (сколько кук разошлось) и список разошедшихся кук. Сумма по дню могла бы спрятать взаимную компенсацию, зерно кука × день — нет; на этом зерне видно, у кого именно искать. В ТЗ целиком приводится только запрос эталонной стороны. Свою нарезку и запрос сверки учащийся пишет сам; ТЗ описывает сверку прозой: ключи, полное соединение и почему оно (у реальных расхождений на своей стороне ключа нет совсем; чисел со стенда в ТЗ не переносить, как и дат), что вывести. Подсказка к нарезке — одно слово «оконные функции» (lagInFrameи накопительная сумма не называются).Расследование. Соединение идет по событиям, а не «сессия к сессии»: у своей сессии и эталонной общего ключа нет, а событие несет оба признака —
VisitIDи номер своей сессии. Для одной разошедшейся куки: вывести ее события за два соседних дня сUTCEventTime,EventDate,VisitIDи номером своей сессии; найти пару соседних событий, где решения разошлись; назвать причину одним предложением. Ответ — кука, день, время двух событий и причина. Затем уточнить правило и добиться нуля расхождений во всех днях. Дальше стендовый SQL остается закрытым: сравнение со стендовой нарезкой поsession_id— материал ступени 4.Ничего не создается. Ни таблиц, ни представлений, ни DDL, ни задачи в
homework_check: результат — запрос и ответ расследования. Обратная связь ступени по спеке — счетная сверка сVisitIDи ручное сопоставление границ, она встроена в задание. Проверка глазами наставника, как график ступени 2.Стендовые ответы.
sql/dds/session_rebuild.sqlиsql/dq/sessions_vs_reference.sqlсодержат нарезку и сверку целиком. ТЗ говорит об этом как в README лаб («Предупреждение» в самопроверке ступени 1): открыть их стоит после своей нарезки, сравнить два решения полезно, списать одно — нет. Без слов «нельзя» и «запрещено».Форма — по образцу
docs/labs/2-1-superset-chart.md: заголовок «Нарежь сессии сам», «Чему учит» одной-двумя фразами, «Сначала прочитать» (глава «Хранилище и его слои», раздел DDS; формат события, таблица «Идентификаторы и время»), «Роль сессии» — зачем аналитику визит и почему его считает хранилище, «Что сделать» с подразделами «Нарезка», «Сверка с эталоном», «Расследование», «Как проверить себя», «Приемка» чекбоксами. Объем — до 120 строк (ТЗ ступени 2 — 90).Язык. Обращение «вы» строчными. Без «ё», без «менти». Короткие фразы, одна мысль на фразу. Без олицетворений и калек. Термины — из путеводителя: кука, визит, сессия (то же понятие словами аналитики), эталон, модельные сутки. Без законов: договоренности стенда — выбор с доводом; прямое предупреждение уместно только там, где иначе технически не выйдет.
README лаб — только две точечные правки, названные в цели. Ничего в других разделах не меняется.
Границы
CONTEXT.md, SQL и даги стенда.Сначала прочитать
docs/specs/2026-09-01-labs-program.md, разделы 2 и 3 (ступень 3).docs/labs/2-1-superset-chart.md— форма и тон.docs/labs/README.md— таблица программы, «Как принято на ступени 1», «Самопроверка ступени 1» (образец предупреждения).docs/architecture/dds/session.md— зерно и сверка (для исполнителя; в ТЗ не ссылаться: это ответ на ступень 4).docs/formats/clickstream-event.md— колонки.sql/dq/sessions_vs_reference.sql— как сверяет стенд (для исполнителя, в ТЗ упоминается только как «ответ»).Проверка
make lintв корне.analystи возвращают то, что обещает текст.Приемка
docs/labs/3-1-sessions-by-hand.mdнаписан по форме ТЗ ступени 2 и решениям выше;FULL OUTER JOIN, сводка по дням и список кук;make lintзеленый.Резолюция
Сделано в PR #233 (ветка 232-lab-sessions), слияние закроет тикет.
Что остается:
docs/labs/3-1-sessions-by-hand.md, 89 строк, по форме ТЗ ступени 2. Источник обеих сторон —ods.event_v(вdds.event_vэталонногоVisitIDнет намеренно). Нарезка: кука и «пауза дольше 30 минут открывает новый визит», события всех типов, день сессии —EventDateпервого события; объем — все прожитые дни. Сверка на зерне кука × день: эталонuniqExact(VisitID),FULL OUTER JOIN, сводка по дням и список кук. Расследование — соединение по событиям (общего ключа у нарезок нет), ответ: кука, день, два события, причина; затем ноль расхождений везде.VisitIDодин в один (десять дней, 72 559 ключей). Единственное расхождение «куки + 30 минут» с эталоном — граница модельных суток: 3 случая на 10 дней, на свежем стенде из восьми дней — один, на последнем дне. Поэтому ТЗ честно предупреждает, что сверка разойдется, а при нуле расхождений просит продвинуть мир. Скрывать правило суток не имело смысла: его называет глоссарийCONTEXT.md, куда путеводитель ведет первым шагом. Урок держится не на секрете, а на поиске строк.UTCEventTimeшов суток виден как 20:00 (пояс счетчика Europe/Samara), поэтому слова «полночь» в ТЗ нет. Ловушки «пауза только между просмотрами» и «равные метки времени» на данных стенда не срабатывают, в ТЗ их нет.homework_checkнет: обратная связь ступени встроена в сверку.Как делалось: постановка — Fable; ревью постановки — Opus (deep-reasoner): 8 находок, главная — правило суток скрыть невозможно, форма переделана на открытое предупреждение; все внесены решениями, перепроверены тем же ревьюером. Проза — Codex gpt-6-astra medium; саморевью — 0. Слепое ревью — Opus (craftsman) свежей сессией, одна линия: 3 находки («наставник» как несуществующее лицо, сортировка по
WatchIDкак сторож без опасности, намек на «ночные» часы) плюс 1 при перепроверке, все закрыты. Вычитка — свежий тред Sol по ai-text-lint, 15 правок. Приемка глазами: две правки (словарь «мир прожил мало дней», убрана фраза, называющая ответ расследования).Вне тикета: глава путеводителя «Хранилище и его слои» дает DDS три предложения и о сессии почти ничего — ссылка из ТЗ на нее ориентирует слабо; кандидат на доработку главы, когда до нее дойдет очередь.
Дальше по карте: ТЗ ступени 4 «Перепиши нарезку стенда» — вычитание
sql/dds/session_rebuild.sql; тикет заводится при старте работы.