docs(specs): закрыты находки внешнего ревью спеки v2 (#17)

- Зачем:
  - слепое ревью постановки нашло места, где исполнителю пришлось бы
    молча изобретать решение.
- Что:
  - зафиксированы: правило соединения purchaseID[1]=order_id, исключение
    declared_* из правила «деньги по бэкенду», имена массивов позиций
    dds.order, контракт присутствия всех 46 полей, класс awaiting_order
    в сверке, счётчик дельт сумм в манифесте.
  - шардирование в разделе 6 и dds.identity_map приведено к
    cityHash64 — буквально тем же выражением, что в 1.3.
- Проверка:
  - сверка правок с индексом находок ревью (SOL-1..SOL-8, кроме
    отклонённого SOL-7).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 15:04:41 +03:00
co-authored by Claude Fable 5
parent 1fccaf8599
commit 9d3a8053d3
+23 -8
View File
@@ -196,7 +196,9 @@ Ecommerce (заполнены только у торговых событий):
Пропущенный день ничего не ломает, следующий слепок самовосстанавливает. Пропущенный день ничего не ломает, следующий слепок самовосстанавливает.
- Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше - Разбор JSON-позиций — **один раз**, в трансформации ODS → DDS; дальше
витрины работают с плоскими массивами. Это единственный носитель навыка витрины работают с плоскими массивами `dds.order`: `item_sku`
Array(String), `item_qty` Array(UInt64), `item_price` Array(Decimal(18,2))
— одной длины, порядок как в JSON. Это единственный носитель навыка
«вложенный JSON в ClickHouse» на стенде. «вложенный JSON в ClickHouse» на стенде.
- Статусы держим все три: смена `created``paid` и есть причина «дыхания» - Статусы держим все три: смена `created``paid` и есть причина «дыхания»
выручки внутри окна; сужение до двух — резервный срез 1. выручки внутри окна; сужение до двух — резервный срез 1.
@@ -212,7 +214,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
## 4. Сверка `purchase` против заказов ## 4. Сверка `purchase` против заказов
Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер Ключ: клиентский `purchaseID` = `order_id` бэкенда (магазин знает номер
заказа на `/confirmation`). Расхождения — перечислимый список, заказа на `/confirmation`). У события `purchase` массив `purchaseID` несёт
ровно один элемент (одно подтверждение — один заказ), сверка соединяет по
`purchaseID[1]`; правило зафиксировать комментарием в SQL сверки.
Расхождения — перечислимый список,
детерминированный от seed, не хаос: детерминированный от seed, не хаос:
| | Расхождение | Механика в генераторе | Ориентир доли | | | Расхождение | Механика в генераторе | Ориентир доли |
@@ -235,7 +240,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
механически дублирует B. механически дублирует B.
Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по Правило стенда: **поведение и атрибуцию считаем по трекеру, деньги — по
бэкенду**. Оно выучивается на конфликте: суммы не сойдутся, менти сам бэкенду**. Единственное разрешённое исключение — клиентская оценка выручки
под именем `declared_*` там, где атрибуция без трекера невозможна (UTM);
слово `declared` в имени — сигнал «это заявка клиента, не деньги
отчётности». Оно выучивается на конфликте: суммы не сойдутся, менти сам
раскопает почему (Float64 против Decimal, промокод, доставка, отмены). раскопает почему (Float64 против Decimal, промокод, доставка, отмены).
## 5. Анонимность и склейка идентичностей ## 5. Анонимность и склейка идентичностей
@@ -286,7 +294,10 @@ CSV в репозитории (`data/catalog/products.csv`: `sku`, `name`, `cate
`cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между `cityHash64(сырой строки)`. Урок: «какая нода читала топик — меняется между
прогонами, куда легли данные — нет». прогонами, куда легли данные — нет».
- **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные - **Приём строгий**: `input_format_skip_unknown_fields = 0`, обязательные
поля — без значений по умолчанию. Несовпадение имени поля — громкая поля — без значений по умолчанию. Контракт присутствия: генератор выдаёт
**все 46 полей в каждом событии**; «пусто» — пустой массив, пустая строка
или 0, а не отсутствие ключа в JSON. Так строгий приём уживается с
полями, пустыми по смыслу (ecommerce у `pageview`, UTM у прямого захода). Несовпадение имени поля — громкая
ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы, ошибка в `*_errors`, а не молчаливые нули: имена CamelCase регистрозависимы,
опечатка иначе не падает. опечатка иначе не падает.
- **Политика соединений**: по ключу ко-локации — обычное соединение с - **Политика соединений**: по ключу ко-локации — обычное соединение с
@@ -370,8 +381,11 @@ Kafka переобработка возможна только из эталон
в порядке приоритета — классы пересекаются, побеждает более ранний). в порядке приоритета — классы пересекаются, побеждает более ранний).
`match` — большинство строк; `amount_delta` — только необъяснённый остаток `match` — большинство строк; `amount_delta` — только необъяснённый остаток
после приведения к сравнимой базе (округления Float64, ~1–2% заказов). после приведения к сравнимой базе (округления Float64, ~1–2% заказов).
Строка «`purchase` без заказа» внутри живого окна — это опоздание, ждущее Строка «`purchase` без заказа» внутри живого окна — опоздание, ждущее
слепка, а не отдельный класс расхождения. слепка, а не расхождение: она получает служебный класс `awaiting_order`
(шестое значение `mismatch_class`, вне приоритетов расхождений). После
закрытия окна K таких строк не остаётся — сироты исключены построением
(раздел 4).
- **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру); - **`v_utm_effectiveness`** — остаётся клиентской (атрибуция по трекеру);
счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется счётчики `purchases`/`add_to_carts` оживают из таксономии, добавляется
`declared_revenue` по UTM. `declared_revenue` по UTM.
@@ -398,7 +412,7 @@ Kafka переобработка возможна только из эталон
Манифест расширяется контрольными числами: Манифест расширяется контрольными числами:
- заказная сторона: заказы и выручка по дням; манифест хранит точные - заказная сторона: заказы и выручка по дням; манифест хранит точные
счётчики по каждому классу расхождений (отмены, потери, дубли) — счётчики по каждому классу расхождений (отмены, потери, дубли, дельты сумм) —
самопроверка лабы сверки; самопроверка лабы сверки;
- идентичность: uniq кук, uniq известных пользователей, число двухкуковых - идентичность: uniq кук, uniq известных пользователей, число двухкуковых
покупателей — лаба склейки получает самопроверку. покупателей — лаба склейки получает самопроверку.
@@ -469,7 +483,8 @@ v2 стартует пустым, поэтому объём ниже — это
9. Мониторинг и runbook «keeper упал / DDL повис в очереди». 9. Мониторинг и runbook «keeper упал / DDL повис в очереди».
Критерий приёмки этапа — честный: `make up` работает и проходят Критерий приёмки этапа — честный: `make up` работает и проходят
smoke-проверки, а не «дашборд зелёный». Документация правится в PR этапа smoke-проверки, а не «дашборд зелёный». Это минимальная планка; свои
наблюдаемые критерии каждый этап получает при разбиении в /to-tickets. Документация правится в PR этапа
(правило AGENTS.md). (правило AGENTS.md).
## 10. Границы: чего не делаем ## 10. Границы: чего не делаем