feat(generator): добавлено опоздание заказов в слепках

- Зачем:
  - менти должен увидеть, как запоздавшая выгрузка меняет открытое окно, не подделывая аудит источника.
- Что:
  - заказам назначена задержка D, D+1 или D+2 последним броском подпотока ORDERS.
  - назначенный планом заказ приезжает сразу, а Woven передаёт эту гарантию именованным полем.
  - обновлены документы, проверки границ механики и эталонные хеши слепков.
- Проверка:
  - make lint, make typecheck и make test в generator: 436 тестов прошли.
  - make config-test: конфигурация прошла проверку.
  - world_live_day и следующий etl_pipeline перевели заказ 20260608-0001 из awaiting_order в match.
This commit is contained in:
ddadmin committed 2026-08-28 17:30:55 -04:00
1 parent 58e944aa87
commit 8db1c58c12
11 files changed
+186 -54

No files matched your search

+31 -6
View File
@@ -24,11 +24,13 @@
одной доли перебрасывала бы весь подпоток после себя
(docs/architecture/orders/fate.md).
**Судьба заказа — исход и его моменты**: из окна изменяемости заказ выходит
оплаченным или отменённым, а когда именно это случилось, сказано смещением в
секундах от рождения заказа. Момента, которого у исхода нет, нет и в данных:
его место занимает −1, а не ноль, — иначе «оплатили в секунду рождения» было
бы не отличить от «не оплатили вовсе».
**Судьба заказа — исход, его моменты и задержка выгрузки**. Из окна
изменяемости заказ выходит оплаченным или отменённым, а когда именно это
случилось, сказано смещением в секундах от рождения заказа. Момента, которого
у исхода нет, нет и в данных: его место занимает −1, а не ноль, — иначе
«оплатили в секунду рождения» было бы не отличить от «не оплатили вовсе».
Задержка говорит, сколько ранних слепков заказ пропустит; назначенный планом
приезжает сразу.
**Состояние на границе суток** — чтение готовой судьбы, а не накопление:
слепок дня D учитывает моменты не позже границы D|D+1 и по ним называет
@@ -62,6 +64,7 @@ _DELIVERY_CUMULATIVE = np.cumsum(
)
_OUTCOME_CUMULATIVE = np.cumsum(world.ORDER_OUTCOME_WEIGHTS)
_MOMENT_HOUR_CUMULATIVE = np.cumsum(world.ORDER_MOMENT_HOUR_WEIGHTS)
_SNAPSHOT_DELAY_CUMULATIVE = np.cumsum(world.ORDER_SNAPSHOT_DELAY_WEIGHTS)
class OrderOutcome(IntEnum):
@@ -103,12 +106,18 @@ class Orders:
outcome: NDArray[np.int64]
paid_after: NDArray[np.int64]
cancelled_after: NDArray[np.int64]
snapshot_delay: NDArray[np.int64]
def __len__(self) -> int:
return self.user_id.size
def of_day(seed: int, day: int, purchases: Purchases) -> Orders:
def of_day(
seed: int,
day: int,
purchases: Purchases,
assigned_order: NDArray[np.bool_],
) -> Orders:
"""Заказы дня `day`: его покупки, к которым бэкенд добавил свои деньги."""
rng = day_stream(seed, day, Component.ORDERS)
delivery = _delivery(rng, len(purchases))
@@ -116,6 +125,7 @@ def of_day(seed: int, day: int, purchases: Purchases) -> Orders:
product, quantity, items_total = _delta(rng, purchases)
created_at = _created_at(rng, purchases)
discount = _discount(purchases, items_total)
snapshot_delay = _snapshot_delay(rng, assigned_order)
return Orders(
day=day,
order_id=purchases.order_id,
@@ -130,6 +140,7 @@ def of_day(seed: int, day: int, purchases: Purchases) -> Orders:
outcome=outcome,
paid_after=paid_after,
cancelled_after=cancelled_after,
snapshot_delay=snapshot_delay,
)
@@ -166,6 +177,11 @@ def at_boundary(rows: Orders, day: int) -> tuple[list[str], NDArray[np.datetime6
return status.tolist(), updated
def arrived(rows: Orders, day: int) -> NDArray[np.bool_]:
"""Какие заказы уже попали в выгрузку к слепку дня `day`."""
return rows.day + rows.snapshot_delay <= day
def _boundary(day: int) -> np.datetime64:
"""Граница суток `day`|`day+1` абсолютной меткой: полночь пояса счётчика.
@@ -218,6 +234,15 @@ def _created_at(
)
def _snapshot_delay(
rng: np.random.Generator,
assigned_order: NDArray[np.bool_],
) -> NDArray[np.int64]:
"""Сколько ранних слепков пропустит заказ; назначенный приезжает сразу."""
delay = pick(rng, _SNAPSHOT_DELAY_CUMULATIVE, assigned_order.size)
return np.where(assigned_order, 0, delay)
def _moment(rng: np.random.Generator, orders: int) -> NDArray[np.int64]:
"""Момент внутри окна: час по таблице весов и равномерная секунда в нём.