docs(orders): нарезка укрупнена до семи файлов
Зачем: девять зон ломали правило «семь плюс-минус два» — оглавление переставало помещаться в голову читателя, навигация мешала чтению. Что: projection.md влит в snapshot.md («Заказ и его слепок»), mismatch.md — в fate.md («Судьба заказа и расхождения»): каждая пара несла одну резолюцию (#71 и #72). Выжили имена, на которые смотрят внешние ссылки; внутренние перенацелены, индекс README обновлён. Проверка: grep по репозиторию — упоминаний удалённых имён нет; обход относительных ссылок набора — битых нет. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0d9a83d6af
commit
54cb52184a
5 files changed
+112
-111
No files matched your search
@@ -20,7 +20,7 @@
|
|||||||
- **Заказ — проекция, не порождение.** Торговая половина дня-функции уже
|
- **Заказ — проекция, не порождение.** Торговая половина дня-функции уже
|
||||||
посчитала корзину, цены, купон и номер заказа; заказная половина навешивает
|
посчитала корзину, цены, купон и номер заказа; заказная половина навешивает
|
||||||
судьбу и собирает слепок. Ни одного нового броска в торговом подпотоке —
|
судьбу и собирает слепок. Ни одного нового броска в торговом подпотоке —
|
||||||
[откуда берётся заказ](projection.md).
|
[откуда берётся заказ](snapshot.md).
|
||||||
- **Слепок дня D — чистая функция (зерно, D)**: состояние заказов, рождённых
|
- **Слепок дня D — чистая функция (зерно, D)**: состояние заказов, рождённых
|
||||||
в дни D−6…D, снятое на границе суток D|D+1. Отправляет его следующий
|
в дни D−6…D, снятое на границе суток D|D+1. Отправляет его следующий
|
||||||
прогон — ночная выгрузка бэкенда за вчера; на проводе — один JSON-документ
|
прогон — ночная выгрузка бэкенда за вчера; на проводе — один JSON-документ
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
- **Расхождения и опоздания — часть мира, а не грязь**: два подпотока —
|
- **Расхождения и опоздания — часть мира, а не грязь**: два подпотока —
|
||||||
заказная и событийная стороны; броски независимы, пересечения выходят
|
заказная и событийная стороны; броски независимы, пересечения выходят
|
||||||
арифметикой, приоритет классов работает по-настоящему —
|
арифметикой, приоритет классов работает по-настоящему —
|
||||||
[классы расхождений и опоздание](mismatch.md).
|
[классы расхождений и опоздание](fate.md).
|
||||||
- **Опись хранит только то, чего движение мира не меняет**: хеш байтов
|
- **Опись хранит только то, чего движение мира не меняет**: хеш байтов
|
||||||
каждого слепка и счётчики наблюдаемых классов —
|
каждого слепка и счётчики наблюдаемых классов —
|
||||||
[что хранит опись](inventory.md).
|
[что хранит опись](inventory.md).
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# Судьба заказа
|
# Судьба заказа и расхождения
|
||||||
|
|
||||||
Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что
|
Резолюция развилки [«Расхождения A–D и опоздания: механика, доли и что
|
||||||
обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72).
|
обещано»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/72).
|
||||||
|
|
||||||
|
## Судьба заказа
|
||||||
|
|
||||||
**Рамка.** Расхождение — не грязь и не шум, а часть мира: судьба заказа,
|
**Рамка.** Расхождение — не грязь и не шум, а часть мира: судьба заказа,
|
||||||
решённая при его рождении и уложенная в окно K целиком.
|
решённая при его рождении и уложенная в окно K целиком.
|
||||||
|
|
||||||
@@ -64,3 +66,75 @@
|
|||||||
деться, кроме как остаться брошенным; *мгновенная отмена при рождении* —
|
деться, кроме как остаться брошенным; *мгновенная отмена при рождении* —
|
||||||
«дыхание» окна на отменах исчезает; *отмен нет вовсе* — страховочный срез 1,
|
«дыхание» окна на отменах исчезает; *отмен нет вовсе* — страховочный срез 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 стал бы мёртвой веткой, которую менти читает как живую.
|
||||||
@@ -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 стал бы мёртвой веткой, которую менти читает как живую.
|
|
||||||
@@ -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 стенда из событий* — второй источник
|
|
||||||
исчезает вместе с уроком «две версии правды».
|
|
||||||
@@ -1,8 +1,42 @@
|
|||||||
# Слепок и его доставка
|
# Заказ и его слепок
|
||||||
|
|
||||||
Резолюция развилки [«Генератор слепков: где живёт и чем связан с
|
Резолюция развилки [«Генератор слепков: где живёт и чем связан с
|
||||||
событиями»](https://git.dementev.space/ddmitry/clickstream-data-platform/issues/71).
|
событиями»](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]`,
|
**Запуск.** Третья команда того же пакета — `snapshot --day D [--days N]`,
|
||||||
свой приёмник, топик `orders`. Два источника — два запуска: трекер и бэкенд
|
свой приёмник, топик `orders`. Два источника — два запуска: трекер и бэкенд
|
||||||
видны глазами как два производителя, каждый со своим топиком. Один прогон с
|
видны глазами как два производителя, каждый со своим топиком. Один прогон с
|
||||||
|
|||||||
Reference in new issue
Block a user