docs(orders): спека превращена в принятый живой набор architecture/orders
Зачем: датированная спека — событие истории, а устройство компонента — живой документ; большое полотно плохо грузится и агентом, и человеком (ADR 0011). Что: вычитание #84 слито с переустройством формы: набор docs/architecture/orders/ — индекс README и семь файлов по частям устройства (нарезка по правилу «семь плюс-минус два»); датированные файлы удалены, ссылки перенацелены, AGENTS.md дополнен правилом подпапки. Приёмка владельцем #88 пройдена, черновой статус снят из README. Проверка: холодная сверка миграции свежим тредом — потерь решений нет; обход относительных ссылок набора — битых нет. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
# Судьба заказа и расхождения
|
||||
|
||||
Резолюция развилки [«Расхождения 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 стал бы мёртвой веткой, которую менти читает как живую.
|
||||
Reference in New Issue
Block a user