From 1267b45f477ef4bc80c427fdb6a06db5cc1b41a9 Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 25 Aug 2026 11:35:31 -0400 Subject: [PATCH 1/2] =?UTF-8?q?feat(generator):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D1=8B=20=D1=81=D1=87=D1=91=D1=82=D1=87?= =?UTF-8?q?=D0=B8=D0=BA=D0=B8=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Зачем: - дать описи контрольные числа для закрытых дней заказного окна; - зафиксировать словарь наблюдаемых классов без изменения самого мира. Что: - посчитаны match, cancelled и amount_delta с приоритетом отмены; - счётчики добавлены только к дням с закрытым окном; - опись, тесты и документация приведены к новому формату. Проверка: - make lint; - make typecheck; - make test — 429 passed; - uv run pytest tests/test_inventory.py -q — 3 passed после ревью. --- README.md | 5 +-- data/world-inventory.json | 7 +++- docs/architecture/orders/inventory.md | 10 +++--- docs/specs/2026-07-30-stand-v2-realism.md | 8 +++-- generator/README.md | 3 +- .../src/clickstream_generator/inventory.py | 34 +++++++++++++++---- generator/tests/test_inventory.py | 12 +++++++ 7 files changed, 61 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a9438d8..3bd7a3c 100644 --- a/README.md +++ b/README.md @@ -151,8 +151,9 @@ Airflow его явную пересборку: удалит прикладны репозитории значило бы держать там кэш. Вместо него лежит **опись мира**, [`data/world-inventory.json`](data/world-inventory.json): зерно, версия генератора, хеш каталога товаров и по строке на каждый день — дата, число -событий и хеш его байтов. Опись отвечает на единственный вопрос: тот ли это -мир, что был вчера. +событий и хеш его байтов. У дней с закрытым окном там же лежат счётчики +классов расхождений по заказам. Опись отвечает на единственный вопрос: тот ли +это мир, что был вчера. Спрашивают её двое. `make test` в `generator/` сверяет опись с тем, что собирается из кода сегодня: правка генератора меняет мир, и опись надо diff --git a/data/world-inventory.json b/data/world-inventory.json index 80d5e67..8c610a6 100644 --- a/data/world-inventory.json +++ b/data/world-inventory.json @@ -7,7 +7,12 @@ "day": 0, "date": "2026-06-01", "events": 50626, - "sha256": "81ad7f402bdd4c8b56425f6439a024b9996e5ca95dd207d444290d20a70ed407" + "sha256": "81ad7f402bdd4c8b56425f6439a024b9996e5ca95dd207d444290d20a70ed407", + "orders": { + "match": 197, + "cancelled": 20, + "amount_delta": 6 + } }, { "day": 1, diff --git a/docs/architecture/orders/inventory.md b/docs/architecture/orders/inventory.md index 5ddba60..7c4d95d 100644 --- a/docs/architecture/orders/inventory.md +++ b/docs/architecture/orders/inventory.md @@ -16,11 +16,11 @@ артефакт мира. Побайтовое обещание («слепок переснимается и даёт те же байты») опись начинает сторожить. - **Счётчики классов расхождений — наблюдаемых, после приоритета**, по - итоговой судьбе заказов дня; единица счёта — заказ, и опись называет её - словом. Сойтись с запросом менти они могут только на днях с закрытым окном: - при N сыгранных днях таких N − 7 (день d закрывается слепком d + 6, а - последний отправленный слепок несёт день N − 2). Читатель у них придёт - этапом 4 — проверка сверки; не окажется читателя — та же бритва режет и их. + итоговой судьбе заказов дня. В строке закрытого дня поле `orders` хранит + число заказов каждого класса; пока окно открыто, поля нет. При N сыгранных + днях закрыты N − 7 (день d закрывается слепком d + 6, а последний + отправленный слепок несёт день N − 2). Для этих счётчиков запланирована + проверка `classes_vs_inventory` из DQ-звена этапа 4. - **Контрольные числа идентичности не заводятся вовсе**: uniq известных пользователей и число двухкуковых пар растут, пока мир едет, — опоры из них не выходит; генератор сторожит хеш, транспорт — счёт событий. diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index 89277ad..c55762b 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -535,9 +535,11 @@ Kafka день переигрывается генератором заново: - по строке на каждый отправленный слепок — хеш байтов: побайтовое обещание «слепок переснимается и даёт те же байты» опись сторожит наравне с днями; -- заказная сторона: заказы, выручка и счётчики наблюдаемых классов - расхождений после приоритета, в заказах — по дням с закрытым окном - (самопроверка лабы сверки; при N сыгранных днях таких дней N − 7); +- заказная сторона: счётчики наблюдаемых классов расхождений после + приоритета; каждый показывает число заказов и хранится только для дня с + закрытым окном (самопроверка лабы сверки; при N сыгранных днях таких дней + N − 7). Отдельных полей для общего числа заказов и выручки в описи пока + нет: для них не назван читатель; - контрольные числа идентичности не заводятся: uniq известных пользователей и число двухкуковых покупателей растут, пока мир едет, а счётчик без названного читателя в опись не кладётся diff --git a/generator/README.md b/generator/README.md index 3f4e082..b007f43 100644 --- a/generator/README.md +++ b/generator/README.md @@ -86,7 +86,8 @@ D0 живёт предыстория, поэтому любой день соб из контракта. Документ руками не правят — пересобирают. - `src/clickstream_generator/inventory.py` — сборка описи мира ([`data/world-inventory.json`](../data/world-inventory.json)): паспорт мира - и хеши восьми дней, которыми наполняется стенд. Руками не правят — + и хеши восьми дней, которыми наполняется стенд, плюс счётчики классов + расхождений по заказам для дней с закрытым окном. Руками не правят — пересобирают целью `make inventory`. - `tests/` — инварианты контракта, свежесть описания и обещания мира: чистота от зерна, приток, гарантия двухкуковых пар, форма суточной волны diff --git a/generator/src/clickstream_generator/inventory.py b/generator/src/clickstream_generator/inventory.py index 9811183..51b1f08 100644 --- a/generator/src/clickstream_generator/inventory.py +++ b/generator/src/clickstream_generator/inventory.py @@ -1,10 +1,10 @@ """Опись мира: чем стенд наполняется при подъёме и каким это обязано выйти. Мир — чистая функция зерна (спека генератора, раздел 2), поэтому в git лежит не -он сам, а опись: паспорт мира, число событий по дням и хеш байтов каждого дня. -Сам мир пересчитывается когда угодно, а опись отвечает на единственный вопрос — -**тот ли это мир, что был вчера**. Разошлись хеши — мир уехал, и дальше уже -неважно, чего от него ждали проверки. +он сам, а опись: паспорт мира, число событий и хеш байтов каждого дня, счётчики +классов расхождений по заказам. Сам мир пересчитывается когда угодно, а опись +отвечает на единственный вопрос — **тот ли это мир, что был вчера**. Разошлись +хеши — мир уехал, и дальше уже неважно, чего от него ждали проверки. Дней в описи восемь: столько заливается в стенд при `make up`. Понедельник по понедельник — полная неделя с выходными и первый замкнутый цикл окна K = 7. @@ -43,9 +43,9 @@ from importlib.metadata import version from pathlib import Path from typing import Any +from clickstream_generator import commerce, serialize, world from clickstream_generator import day as day_module from clickstream_generator import orders as orders_module -from clickstream_generator import serialize, world from clickstream_generator.catalog import CATALOG_PATH from clickstream_generator.seeds import CANONICAL_SEED @@ -66,7 +66,10 @@ def build() -> dict[str, Any]: orders = [] for number in range(STARTING_DAYS): today = day_module.stream(CANONICAL_SEED, number) - days.append(_day(number, today)) + row = _day(number, today) + if number < STARTING_DAYS - world.ORDER_WINDOW_DAYS: + row["orders"] = _order_class_counts(today) + days.append(row) orders.append(today.orders) return { @@ -115,6 +118,25 @@ def _snapshot(number: int, orders: list[orders_module.Orders]) -> dict[str, Any] } +def _order_class_counts(today: day_module.Day) -> dict[str, int]: + """Заказы дня по итоговому классу: отмена перевешивает дельту суммы.""" + purchase = today.columns["EventType"] == commerce.PURCHASE + declared = today.columns["purchaseRevenue"][purchase] + counts = {"match": 0, "cancelled": 0, "amount_delta": 0} + + for outcome, items_total, revenue in zip( + today.orders.outcome, today.orders.items_total, declared, strict=True + ): + if outcome != orders_module.OrderOutcome.PAID: + counts["cancelled"] += 1 + elif items_total != round(revenue[0] * commerce.KOPECKS): + counts["amount_delta"] += 1 + else: + counts["match"] += 1 + + return counts + + def _date(number: int) -> str: return (world.ORIGIN + timedelta(days=number)).isoformat() diff --git a/generator/tests/test_inventory.py b/generator/tests/test_inventory.py index 3e9b088..fbe303a 100644 --- a/generator/tests/test_inventory.py +++ b/generator/tests/test_inventory.py @@ -17,7 +17,9 @@ from pathlib import Path import pytest from clickstream_generator import cli +from clickstream_generator import day as day_module from clickstream_generator.inventory import STARTING_DAYS, build +from clickstream_generator.seeds import CANONICAL_SEED INVENTORY_PATH = Path(__file__).resolve().parents[2] / "data" / "world-inventory.json" @@ -41,6 +43,16 @@ def test_inventory_is_up_to_date(inventory: dict): ) +def test_order_class_counters_cover_only_closed_days(inventory: dict): + """Итог заказа становится опорой только после закрытия его окна.""" + rows = [row for row in inventory["days"] if "orders" in row] + + assert [row["day"] for row in rows] == [0] + assert set(rows[0]["orders"]) == {"match", "cancelled", "amount_delta"} + today = day_module.stream(CANONICAL_SEED, rows[0]["day"]) + assert sum(rows[0]["orders"].values()) == len(today.orders) + + def test_the_inventory_holds_the_hash_of_every_sent_snapshot(inventory: dict, tmp_path): """У каждого отправленного слепка — своя строка с хешем его байтов. From f45ad53b7f4999fd18e609c5b70b79e54218ebdb Mon Sep 17 00:00:00 2001 From: Dmitry Dementev Date: Tue, 25 Aug 2026 11:58:54 -0400 Subject: [PATCH 2/2] =?UTF-8?q?docs(dq):=20classes=5Fvs=5Finventory=20?= =?UTF-8?q?=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD=D0=B0=20=D1=82=D0=B5=D1=85?= =?UTF-8?q?=D0=BD=D0=B8=D1=87=D0=B5=D1=81=D0=BA=D0=BE=D0=B9=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Зачем: - сверка с описью подглядывает внутрь генератора: у настоящего магазина такого оракула нет, и выдавать её за учебный урок DQ нельзя (развилка из комментария к #157). - Что: - в dm.md проверка счётчиков классов помечена технической, её обещание сужено честно: до витрин доехал рождённый мир; верно ли мир рождён, проверка не видит — опись считает тот же генератор. - в §8 мастер-спеки читатель счётчиков «самопроверка лабы сверки» заменён на техническую проверку classes_vs_inventory звена DQ. - словарь: короткий термин «Техническая проверка» в CONTEXT.md — только язык, решение живёт в dm.md; та же формулировка читателя в inventory.md. - Проверка: - grep -rn "лабы сверки" docs/ — упоминаний не осталось. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 5 +++++ docs/architecture/dm.md | 6 +++++- docs/architecture/orders/inventory.md | 2 +- docs/specs/2026-07-30-stand-v2-realism.md | 7 ++++--- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index fd73d35..b7135b1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -209,3 +209,8 @@ _Избегать_: описание схемы, документация кон потоковый приём событий. _Избегать_: путать с приёмником и пакетным режимом — те про сторону генератора, про отправку; забор — про сторону хранилища, про чтение. + +**Техническая проверка**: +Проверка, чей оракул — внутренний эталон стенда (опись мира), которого у +настоящего магазина нет. За учебный урок DQ не выдаётся. +_Избегать_: DQ-проверка — та работает на наблюдаемых данных магазина. diff --git a/docs/architecture/dm.md b/docs/architecture/dm.md index 7371074..1e96218 100644 --- a/docs/architecture/dm.md +++ b/docs/architecture/dm.md @@ -87,7 +87,11 @@ Airflow. Партиция — день данных; прогон звена DQ - счётная сверка сессий с эталоном `VisitID` — кука × день, эталон читается из `ods.event_v` ([модель сессии](dds/session.md)); - счётчики классов расхождений против описи мира — по дням с закрытым - окном; + окном. Проверка техническая, не урок DQ: её оракул — опись генератора, + а у настоящего магазина оракула нет. Обещание узкое и честное: до + витрин доехал тот мир, который родился, — одна растяжка через весь + путь вместо сторожей на каждом шаге. Верно ли сам мир рождён, она + не видит: опись считает тот же генератор; - счётные сверки витрин стенда с их DDS-источниками. Результаты ложатся в `dm.dq_summary`. Звено — не только сторож конвейера, diff --git a/docs/architecture/orders/inventory.md b/docs/architecture/orders/inventory.md index 7c4d95d..6362a66 100644 --- a/docs/architecture/orders/inventory.md +++ b/docs/architecture/orders/inventory.md @@ -19,7 +19,7 @@ итоговой судьбе заказов дня. В строке закрытого дня поле `orders` хранит число заказов каждого класса; пока окно открыто, поля нет. При N сыгранных днях закрыты N − 7 (день d закрывается слепком d + 6, а последний - отправленный слепок несёт день N − 2). Для этих счётчиков запланирована + отправленный слепок несёт день N − 2). Их читатель — техническая проверка `classes_vs_inventory` из DQ-звена этапа 4. - **Контрольные числа идентичности не заводятся вовсе**: uniq известных пользователей и число двухкуковых пар растут, пока мир едет, — опоры из них diff --git a/docs/specs/2026-07-30-stand-v2-realism.md b/docs/specs/2026-07-30-stand-v2-realism.md index c55762b..e9e2fdf 100644 --- a/docs/specs/2026-07-30-stand-v2-realism.md +++ b/docs/specs/2026-07-30-stand-v2-realism.md @@ -537,9 +537,10 @@ Kafka день переигрывается генератором заново: «слепок переснимается и даёт те же байты» опись сторожит наравне с днями; - заказная сторона: счётчики наблюдаемых классов расхождений после приоритета; каждый показывает число заказов и хранится только для дня с - закрытым окном (самопроверка лабы сверки; при N сыгранных днях таких дней - N − 7). Отдельных полей для общего числа заказов и выручки в описи пока - нет: для них не назван читатель; + закрытым окном (читатель — техническая проверка `classes_vs_inventory` + звена DQ; при N сыгранных днях таких дней N − 7). Отдельных полей для + общего числа заказов и выручки в описи пока нет: для них не назван + читатель; - контрольные числа идентичности не заводятся: uniq известных пользователей и число двухкуковых покупателей растут, пока мир едет, а счётчик без названного читателя в опись не кладётся