Files
clickstream-data-platform/docs/architecture/orders/snapshot.md
T
ddadminandClaude Fable 5 cf1c6c4636 docs(orders): спека превращена в принятый живой набор architecture/orders
Зачем: датированная спека — событие истории, а устройство компонента — живой
документ; большое полотно плохо грузится и агентом, и человеком (ADR 0011).

Что: вычитание #84 слито с переустройством формы: набор
docs/architecture/orders/ — индекс README и семь файлов по частям устройства
(нарезка по правилу «семь плюс-минус два»); датированные файлы удалены,
ссылки перенацелены, AGENTS.md дополнен правилом подпапки. Приёмка
владельцем #88 пройдена, черновой статус снят из README.

Проверка: холодная сверка миграции свежим тредом — потерь решений нет;
обход относительных ссылок набора — битых нет.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-17 22:05:52 +03:00

97 lines
8.9 KiB
Markdown

# Заказ и его слепок
Резолюция развилки [«Генератор слепков: где живёт и чем связан с
событиями»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/71).
## Откуда берётся заказ
Заказ и событие `purchase` — не два порождения, а две проекции одного факта
мира. Корзина, цены, купон и номер заказа посчитаны торговой половиной
дня-функции; заказная половина берёт заказы дня готовой структурой — вторым
выходом `commerce`, — навешивает на них жизнь заказа и собирает слепок. В
подпоток `COMMERCE` не добавляется ни одного нового броска: мир не сдвигается,
согласованность двух источников не удерживается, а получается по построению.
Следствия, которые уже решены соседями и здесь только связываются:
- у всякого заказа изначально ровно одно событие `purchase`; заказ без события
в трекере — не отдельная порода, а класс B, и делает его событийная сторона
выбрасыванием события после присвоения номера
([классы расхождений](fate.md));
- номер заказа общий у обеих проекций: `order_id` = клиентский `purchaseID`,
читаемый номер «день и порядковый номер покупки» ([спека
генератора](../../specs/2026-08-01-generator.md), раздел 9); нумеруются все
покупки, дошедшие до потока дня, — до всяких потерь;
- скидка заказа выводится из промокода события по таблице «код → скидка» —
числу мира, которое этап 3 берёт готовым (спека генератора, разделы 8 и 9).
В модели строка заказа в базе источника создаётся синхронно с покупкой,
поэтому день рождения заказа и день создания строки совпадают.
Отклонено: *выводить заказ разбором собственного вывода* (`purchaseID`, сырой
`ecommerce`) — бэкенд стал бы читателем трекера ровно там, где стенд учит, что
это разные источники; *независимая модель бэкенда* (заказ первичен, событие —
эхо) — кто купил, решает воронка, а воронка — это трафик, то есть опрокидывание
всего генератора; *слепок собирает SQL стенда из событий* — второй источник
исчезает вместе с уроком «две версии правды».
## Слепок и его доставка
**Запуск.** Третья команда того же пакета — `snapshot --day D [--days N]`,
свой приёмник, топик `orders`. Два источника — два запуска: трекер и бэкенд
видны глазами как два производителя, каждый со своим топиком. Один прогон с
двумя выходами отклонён: экономии он не даёт (со сдвигом отправки окно слепка
и сыгранный день не пересекаются вовсе), а правило «приёмник выбирается тем,
что для него назвали» ломает. Отклонены также: *отдельный пакет и образ*
библиотека на двоих ради одной команды; *генератор пишет слепок файлом, в
топик льёт даг* — второй путь доставки и второй сериализатор; *слепок едет
топиком `hits`* — убивает два режима приёма.
**Сборка окна.** Слепок дня D несёт заказы, рождённые в дни D−6…D, и
собирается переигровкой этих семи дней: заказы дня — производная всей воронки
дня, дешёвого пути к ним нет. Цена — семь проигрышей дня (~14 с) на слепок; у
начала оси окно усекается само. Отклонено: *кэш заказов на томе* — состояние
между прогонами; *окно держит хранилище* — топик перестаёт нести слепок;
*K = 1* — это страховочный срез 1 мастер-спеки, он в резерве.
**Отправка.** Слепок **снимается на границе суток, а отправляется следующим
прогоном**: даг, играющий день D, отправляет слепок дня D−1 — ночная выгрузка
бэкенда за вчера, как в бою. Содержимое слепка — чистая функция (зерно, D), от
момента отправки не зависит. Следствия:
- живой день перестаёт быть особым случаем: своего дага у него нет, слепок
живого дня отправит следующий прогон;
- пропущенный день лечится окном: слепок переснимается и даёт те же байты,
отдельного механизма самовосстановления нет;
- покупки текущего дня в сверке всегда `awaiting_order` — сюжет «вчера не
сходилось, сегодня сошлось», ради которого мастер-спека этот класс завела;
- цена — один лишний проигрыш дня на прогон (окно и сыгранный день не
пересекаются, проигрышей всегда восемь).
На старте оси дня −1 нет, поэтому прогон дня 0 не отправляет ничего; первый
слепок — дня 0 — уезжает прогоном дня 1 ([исследование
формата](../../research/2026-08-16-order-snapshot-wire-format.md)).
**Случайность.** Судьбу заказов бросает свой подпоток, ветвящийся по дню
рождения заказа: слепок несёт семь дней рождения сразу, и судьбу каждого
заказа обязан читать из его собственного дня. Вся судьба решается при
рождении, поэтому слепок любого дня — чтение готовой судьбы, а не накопление
состояния. Отклонено: *дописывать броски в конец `COMMERCE`* — правка заказа
и правка торгового поведения стали бы одним рычагом; *бросать состояние в
подпотоке дня слепка* — траектория заказа зависела бы от того, какие слепки
снимали.
## Запись на проводе
Контракт провода — на заказ один JSON-документ: деньги строками с двумя
знаками, времена RFC 3339 в UTC с миллисекундами, `items` обычным массивом —
целиком описан мастер-спекой (раздел 2); основания, отклонённые варианты и
проверка разбора — в [исследовании
формата](../../research/2026-08-16-order-snapshot-wire-format.md) (резолюция
развилки
[«Форма записи слепка на проводе»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/81)).
Сверх контракта здесь живёт одно правило: **порядок строк внутри слепка —
порядок рождения заказов, он же возрастание `order_id`**. Детерминизм даёт
его даром, а хешу слепка в описи нужен именно названный порядок.