- Зачем:
- справочники читает человек со стороны, а «пульт мира», «опись мира»,
«работник» и «сырьё» звучали шуткой или калькой времён разработки
(тикет #214, выбор владельца).
- Что:
- CONTEXT.md: статьи «Паспорт мира», «Даги модельного времени»,
«Шаговый даг», «Сырые данные», старые слова в строках «Избегать»;
пометки в ADR 0005, 0008, 0009 без переписывания решений.
- замены по README, справочникам, путеводителю, лабам, спекам,
комментариям дагов и SQL, тегу дага и панели Grafana; имена в коде
не тронуты.
- world.md и stand.md: 35 находок вычитки внесены, регистр выровнен.
- Проверка:
- make config-test; make lint в корне и в generator/; make test в
generator/ (437 passed); скрипт ссылок из #212 пуст; grep старых
слов вне ADR, research и CONTEXT.md пуст.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014QBCtU38qSHg5DvgtnKXcW
155 lines
16 KiB
Markdown
155 lines
16 KiB
Markdown
# Судьба заказа и расхождения
|
||
|
||
Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что
|
||
обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72).
|
||
|
||
## Судьба заказа
|
||
|
||
**Рамка.** Расхождение — не грязь и не шум, а часть мира: судьба заказа,
|
||
решённая при его рождении и уложенная в окно K целиком.
|
||
|
||
**Два подпотока дня.** Заказная сторона — судьба заказа: исход, моменты, дельта,
|
||
опоздание. Событийная сторона — порча событийного потока: потеря и дубль. Довод
|
||
за разделение — различимость по паспорту: правка заказной механики не двигает
|
||
хеши событий, правка событийной не двигает байты слепка, и по покрасневшим хешам
|
||
видно, какую сторону трогали. Прежние имена `DISCREPANCIES` и `LATECOMERS`
|
||
решения не переживают — они названы по классам витрины, а компонент называет
|
||
часть мира; новые стороны занимают те же позиции. Заказная сторона названа при
|
||
реализации — подпоток `ORDERS` на позиции 2; событийная — `BEACONS` на позиции
|
||
3: маячок счетчика либо не долетает, либо отправляется повторно. Отклонено:
|
||
*один компонент на всю судьбу* — правка событийной механики молча меняла бы
|
||
байты слепка; *компонент на класс* — пять имён под ручки калибровки, которые
|
||
крутятся разом.
|
||
|
||
**Правило формы, без которого разделение не работает: броски заказной стороны
|
||
делаются на полную длину дня, а не на отобранных заказах.** Иначе длина броска
|
||
становится функцией доли, и правка одной доли перебрасывает весь подпоток
|
||
после себя. Изоляцию даёт форма броска, а не число подпотоков.
|
||
|
||
**Путь по статусам.** Три исхода, все внутри окна: **оплачен**; **оплачен и
|
||
отменён**; **не оплачен и отменён**. Инвариант на выходе из окна: заказ либо
|
||
`paid`, либо `cancelled`; `created` — только промежуточное состояние. За окном
|
||
будущего у заказа нет, а заказ, навсегда застрявший в `created`, — модельная
|
||
небрежность, которой в выгрузке живого магазина соответствия нет. Поэтому
|
||
нового значения `mismatch_class` не нужно: `cancelled` покрывает обе дороги
|
||
отмены (шестое значение занято `awaiting_order`).
|
||
|
||
**Форма броска.**
|
||
|
||
1. **Исход** — таблица долей из трёх строк; доля неоплаченных пишется явной
|
||
строкой, а не оставляется читателю складывать хвост в уме.
|
||
2. **Моменты** — таблица целых весов «сколько часов от рождения — с каким
|
||
весом», строки 0…143, плюс равномерная секунда внутри часа — чтобы разности
|
||
времён аудита не давали точных равенств (урок правки #50).
|
||
3. Моментов бросается **всегда два, на полную длину дня**; у одномоментных
|
||
исходов второй выбрасывается. Где их два по существу, ранний считается
|
||
оплатой — порядок выходит сортировкой, условной точки отсчёта не нужно.
|
||
|
||
143 часа — самый узкий край окна: у заказа, рождённого в конце суток, до
|
||
последнего его слепка 144 часа. Таблица кончается там, где кончается окно у
|
||
самого невезучего: вылезти нечему, сторожа не нужно. Цена — заказ, рождённый в
|
||
начале суток, не использует почти сутки своего окна; в хвосте таблицы веса
|
||
мизерные, в данных это не видно. Форма из #71 — «вес за краем окна означает
|
||
„не оплачен никогда“» — этим отменена: такой заказ теперь отменяется, а край
|
||
окна не выражается числом часов — иначе доля неоплаченных стала бы функцией
|
||
часа покупки, и менти нашёл бы этот наклон первым же разрезом.
|
||
|
||
**Доли.** Ориентир мастер-спеки ~5% читается как доля отменённых вообще; как
|
||
она делится между двумя дорогами — строки таблицы исходов. Замер на
|
||
каноническом мире (28 дней, 2026-08-30, docs/architecture/testing.md, «Что
|
||
проверено»): отменено 5,15% заказов — 3,03% оплачен-потом-отменен, 2,12%
|
||
отменен неоплаченным; в пределах ориентира. Проверок вида «отмен от 4 до 6
|
||
процентов» не заводим.
|
||
|
||
**Что из двух дорог видно.** В `dds.order` дороги неразличимы — там последняя
|
||
версия; различает их история версий: строки STG и физические версии `ods.order`
|
||
до фоновых слияний, а в витринах — выручка дня, которая сначала выросла, потом
|
||
убыла. Полное различение не обещано: слепок — состояние на границе суток, и
|
||
оплата с отменой в один день в сырых данных неразличимы; то же у сильно
|
||
опоздавших, приехавших уже терминальными. Точную форму этого урока решает этап
|
||
4. Отклонено: *отмена только после оплаты* — неоплаченному некуда деться, кроме
|
||
как остаться брошенным; *мгновенная отмена при рождении* — «дыхание» окна на
|
||
отменах исчезает; *отмен нет вовсе* — страховочный срез 1, он в резерве.
|
||
|
||
## Классы расхождений и опоздание
|
||
|
||
Классы и ориентиры долей — [мастер-спека,
|
||
раздел 4](../../specs/2026-07-30-stand-v2-realism.md); здесь — механика
|
||
каждого.
|
||
|
||
**Дельта суммы (C) — вычеркнутая позиция.** Товара не оказалось в наличии,
|
||
позицию сняли: у заказа на одну позицию меньше, чем в клиентских массивах, а
|
||
`items_total` меньше на её полную стоимость. После вычёркивания деньги заказа
|
||
считаются от оставшейся корзины по [общему
|
||
правилу](snapshot.md#откуда-берётся-заказ), поэтому `total` остаётся
|
||
неотрицательным по построению. Момента у дельты нет — заказ приезжает урезанным
|
||
во всех своих слепках: первый слепок снимается на границе суток, когда склад
|
||
заказ уже собрал. Заказ из одной позиции дельты не получает — пустых заказов
|
||
не бывает. Позиция выбирается равновероятно: корреляция со спросом на доле
|
||
1–2% статистически ненаблюдаема — менти платил бы за неё таблицей чисел мира,
|
||
а увидеть не мог бы ничем. Доводы за вычёркивание: остаток — сотни рублей, он
|
||
торчит в витрине сверки сам; расхождение объясняется сравнением позиций —
|
||
разбором вложенного JSON и `ARRAY JOIN`, ровно тем навыком, ради которого
|
||
позиции разбираются; история рассказывается словами без легенды про генератор.
|
||
Отклонено: *сохранять скидку исходной корзины и отбирать только безопасные
|
||
позиции* — отсутствие товара стало бы зависеть от купона и доставки, а выбор
|
||
позиции перестал бы быть равновероятным; *разрешить отрицательный `total` или
|
||
ослабить строгий приём* — ошибка источника превратилась бы в брак, который
|
||
молча терпит хранилище; *переоценка позиции* и *другое количество* — дельта в
|
||
десятки рублей, её надо захотеть заметить; *чистая дельта без истории* —
|
||
тупик, объяснить нечем; *врёт клиент, а не бэкенд* — заказ у нас проекция той
|
||
же корзины.
|
||
|
||
**Потеря события (B) — точечная.** Уходит строка `purchase`, просмотр
|
||
`/confirmation` остаётся: события уезжают разными запросами, потерять один и
|
||
сохранить другой — обычное дело. Единственный вариант, при котором потеря
|
||
видна со стороны трекера: до подтверждения дошли сто, покупок девяносто семь.
|
||
|
||
**Дубль события (D) — сюжетный.** Обновление страницы шлёт и просмотр, и
|
||
покупку. Довод не в связности легенды: точечный дубль ломал бы урок соседнего
|
||
класса — разрыв воронки, на котором держится потеря, сжался бы втрое; при
|
||
сюжетном разрыв снова равен доле потерь. `purchaseID`, суммы и позиции у дубля
|
||
один в один — посчитал наивно, удвоил выручку. Дубль случается только там,
|
||
где пауза до следующего визита куки длиннее таймаута и задержки дубля вместе:
|
||
иначе сборка сессий у менти разошлась бы с `VisitID` — сломался бы эталон,
|
||
ради которого `VisitID` в потоке лежит. Задержка дубля —
|
||
секунды-минуты, короче таймаута визита, поэтому `VisitID` тот же. Полночь
|
||
режет дубль парой — просмотр вместе с покупкой, по тому же правилу, что у
|
||
подтверждения с торговым хвостом.
|
||
Отклонено: *обе точечные* — дубль затирает урок потери; *обе сюжетные* —
|
||
потеря перестаёт быть видна со стороны трекера.
|
||
|
||
**Гарантия моста сильнее порчи.** Назначенные планом покупки не теряются, и
|
||
назначенные планом заказы не опаздывают: `dds.identity_map` строится из моста
|
||
«`purchase` ↔ заказ», и выброшенное событие — как и заказ, не попавший ни в
|
||
один снятый слепок, — уносит куку из карты. Это был бы отказ лабы склейки, а
|
||
не расхождение в данных; менти различить не может.
|
||
|
||
**Опоздание — заказ прячется от ранних слепков.** `created_at` не
|
||
подделывается — строка создана, когда заказ родился, — но в слепках дней
|
||
d…d+δ−1 её нет, а с d+δ она появляется в том состоянии, до которого заказ
|
||
дожил: отменённый на второй день и опоздавший на третий приедет в первом же
|
||
своём слепке как `cancelled` — «выгрузка догоняет жизнь». Таблица задержки
|
||
состоит из трёх строк: δ = 0/1/2 с весами 90/5/5; 1 и 2 — это D+1 и D+2
|
||
мастер-спеки. Бросок идёт последним в подпотоке `ORDERS`; затем δ назначенного
|
||
планом заказа обнуляется. Меряется задержка в днях снятия слепка, а не
|
||
отправки: иначе сдвиг отправки удвоился бы, и обещанные D+1/D+2 стали бы
|
||
D+2/D+3. Дальше таблица не идёт: заказ с δ = 6 приехал бы ровно в одном
|
||
слепке, и обещание «пропущенный день ничего не ломает» на нём перестало бы
|
||
быть верным; при δ ≤ 2 у всякого заказа слепков не меньше пяти.
|
||
Легенда: заказ ушёл в ручную обработку и попал в выгрузку позже. Отклонено:
|
||
*сдвиг `created_at`* — подделка аудита источника: день создания строки
|
||
разошёлся бы с днём покупки, чьё равенство держит синхронная модель
|
||
([откуда берётся заказ](snapshot.md)), заказ уехал бы в чужую партицию,
|
||
и «выручка дня D» перестала бы отвечать покупкам дня D — сломалась бы та
|
||
самая сверка, ради которой всё строится.
|
||
|
||
**Пересечения.** Броски независимы, пересечения выходят арифметикой, приоритет
|
||
мастер-спеки работает по-настоящему. Исключений два, и оба названы выше: дубль
|
||
решается только у выживших покупок, а назначенное планом не теряется и не
|
||
опаздывает — дельта и отмена ему разрешены, моста они не рвут.
|
||
Следствие для калибровки: брошенная доля и наблюдаемая в сверке — разные числа
|
||
(часть заказов забирают победители по приоритету, часть у класса C недоступна —
|
||
однопозиционных заказов больше половины). Отклонено: *один класс на заказ* —
|
||
приоритет в SQL стал бы мёртвой веткой, которую менти читает как живую.
|