diff --git a/docs/architecture/orders/README.md b/docs/architecture/orders/README.md index e7d771c..f956842 100644 --- a/docs/architecture/orders/README.md +++ b/docs/architecture/orders/README.md @@ -20,7 +20,7 @@ - **Заказ — проекция, не порождение.** Торговая половина дня-функции уже посчитала корзину, цены, купон и номер заказа; заказная половина навешивает судьбу и собирает слепок. Ни одного нового броска в торговом подпотоке — - [откуда берётся заказ](projection.md). + [откуда берётся заказ](snapshot.md). - **Слепок дня D — чистая функция (зерно, D)**: состояние заказов, рождённых в дни D−6…D, снятое на границе суток D|D+1. Отправляет его следующий прогон — ночная выгрузка бэкенда за вчера; на проводе — один JSON-документ @@ -31,7 +31,7 @@ - **Расхождения и опоздания — часть мира, а не грязь**: два подпотока — заказная и событийная стороны; броски независимы, пересечения выходят арифметикой, приоритет классов работает по-настоящему — - [классы расхождений и опоздание](mismatch.md). + [классы расхождений и опоздание](fate.md). - **Опись хранит только то, чего движение мира не меняет**: хеш байтов каждого слепка и счётчики наблюдаемых классов — [что хранит опись](inventory.md). diff --git a/docs/architecture/orders/fate.md b/docs/architecture/orders/fate.md index c709906..c5534b5 100644 --- a/docs/architecture/orders/fate.md +++ b/docs/architecture/orders/fate.md @@ -1,8 +1,10 @@ -# Судьба заказа +# Судьба заказа и расхождения Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72). +## Судьба заказа + **Рамка.** Расхождение — не грязь и не шум, а часть мира: судьба заказа, решённая при его рождении и уложенная в окно K целиком. @@ -64,3 +66,75 @@ деться, кроме как остаться брошенным; *мгновенная отмена при рождении* — «дыхание» окна на отменах исчезает; *отмен нет вовсе* — страховочный срез 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 стал бы мёртвой веткой, которую менти читает как живую. diff --git a/docs/architecture/orders/mismatch.md b/docs/architecture/orders/mismatch.md deleted file mode 100644 index 5d61376..0000000 --- a/docs/architecture/orders/mismatch.md +++ /dev/null @@ -1,73 +0,0 @@ -# Классы расхождений и опоздание - -Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что -обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72). -Классы и ориентиры долей — [мастер-спека, -раздел 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`* — подделка аудита источника: -день создания строки разошёлся бы с днём покупки, чьё равенство держит -синхронная модель ([откуда берётся заказ](projection.md)), заказ уехал бы в -чужую партицию, и «выручка дня D» перестала бы отвечать покупкам дня D — -сломалась бы та самая сверка, ради которой всё строится. - -**Пересечения.** Броски независимы, пересечения выходят арифметикой, приоритет -мастер-спеки работает по-настоящему. Исключений два, и оба названы выше: дубль -решается только у выживших покупок, а назначенное планом не теряется и не -опаздывает — дельта и отмена ему разрешены, моста они не рвут. -Следствие для калибровки: брошенная доля и наблюдаемая в сверке — разные числа -(часть заказов забирают победители по приоритету, часть у класса C недоступна — -однопозиционных заказов больше половины). Отклонено: *один класс на заказ* — -приоритет в SQL стал бы мёртвой веткой, которую менти читает как живую. diff --git a/docs/architecture/orders/projection.md b/docs/architecture/orders/projection.md deleted file mode 100644 index 5d68938..0000000 --- a/docs/architecture/orders/projection.md +++ /dev/null @@ -1,34 +0,0 @@ -# Откуда берётся заказ - -Резолюция развилки [«Генератор слепков: где живёт и чем связан с -событиями»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/71). - -Заказ и событие `purchase` — не два порождения, а две проекции одного факта -мира. Корзина, цены, купон и номер заказа посчитаны торговой половиной -дня-функции; заказная половина берёт заказы дня готовой структурой — вторым -выходом `commerce`, — навешивает на них жизнь заказа и собирает слепок. В -подпоток `COMMERCE` не добавляется ни одного нового броска: мир не сдвигается, -согласованность двух источников не удерживается, а получается по построению. - -Следствия, которые уже решены соседями и здесь только связываются: - -- у всякого заказа изначально ровно одно событие `purchase`; заказ без события - в трекере — не отдельная порода, а класс B, и делает его событийная сторона - выбрасыванием события после присвоения номера - ([классы расхождений](mismatch.md)); -- номер заказа общий у обеих проекций: `order_id` = клиентский `purchaseID`, - читаемый номер «день и порядковый номер покупки» ([спека - генератора](../../specs/2026-08-01-generator.md), раздел 9); нумеруются все - покупки, дошедшие до потока дня, — до всяких потерь; -- скидка заказа выводится из промокода события по таблице «код → скидка» — - числу мира, которое этап 3 берёт готовым (спека генератора, разделы 8 и 9). - -В модели строка заказа в базе источника создаётся синхронно с покупкой, -поэтому день рождения заказа и день создания строки совпадают. - -Отклонено: *выводить заказ разбором собственного вывода* (`purchaseID`, сырой -`ecommerce`) — бэкенд стал бы читателем трекера ровно там, где стенд учит, что -это разные источники; *независимая модель бэкенда* (заказ первичен, событие — -эхо) — кто купил, решает воронка, а воронка — это трафик, то есть опрокидывание -всего генератора; *слепок собирает SQL стенда из событий* — второй источник -исчезает вместе с уроком «две версии правды». diff --git a/docs/architecture/orders/snapshot.md b/docs/architecture/orders/snapshot.md index 601b184..d733745 100644 --- a/docs/architecture/orders/snapshot.md +++ b/docs/architecture/orders/snapshot.md @@ -1,8 +1,42 @@ -# Слепок и его доставка +# Заказ и его слепок Резолюция развилки [«Генератор слепков: где живёт и чем связан с событиями»](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`. Два источника — два запуска: трекер и бэкенд видны глазами как два производителя, каждый со своим топиком. Один прогон с