Files
clickstream-data-platform/docs/architecture/orders/fate.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

141 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Судьба заказа и расхождения
Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что
обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72).
## Судьба заказа
**Рамка.** Расхождение — не грязь и не шум, а часть мира: судьба заказа,
решённая при его рождении и уложенная в окно K целиком.
**Два подпотока дня.** Заказная сторона — судьба заказа: исход, моменты,
дельта, опоздание. Событийная сторона — порча событийного потока: потеря и
дубль. Довод за разделение — различимость по описи: правка заказной механики
не двигает хеши событий, правка событийной не двигает байты слепка, и по
покрасневшим хешам видно, какую сторону трогали. Прежние имена `DISCREPANCIES`
и `LATECOMERS` решения не переживают — они названы по классам витрины, а
компонент называет часть мира; новые стороны занимают те же позиции, имена —
при реализации. Отклонено: *один компонент на всю судьбу* — правка событийной
механики молча меняла бы байты слепка; *компонент на класс* — пять имён под
ручки калибровки, которые крутятся разом.
**Правило формы, без которого разделение не работает: броски заказной стороны
делаются на полную длину дня, а не на отобранных заказах.** Иначе длина броска
становится функцией доли, и правка одной доли перебрасывает весь подпоток
после себя. Изоляцию даёт форма броска, а не число подпотоков.
**Путь по статусам.** Три исхода, все внутри окна: **оплачен**; **оплачен и
отменён**; **не оплачен и отменён**. Инвариант на выходе из окна: заказ либо
`paid`, либо `cancelled`; `created` — только промежуточное состояние. За окном
будущего у заказа нет, а заказ, навсегда застрявший в `created`, — модельная
небрежность, которой в выгрузке живого магазина соответствия нет. Поэтому
нового значения `mismatch_class` не нужно: `cancelled` покрывает обе дороги
отмены (шестое значение занято `awaiting_order`).
**Форма броска.**
1. **Исход** — таблица долей из трёх строк; доля неоплаченных пишется явной
строкой, а не оставляется читателю складывать хвост в уме.
2. **Моменты** — таблица целых весов «сколько часов от рождения — с каким
весом», строки 0…143, плюс равномерная секунда внутри часа — чтобы разности
времён аудита не давали точных равенств (урок правки #50).
3. Моментов бросается **всегда два, на полную длину дня**; у одномоментных
исходов второй выбрасывается. Где их два по существу, ранний считается
оплатой — порядок выходит сортировкой, условной точки отсчёта не нужно.
143 часа — самый узкий край окна: у заказа, рождённого в конце суток, до
последнего его слепка 144 часа. Таблица кончается там, где кончается окно у
самого невезучего: вылезти нечему, сторожа не нужно. Цена — заказ, рождённый в
начале суток, не использует почти сутки своего окна; в хвосте таблицы веса
мизерные, в данных это не видно. Форма из #71 — «вес за краем окна означает
„не оплачен никогда“» — этим отменена: такой заказ теперь отменяется, а край
окна не выражается числом часов — иначе доля неоплаченных стала бы функцией
часа покупки, и менти нашёл бы этот наклон первым же разрезом.
**Доли.** Ориентир мастер-спеки ~5% читается как доля отменённых вообще; как
она делится между двумя дорогами — строки таблицы исходов. Точные числа —
калибровка этапа 7; проверок вида «отмен от 4 до 6 процентов» не заводим.
**Что из двух дорог видно.** В `dds.order` дороги неразличимы — там последняя
версия; различает их история версий: сырьё STG и физические версии
`ods.order_snapshot` до фоновых слияний, а в витринах — выручка дня, которая
сначала выросла, потом убыла. Полное различение не обещано: слепок — состояние
на границе суток, и оплата с отменой в один день в сырье неразличимы; то же у
сильно опоздавших, приехавших уже терминальными. Точную форму этого урока
решает этап 4. Отклонено: *отмена только после оплаты* — неоплаченному некуда
деться, кроме как остаться брошенным; *мгновенная отмена при рождении*
«дыхание» окна на отменах исчезает; *отмен нет вовсе* — страховочный срез 1,
он в резерве.
## Классы расхождений и опоздание
Классы и ориентиры долей — [мастер-спека,
раздел 4](../../specs/2026-07-30-stand-v2-realism.md); здесь — механика
каждого.
**Дельта суммы (C) — вычеркнутая позиция.** Товара не оказалось в наличии,
позицию сняли: у заказа на одну позицию меньше, чем в клиентских массивах, а
`items_total` меньше на её стоимость. Момента у неё нет — заказ приезжает
урезанным во всех своих слепках: первый слепок снимается на границе суток,
когда склад заказ уже собрал. Заказ из одной позиции дельты не получает —
пустых заказов не бывает. Позиция выбирается равновероятно: корреляция со
спросом на доле 1–2% статистически ненаблюдаема — менти платил бы за неё
таблицей чисел мира, а увидеть не мог бы ничем. Доводы за вычёркивание:
остаток — сотни рублей, он торчит в витрине сверки сам; расхождение
объясняется сравнением позиций — разбором вложенного JSON и `ARRAY JOIN`,
ровно тем навыком, ради которого позиции разбираются; история рассказывается
словами без легенды про генератор. Отклонено: *переоценка позиции* и *другое
количество* — дельта в десятки рублей, её надо захотеть заметить; *чистая
дельта без истории* — тупик, объяснить нечем; *врёт клиент, а не бэкенд*
заказ у нас проекция той же корзины.
**Потеря события (B) — точечная.** Уходит строка `purchase`, просмотр
`/confirmation` остаётся: события уезжают разными запросами, потерять один и
сохранить другой — обычное дело. Единственный вариант, при котором потеря
видна со стороны трекера: до подтверждения дошли сто, покупок девяносто семь.
**Дубль события (D) — сюжетный.** Обновление страницы шлёт и просмотр, и
покупку. Довод не в связности легенды: точечный дубль ломал бы урок соседнего
класса — разрыв воронки, на котором держится потеря, сжался бы втрое; при
сюжетном разрыв снова равен доле потерь. `purchaseID`, суммы и позиции у дубля
один в один — посчитал наивно, удвоил выручку. Дубль случается только там, где
до следующего визита куки остаётся запас сверх таймаута: иначе сборка сессий у
менти разошлась бы с `VisitID` — сломался бы эталон, ради которого `VisitID`
в потоке лежит. Задержка дубля — секунды-минуты, короче таймаута визита,
поэтому `VisitID` тот же. Полночь режет дубль парой — просмотр вместе с
покупкой, по тому же правилу, что у подтверждения с торговым хвостом.
Отклонено: *обе точечные* — дубль затирает урок потери; *обе сюжетные*
потеря перестаёт быть видна со стороны трекера.
**Гарантия моста сильнее порчи.** Назначенные планом покупки не теряются, и
назначенные планом заказы не опаздывают: `dds.identity_map` строится из моста
«`purchase` ↔ заказ», и выброшенное событие — как и заказ, не попавший ни в
один снятый слепок, — уносит куку из карты. Это был бы отказ лабы склейки, а
не расхождение в данных; менти различить не может.
**Опоздание — заказ прячется от ранних слепков.** `created_at` не
подделывается — строка создана, когда заказ родился, — но в слепках дней
d…d+δ−1 её нет, а с d+δ она появляется в том состоянии, до которого заказ
дожил: отменённый на второй день и опоздавший на третий приедет в первом же
своём слепке как `cancelled` — «выгрузка догоняет жизнь». Задержка — таблица
весов из трёх строк: 0 на подавляющем весе, 1 и 2 — это и есть «D+1/D+2»
мастер-спеки. Меряется она в днях снятия слепка, а не отправки: иначе сдвиг
отправки удвоился бы, и обещанные D+1/D+2 стали бы D+2/D+3. Дальше таблица не
идёт: заказ с δ = 6 приехал бы ровно в одном слепке, и обещание «пропущенный
день ничего не ломает» на нём перестало бы быть верным; при δ ≤ 2 у всякого
заказа слепков не меньше пяти. Легенда: заказ ушёл в ручную обработку и попал
в выгрузку позже. Отклонено: *сдвиг `created_at`* — подделка аудита источника:
день создания строки разошёлся бы с днём покупки, чьё равенство держит
синхронная модель ([откуда берётся заказ](snapshot.md)), заказ уехал бы в
чужую партицию, и «выручка дня D» перестала бы отвечать покупкам дня D —
сломалась бы та самая сверка, ради которой всё строится.
**Пересечения.** Броски независимы, пересечения выходят арифметикой, приоритет
мастер-спеки работает по-настоящему. Исключений два, и оба названы выше: дубль
решается только у выживших покупок, а назначенное планом не теряется и не
опаздывает — дельта и отмена ему разрешены, моста они не рвут.
Следствие для калибровки: брошенная доля и наблюдаемая в сверке — разные числа
(часть заказов забирают победители по приоритету, часть у класса C недоступна —
однопозиционных заказов больше половины). Отклонено: *один класс на заказ*
приоритет в SQL стал бы мёртвой веткой, которую менти читает как живую.