Files
dzentra_bot/docs/migrations/build_060_29_architecture.md

1463 lines
74 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Build 060.29 — Market Data Access and Replay Architecture
**Статус:** Accepted
**Build:** 060.29
**Подсистема:** Market Data / Historical Access and Replay
**Дата начала:** 2026-08-02
**Дата завершения:** 2026-08-02
**Версия документа:** 1.9
---
## 1. Назначение
Build 060.29 добавляет безопасное чтение сохранённых Canonical Market
Data и их детерминированное воспроизведение.
Build опирается на:
- PostgreSQL Canonical Storage из 060.27;
- Persistent Checkpoint и Startup Recovery из 060.28;
- существующие неизменяемые Canonical `Trade`, `Quote` и `Candle`.
Итоговая цепочка должна выглядеть так:
```text
PostgreSQL Canonical Market Data
Historical Access
bounded immutable ReplayPlan
ReplaySession + Virtual Clock
Canonical consumer
```
Historical Access и Replay являются read-side подсистемами. Они не
заменяют Acquisition Runtime, не продвигают operational checkpoint и не
записывают повторно воспроизводимые события в Market Data Storage.
---
## 2. Статус подэтапов
| Подэтап | Название | Статус |
|---|---|---|
| 060.29.0 | Architecture, Boundaries and Ordering Policy | Accepted |
| 060.29.1 | Historical Access and Replay Contracts | Accepted |
| 060.29.2 | Global Replay Sequence Migration and PostgreSQL Trade History | Accepted |
| 060.29.3 | Quote/Candle History and Snapshot Plan Builder | Accepted |
| 060.29.4 | Deterministic Replay Clock | Accepted |
| 060.29.5 | Replay Session and Engine | Accepted |
| 060.29.6 | Consumer and Composition Integration | Accepted |
| 060.29.7 | PostgreSQL Replay and Failure Verification | Accepted |
| 060.29.8 | Final Regression and Acceptance | Accepted |
Подэтапы 060.29.0060.29.6 приняты после отдельных приёмочных
read-only review. Реализация 060.29.2 ограничена versioned migration 9,
совместимостью существующих writers и отдельным PostgreSQL Trade
History reader. 060.29.3 добавляет Quote/Candle History и отдельный
bounded Replay snapshot. 060.29.4 добавляет детерминированные UTC-часы
без wall clock, reset и скрытого lifecycle. На этом этапе Build оставался
`In Progress`. Контракт 060.29.5 утверждён; отдельный `ReplayEngine` не
создаётся, а принятый playback engine реализован внутри
`ReplaySession.run()`. Контракт 060.29.6 утверждён: composition использует
обязательную фабрику consumer, один синхронный blocking
`ReplaySessionFactory`, новый граф зависимостей на каждый вызов и не
выполняет автоматический startup. Реализация 060.29.6 принята после
чистого формального read-only review.
Контракт 060.29.7 утверждён как verification-only: новые тесты используют
существующий безопасный opt-in PostgreSQL harness, реальные Storage,
Historical Access, ReplayPlan Builder, Composition и Session, но не
добавляют production consumer, Bootstrap wiring или automatic startup.
Production-код заранее не изменяется.
060.29.7 принят после исправления найденных Pylance/Pyright diagnostics,
введения обязательного static gate, полной повторной регрессии и
чистого формального read-only review. Единственная production-правка
подэтапа уточняет статическое сужение уже проверенного точного union-типа
без изменения runtime-контракта или поведения Replay.
060.29.8 выполнил финальную verification-only матрицу без изменения
production-кода. Повторены статическая проверка, unit, PostgreSQL,
integration, opt-out, fixed stress и полная offline-регрессия. Три
независимых read-only review не выявили открытых findings; подэтап и
Build 060.29 приняты.
---
## 3. Исходные инварианты 060.27060.28
1. Canonical Market Data уже прошли transport validation и mapping.
2. `MarketDataStorage` остаётся фасадом только для записи.
3. Checkpoint reads остаются узкой границей Startup Hydration и не
превращаются в Historical API.
4. Production Runtime сохраняет Canonical Trade до продвижения
operational checkpoint.
5. Live и Recovery используют одного владельца записи Trades.
6. PostgreSQL pool имеет явный Application lifecycle.
7. Consistency, Recovery и Runtime не зависят от `psycopg`.
8. Trade ID использует signed 32-bit rollover-aware контракт.
9. Retention по умолчанию выключен и запускается только явно.
---
## 4. Принятые архитектурные решения
### 4.1. Отдельная read-side граница
Historical Access размещается отдельно от существующего write-only
Storage API:
```text
market_data.access contracts
PostgreSQL History adapters
```
Существующие `TradeStorageProtocol`, `QuoteStorageProtocol`,
`CandleStorageProtocol`, `TradeCheckpointStorageProtocol` и
`MarketDataStorage` не расширяются историческими запросами.
PostgreSQL implementation зависит от DB-neutral read-контрактов.
Контракты не импортируют `psycopg`, pool, Bootstrap, Runtime, Recovery,
Telegram или Trading.
### 4.2. Scope типов данных
Historical Access предусматривает чтение трёх уже существующих
Canonical таблиц:
- Trades;
- Quotes;
- Candle revisions.
Это не подключает persistent Quote/Candle consumers и не объявляет их
Production Runtime завершённым. Полная production-проверка
`Live → Storage → Historical → Replay` в этом Build обязательна для
Trades. Quote/Candle read-path проверяется на repository и PostgreSQL
уровне.
### 4.3. Совместимость Live и Replay
Live и Replay используют одинаковые Canonical classes и значения:
```text
Trade | Quote | Candle
```
PostgreSQL reader создаёт новый Canonical объект, поэтому identity с
первоначальным Live-объектом не обещается. После материализации
Historical record и созданный из него Replay event используют один и
тот же payload без повторного копирования.
Replay не создаёт `ReplayTrade`, не меняет `source` на `replay` и не
восстанавливает transport document. Технический Replay envelope не
объявляется существующим Live Runtime event-контрактом. Совместимость
гарантируется на уровне точных Canonical payload classes и значений.
### 4.4. Гарантируемый порядок
Build гарантирует детерминированную event-time хронологию Canonical
данных. Он не обещает точное повторение порядка исходных сетевых
пакетов, потому что raw transport arrival log не сохраняется.
Оси времени:
| Тип | Historical Query | Replay |
|---|---|---|
| Trade | `executed_at` | `executed_at` |
| Quote | `received_at` | `received_at` |
| Candle revision | `open_time` | `observed_at` |
`exchange_timestamp` Quote не используется как Replay time, чтобы не
создавать look-ahead. Candle revision воспроизводится в момент её
наблюдения, а не задним числом в `open_time`.
---
## 5. Архитектурная граница и направление импортов
```text
Canonical Models
↑ ↑
Historical contracts Replay contracts
↑ ↑
PostgreSQL readers → ReplayPlan builder
ReplaySession
async consumer
```
Запрещённые зависимости:
```text
access/replay → acquisition.runtime
access/replay → storage write facade
access/replay → bootstrap/application
access/replay → telegram/trading
access/replay → runtime_events/EventBus
```
Replay не вызывает `TradeStreamConsistencyController`. Иначе
воспроизведение изменило бы durable history и operational checkpoint.
---
## 6. Historical contracts
### 6.1. Time range
Все публичные запросы используют timezone-aware полуоткрытый диапазон:
```text
[start_time, end_time)
```
Время нормализуется в UTC. Требуется строгое
`start_time < end_time`.
### 6.2. Records
Historical record хранит точный Canonical payload и технические поля,
которые не должны добавляться в Canonical model:
```text
TradeHistoryRecord
├── venue
├── trade: Trade
├── first_observed_at
├── last_observed_at
├── observation_sources
├── replay_sequence
└── canonical_schema_version
QuoteHistoryRecord
├── venue
├── quote: Quote
├── observation_sources
├── replay_sequence
└── canonical_schema_version
CandleRevisionHistoryRecord
├── venue
├── candle: Candle
├── observed_at
├── is_final
├── observation_sources
├── replay_sequence
└── canonical_schema_version
```
Records являются immutable, используют `slots` и не копируют Canonical
payload.
### 6.3. Queries и pages
Каждый запрос содержит один `venue`, один canonical `symbol`, один
UTC-диапазон и bounded page limit. Candle query дополнительно содержит
case-sensitive `interval`.
Пустая страница является корректным результатом. Повреждённая строка,
неизвестная версия Canonical schema или неверный enum являются
integrity error; строка не пропускается молча.
---
## 7. Global replay sequence, ordering и keyset
### 7.1. Причина нового durable tie-breaker
Текущие таблицы не содержат подходящего immutable порядка Replay:
- signed `trade_id` нельзя сортировать обычным числовым сравнением на
границах rollover;
- `first_observed_at` может измениться при обновлении provenance;
- между разными типами данных нет общего устойчивого tie-breaker.
060.29.2 добавляет одну глобальную положительную
`replay_sequence` для Trades, Quotes и Candle revisions.
Для новых строк sequence назначается только первой durable-вставке и не
изменяется при duplicate/provenance update. Пропуски sequence после
rollback допустимы: важен порядок, а не непрерывность значений.
### 7.2. Детерминированный backfill существующих строк
До migration 060.29.2 база не сохраняла global ordinal. Поэтому точный
первоначальный transport, insertion или commit order существующих строк
восстановить невозможно.
Backfill использует один явно фиксированный порядок:
```text
Replay event_time
→ data_type rank: Trade=1, Quote=2, Candle revision=3
→ venue COLLATE "C"
→ symbol COLLATE "C"
→ полная durable identity типа
```
Полная identity:
```text
Trade: executed_at, trade_id
Quote: received_at
Candle revision: interval COLLATE "C", open_time, observed_at
```
Этот порядок воспроизводим для одинакового committed dataset, но не
выдаётся за исторический порядок сетевых пакетов. В частности, для уже
существующих Trades с одинаковым `executed_at` backfill не может
доказать первоначальный rollover order.
Для новых строк immutable sequence сохраняет порядок первой записи при
одинаковом event time. Batch writer обязан назначать sequence в порядке
входного Canonical batch, даже если блокировки строк берутся в другом
безопасном порядке. Между конкурентными транзакциями global sequence
остаётся устойчивым tie-breaker, но не объявляется commit chronology.
### 7.3. Stable order
Historical и Replay ordering используют:
```text
(event_time, replay_sequence)
```
`trade_id` не входит в cursor и order key. Для новых строк одинаковый
timestamp сохраняет durable insertion order на границах:
```text
INT32_MAX → INT32_MIN
-1 → 0
```
Для backfilled строк порядок остаётся детерминированным по policy
раздела 7.2, но не заявляется точным первоначальным rollover order.
### 7.4. Pagination
Historical Queries используют только forward keyset pagination.
`OFFSET` запрещён.
Структурированный cursor содержит:
- тип данных;
- query scope;
- исходный временной диапазон;
- последнее `event_time`;
- последнее `replay_sequence`;
- version.
Cursor другого типа, symbol, venue, interval или окна отклоняется.
Page limit можно менять между запросами с тем же cursor.
### 7.5. Shared sequence и durable schema
Migration 9 `add_global_market_data_replay_sequence` создаёт один общий
PostgreSQL sequence:
```text
market_data.replay_sequence
BIGINT
MINVALUE 1
CACHE 1
NO CYCLE
OWNED BY NONE
```
Отдельная registry/counter table не создаётся. Единственным
распределителем ordinal для новых Trades, Quotes и Candle revisions
является shared sequence. Его нельзя привязать через `OWNED BY` к одной
колонке, потому что он обслуживает сразу три таблицы.
Каждая из таблиц получает колонку `replay_sequence BIGINT`, общий
`DEFAULT nextval(...)`, `NOT NULL` и именованный `CHECK (> 0)`. После
backfill изменение уже назначенного значения запрещает database trigger
на partitioned parent. Тот же контракт обязан действовать в default,
ранее созданных monthly и будущих partitions.
Один cross-table `UNIQUE` constraint для трёх partitioned tables в
PostgreSQL недоступен. Глобальная уникальность обеспечивается shared
sequence и контролируемыми repositories. Ручная вставка явно заданного
`replay_sequence` вне этих adapters не входит в поддерживаемый контракт.
### 7.6. Блокирующая migration и атомарный backfill
Migration 9 выполняется существующим `StorageMigrationRunner` в одной
transaction. Сначала runner владеет общим
`STORAGE_MIGRATION_ADVISORY_LOCK_ID`. Затем первым statement migration 9,
до любых table locks, берётся единый
`MARKET_DATA_PARTITION_ADVISORY_LOCK_ID`. Этот же partition advisory lock
используют `PostgresMarketDataPartitionManager` и
`PostgresMarketDataRetentionService`.
Полный обязательный порядок блокировок:
```text
STORAGE_MIGRATION_ADVISORY_LOCK_ID
→ MARKET_DATA_PARTITION_ADVISORY_LOCK_ID
→ ACCESS EXCLUSIVE trades
→ ACCESS EXCLUSIVE quotes
→ ACCESS EXCLUSIVE candle_revisions
```
Так migration, Partition Manager и Retention сериализуют partition DDL
до захвата table locks. Ни один участник не может одновременно держать
lock default partition и ждать parent table, пока migration держит
parent и ждёт default. Это исключает deadlock `parent ↔ default`.
После двух advisory locks для parent tables и их partitions рекурсивно
берётся `ACCESS EXCLUSIVE` lock в одном порядке:
```text
trades → quotes → candle_revisions
```
Блокировка запрещает конкурентные writes и partition DDL до завершения
migration. Так новая строка не может появиться между построением
глобального порядка, установкой defaults и включением ограничений.
Backfill строит transaction-local mapping через один `UNION ALL` всех
трёх типов и назначает `row_number()` по policy раздела 7.2. Обновление
строк выполняется по полной durable primary identity. Использование
`ctid` и зависимость от физического порядка строк или порядка вычисления
`nextval()` запрещены.
После backfill sequence переводится за максимальное назначенное
значение. Для пустого dataset следующий вызов возвращает `1`. Только
после этого устанавливаются `DEFAULT`, `NOT NULL`, positive checks,
immutable triggers и partitioned keyset indexes:
```text
Trades: (venue, symbol, executed_at, replay_sequence)
Quotes: (venue, symbol, received_at, replay_sequence)
Candles: (venue, symbol, interval, open_time, replay_sequence)
Candles replay preparation:
(venue, symbol, interval, observed_at, replay_sequence)
```
Существующий Trade index с `trade_id` в migration 9 не удаляется.
Ошибка любого statement откатывает schema, backfill и запись версии 9;
повторный запуск начинается с согласованного состояния.
> **Внимание: migration 9 является блокирующей.** Её длительность
> пропорциональна числу сохранённых строк, стоимости backfill и созданию
> indexes. Перед применением к большой production database обязательны
> замер времени и дополнительного дискового места на её актуальной
> копии, проверенный backup, rollback plan и согласованное maintenance
> window. Online staged migration с chunked backfill, concurrent indexes
> и поэтапным включением ограничений в 060.29.2 не реализуется. Если она
> понадобится, это будет отдельное архитектурное и deployment-решение.
### 7.7. Совместимость writers
Single Trade, атомарная запись Trade с checkpoint, Quote и Candle
используют database default. Duplicate либо provenance update сохраняет
первоначальный `replay_sequence` и не включает эту колонку в `UPDATE`.
Trade batch сначала полностью валидируется. Затем writer назначает по
одному sequence каждому элементу строго в исходном Canonical tuple.
Только связанные пары `(prepared_trade, replay_sequence)` сортируются по
существующему identity lock order и записываются. Поэтому безопасный
порядок захвата row locks не меняет порядок событий внутри исходного
batch, включая `INT32_MAX → INT32_MIN` и `-1 → 0`.
Sequence не является transaction counter: duplicate, conflict или
rollback могут оставить пропуск. Между конкурентными transactions он
показывает порядок выдачи ordinal, но не обещает commit chronology.
### 7.8. PostgreSQL Trade History reader
060.29.2 добавляет отдельный synchronous provider-based adapter
`PostgresTradeHistoryRepository`. Он реализует только
`TradeHistoryReaderProtocol`, не расширяет write facade, не читает
checkpoint и не владеет lifecycle PostgreSQL pool.
Query использует один parameterized `SELECT`, полуоткрытый фильтр
`[start_time, end_time)`, forward keyset predicate
`(executed_at, replay_sequence) > (...)`, тот же ascending `ORDER BY` и
`LIMIT query.limit + 1`. `OFFSET`, `SELECT *`, `FOR UPDATE` и любые
writes запрещены. `next_cursor` указывает на последний возвращённый
record только тогда, когда получена лишняя строка.
Reader явно проецирует все Canonical и provenance columns и строго
проверяет signed Trade ID, Decimal values, aggressor side, UTC times,
Canonical schema version, observation sources, query scope и positive
sequence. Повреждённая строка не пропускается.
Обычная страница является текущим committed view одного statement.
Repository не выполняет `commit`, `rollback` и не меняет isolation
level: transaction принадлежит connection provider. Это позволяет
обычным Historical callers явно владеть границей чтения. 060.29.3
повторно использует тот же строгий row mapper, но строит Replay через
отдельный snapshot query внутри короткой `READ ONLY REPEATABLE READ`
transaction.
Неверный тип query отклоняется до получения connection как
`MarketDataAccessValidationError`. Повреждённая строка или page
invariant дают `MarketDataAccessIntegrityError`; backend/provider error
даёт `MarketDataAccessOperationError` с исходной причиной. Существующие
Access errors не переоборачиваются, а `KeyboardInterrupt` и
`SystemExit` не перехватываются.
### 7.9. PostgreSQL Quote/Candle History readers
060.29.3 добавляет независимые `PostgresQuoteHistoryRepository` и
`PostgresCandleRevisionHistoryRepository`. Они используют тот же
provider-based lifecycle и ту же строгую политику ошибок, что и Trade
History reader, но сохраняют собственные публичные оси истории:
```text
Quote: (received_at, replay_sequence)
Candle revision: (open_time, replay_sequence)
```
Quote query использует полуоткрытый диапазон `received_at` и явно
проецирует Canonical payload, provenance, global sequence и schema
version. Candle query дополнительно требует один case-sensitive
`interval` и использует полуоткрытый диапазон `open_time`. Оба reader
используют forward keyset pagination и `LIMIT query.limit + 1`.
Строгая материализация PostgreSQL-строк вынесена в общий внутренний
модуль. Trade, Quote, Candle History и Replay Plan Builder поэтому
одинаково проверяют Canonical values, UTC, provenance, schema version и
positive global sequence. Извлечение Trade-маппера не меняет публичное
поведение принятого 060.29.2.
`MarketDataHistoricalAccess` является DB-neutral фасадом трёх reader
protocols. Его создание не выполняет I/O; фасад не владеет pool и не
переоборачивает ошибки конкретного reader.
---
## 8. Snapshot и concurrent writes
Historical pages являются текущим committed view. Между страницами
Live/Recovery могут добавить строки, а Retention может удалить данные.
Поэтому обычная пагинация не обещает межстраничный snapshot.
Replay не читает медленно меняющиеся страницы во время playback.
Сначала в одной короткой транзакции:
```text
READ ONLY
REPEATABLE READ
```
материализуется bounded `ReplayPlan`. После этого cursor, transaction и
connection закрываются, а Replay работает только с immutable tuple в
памяти.
Долговременная PostgreSQL transaction на период виртуального времени
запрещена. Превышение `max_records` является явной ошибкой, а не
частичным либо молчаливо обрезанным Replay.
### 8.1. Отдельный PostgreSQL Replay snapshot query
Публичный Candle History query нельзя использовать как источник Replay:
он выбирает свечи по `open_time`, тогда как конкретная ревизия должна
воспроизводиться по `observed_at`.
`PostgresReplayPlanBuilder` выполняет один отдельный parameterized
`UNION ALL` query только для запрошенных типов:
```text
Trade → replay_at = executed_at
Quote → replay_at = received_at
Candle revision → replay_at = observed_at
```
Static SQL branches выбираются только по `ReplayDataType`; venue,
symbols, временной диапазон и intervals всегда передаются параметрами.
Итоговый query сортирует все типы по
`(replay_at, replay_sequence)` и запрашивает `max_records + 1` строк.
Лишняя строка вызывает `ReplayPlanLimitExceededError`; частичный plan не
создаётся.
Builder владеет только короткими cursor и transaction внутри одного
вызова `create_plan()`. Первой SQL-командой transaction устанавливается:
```sql
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ, READ ONLY;
```
Pool и connection provider принадлежат приложению. После materialization
transaction и connection закрыты, а возвращённый `ReplayPlan` не зависит
от PostgreSQL. Повреждённые строки дают
`MarketDataAccessIntegrityError`, backend/provider failures —
`MarketDataAccessOperationError` с исходной причиной, превышение лимита
остаётся отдельной Replay-ошибкой. `BaseException` не перехватывается.
---
## 9. Replay contracts
### 9.1. ReplayEvent и ReplayPlan
`ReplayEvent` содержит:
- `venue`;
- UTC `replay_at`;
- immutable `replay_sequence`;
- точный Canonical payload;
- `is_final` только для Candle revision.
`ReplayPlan` хранит исходный immutable request и полностью
материализованный tuple событий. Пустой plan является допустимым no-op.
Непустой plan строго возрастает по `(replay_at, replay_sequence)` и не
содержит повторяющихся global sequence.
### 9.2. Clock
Контракты разделяются:
- `MarketDataClockProtocol` — только чтение текущего времени;
- `ReplayClockProtocol` — управляемое `advance_to()`.
Одна Replay Session владеет одними Virtual Clock. Стартовое время равно
`request.start_time`, включая пустой plan. Время не может уменьшаться.
060.29.4 добавляет concrete `DeterministicReplayClock` с единственным
изменяемым состоянием — текущим UTC-временем. Clock не читает системное
время, не выполняет sleep, I/O и не создаёт задач.
Начальное значение и каждый `advance_to()` принимают timezone-aware
`datetime`, совместимый с уже принятыми Replay time contracts, и
канонизируют его в обычный UTC `datetime`. Naive datetime запрещён.
Переход вперёд изменяет `now`. Переход на тот же абсолютный момент
разрешён как идемпотентный no-op: несколько событий могут иметь один
`replay_at` и различаться только `replay_sequence`. Только переход назад
вызывает `ReplayClockError`; после любой ошибки состояние не меняется.
Clock не знает о `ReplayPlan`, sequence и верхней границе request. У него
нет `reset()`, `advance_by()`, `start()` и `stop()`. Один экземпляр
принадлежит одной Session и не разделяется между Session или OS threads.
Внутренние lock и owner registry не добавляются; управление
последовательно и синхронно выполняет будущая Replay Session.
### 9.3. Consumer и Session
Одна Session имеет одного async consumer. Доставка выполняется строго
последовательно:
```text
clock.advance_to(event.replay_at)
await consumer.consume(event)
```
Fan-out, fire-and-forget и error isolation в Build не добавляются.
060.29.5 добавляет concrete `ReplaySession`. Отдельный класс
`ReplayEngine` не создаётся: engine является последовательным циклом
внутри `ReplaySession.run()`.
Конструктор получает уже материализованный точный `ReplayPlan`, один
`ReplayClockProtocol` и один `ReplayConsumerProtocol`. Clock передаётся
Session в исключительное владение и до запуска обязан находиться в
каноническом UTC-времени `plan.request.time_range.start_time`.
Несовпадение считается ошибкой dependency и даёт
`MarketDataReplayValidationError`; Session не выполняет скрытый reset.
Plan сохраняется по identity и может использоваться для создания других
независимых Session. Public `clock` предоставляет consumer-facing
границу `MarketDataClockProtocol`, а lifecycle consumer остаётся у
caller/composition.
Session является one-shot и имеет состояния:
```text
CREATED → RUNNING → COMPLETED
↘ FAILED
↘ CANCELLED
```
Повторный, конкурентный или рекурсивный `run()` запрещён. Session
переводится в `RUNNING` до первого `await`, поэтому внутри одного event
loop второй caller немедленно получает `ReplaySessionStateError` без
lock и без влияния на первый запуск. Для повторения создаются новые
Session и Clock.
Empty plan штатно проходит `CREATED → RUNNING → COMPLETED`, не вызывает
Clock или consumer и оставляет время в `request.start_time`. `run()` не
возвращает Result DTO или progress: успешный результат равен `None`.
### 9.4. Consumer Factory и Composition
060.29.6 добавляет только две новые публичные границы:
```text
ReplayConsumerFactoryProtocol
ReplaySessionFactory.prepare_session(request)
```
`ReplayConsumerFactoryProtocol` синхронно создаёт отдельный consumer для
одной Session и получает:
- точный immutable `ReplayPlan`;
- Clock этой же Session через read-only `MarketDataClockProtocol`.
Factory обязана возвращать новый stateful consumer при каждом вызове,
не выполнять I/O, не создавать task и не запускать lifecycle consumer.
Default/no-op consumer не предоставляется: конкретный consumer выбирает
вызывающая подсистема с реальным use case.
`ReplaySessionFactory` получает только `ReplayPlanBuilderProtocol` и
`ReplayConsumerFactoryProtocol`. Конструктор сохраняет зависимости и не
выполняет I/O. Явный `prepare_session()` выполняет строго одну
последовательность:
```text
exact ReplayPlanRequest validation
plan_builder.create_plan(request)
exact ReplayPlan + plan.request identity validation
fresh DeterministicReplayClock(request.start_time)
consumer_factory.create_consumer(plan, тот же Clock)
fresh ReplaySession(plan, тот же Clock, consumer)
```
Метод `prepare_session()` намеренно является синхронным и потенциально
блокирующим: concrete PostgreSQL builder выполняет чтение snapshot в
потоке вызывающего кода. Composition не скрывает SQL через
`asyncio.to_thread()`, потому что cancellation asyncio task не остановит
уже выполняющийся SQL в worker thread. Выбор отдельного worker остаётся
у внешнего caller.
Возвращается обычная `ReplaySession` в состоянии `CREATED`.
Composition не вводит Result DTO, отдельный `ReplayEngine`, service,
registry, cache, lock или single-flight. Caller отдельно и явно
выполняет `await session.run()` и владеет этой coroutine/task.
Каждый вызов `prepare_session()` создаёт новый Clock, consumer и
Session. Один immutable plan может быть повторно возвращён injected
builder, но изменяемые части графа между Session не разделяются. Один и
тот же concrete Clock по identity передаётся consumer factory через
read-only Protocol и Session через управляющий Protocol.
Composition не сериализует параллельные вызовы. Thread-safety общего
injected builder и consumer factory является их внешним контрактом;
два уже созданных графа не разделяют изменяемые Session/Clock/consumer.
Ошибки builder и consumer factory распространяются без обёртки, retry и
частичного результата. Неверный тип request, неверный exact plan или
подмена `plan.request` дают `MarketDataReplayValidationError` до создания
следующей зависимости. Class objects и async implementations вместо
синхронных dependency instances отклоняются при создании composition.
060.29.6 не меняет Bootstrap, settings, SQL, PostgreSQL pool, Runtime,
Telegram или startup приложения. Подготовка выполняется только после
явного вызова `prepare_session()`, а playback — только после явного
вызова `run()`.
---
## 10. Lifecycle, cancellation и ошибки
1. Создание contracts, access facade и `ReplaySessionFactory` не
выполняет I/O; явный `prepare_session()` является blocking-границей.
2. Historical readers используют существующий managed pool, но не
открывают и не закрывают его.
3. ReplayPlan полностью отделяется от PostgreSQL до playback.
4. Caller владеет coroutine task `ReplaySession.run()`.
5. Session не создаёт скрытую root/background task.
6. Consumer error распространяется вызывающему коду и останавливает
доставку следующих событий.
7. `CancelledError` не проглатывается.
8. Clock error является terminal для Session.
9. Частично доставленный consumer prefix нельзя транзакционно отменить;
повтор выполняется новой Session по явному решению caller.
10. Clock переводится до consumer; после consumer failure или
cancellation остаётся во времени начатого события.
11. Исходные error и cancellation распространяются без обёртки; Session
только фиксирует `FAILED` или `CANCELLED`.
12. Session не создаёт task, lock, connection, retry или cleanup и не
управляет lifecycle consumer.
---
## 11. Scope 060.29.0060.29.1
### 060.29.0
- этот architecture document;
- read/write boundary;
- event-time и ordering policy;
- global replay sequence policy;
- snapshot, resource и failure policy;
- разбиение Build и acceptance criteria.
### 060.29.1
- immutable Historical records;
- time range, queries, typed cursors и pages;
- runtime-checkable Historical reader protocols;
- отдельная error hierarchy;
- Replay request, event, materialized plan и session state;
- clock, consumer, plan builder и session Protocol-контракты;
- DB-neutral unit-тесты.
060.29.0060.29.1 не выполняют SQL, network I/O, filesystem I/O и не
создают asyncio tasks.
---
## 12. Следующие подэтапы
### 060.29.2
- migration 9 с shared global `replay_sequence` без registry table;
- блокирующий атомарный backfill существующих строк;
- immutable sequence и partitioned keyset indexes;
- совместимость single/batch writers без изменения checkpoint semantics;
- отдельный PostgreSQL Trade Historical reader;
- unit- и opt-in PostgreSQL verification.
Детальный утверждённый контракт закреплён в разделах 7.57.8.
### 060.29.3
- Quote History по `(received_at, replay_sequence)`;
- Candle History по `(open_time, replay_sequence)` и одному interval;
- общий DB-neutral Historical Access facade;
- единые строгие PostgreSQL row mappers;
- отдельный snapshot query с Candle replay axis `observed_at`;
- короткая `READ ONLY REPEATABLE READ` transaction;
- bounded immutable ReplayPlan и fail-fast `max_records + 1`.
### 060.29.4
- concrete `DeterministicReplayClock`;
- UTC normalization без чтения wall clock;
- разрешённый равный переход и запрет движения назад;
- single-owner policy без lock, reset и lifecycle;
- public export и DB-neutral unit-тесты.
### 060.29.5
- concrete `ReplaySession` без отдельного `ReplayEngine`;
- injected Clock/consumer и fail-fast проверка начального времени;
- one-shot lifecycle и последовательный playback loop;
- error/cancellation propagation без retry и rollback;
- DB-neutral async unit-тесты и public export.
### 060.29.6
- `ReplayConsumerFactoryProtocol` для обязательного concrete consumer;
- единый `ReplaySessionFactory.prepare_session()`;
- явная blocking composition
`request → plan → fresh Clock → fresh consumer → fresh Session`;
- один Clock по identity для Session и consumer через разные
Protocol-границы;
- отсутствие default consumer, скрытого `to_thread`, cache и
автоматического startup;
- DB-neutral unit-тесты composition, identity, ошибок и конкурентности.
### 060.29.7
- verification-only integration без предварительного production diff;
- существующий opt-in harness только для локальной `dzentra_test_*` БД;
- локальный путь
`Loopback Trade Runtime → Storage → Historical Access → Replay`;
- mixed-type PostgreSQL snapshot и caller-owned playback;
- реальная граница `READ ONLY REPEATABLE READ` при concurrent commit;
- возврат PostgreSQL resources до первого consumer call;
- limit, integrity, provider/backend и consumer failure paths;
- независимые графы двух concurrent blocking preparations;
- десятикратный повтор нового PostgreSQL target.
### 060.29.8
- обязательный Pyright gate, compileall и проверка зависимостей;
- расширенный Access/Replay/Storage/migration unit-набор;
- десятикратный PostgreSQL Replay target;
- полный PostgreSQL Storage и общий integration-наборы;
- отдельная opt-out проверка без подключения к базе;
- fixed stress и полная offline-регрессия;
- итоговый read-only review архитектуры, SQL, ordering, lifecycle,
обработки ошибок, освобождения ресурсов и документации.
Подэтап остаётся verification-only. Dzengi live и acquisition soak не
являются обязательными: Build не добавляет live endpoint wiring или
долгоживущие Replay-задачи, а детерминированный путь
`Loopback Runtime → Storage → Historical Access → Replay` проверен на
локальной PostgreSQL.
---
## 13. Test strategy
### Обязательная статическая типизация
- Pyright `1.1.411` запускается в режиме `standard`, совпадающем с
используемым Pylance;
- каноническая команда — `scripts/check_python_types.sh`;
- допустимый результат — только `0 errors, 0 warnings`;
- проверяются Market Data, Storage, Bootstrap, Runtime Events и все их
unit/integration/stress/live/support-тесты;
- gate входит в обычную offline pytest-регрессию через отдельный
static-тест и поэтому не может быть пропущен при приёмке Build;
- `type: ignore`, отключение диагностик или ослабление режима не
используются для исправления новых ошибок;
- область проверки в следующих Build может только расширяться.
### Contracts
- frozen/slots;
- строгие типы, включая запрет `bool` вместо `int`;
- UTC normalization и запрет naive datetime;
- полуоткрытые временные границы;
- canonical symbol, venue и case-sensitive Candle interval;
- provenance и Canonical schema version;
- cursor scope/version;
- typed pages и строгий order key;
- runtime-checkable Protocols.
### Ordering
- одинаковое event time и разные sequence;
- `INT32_MAX → INT32_MIN`;
- `-1 → 0`;
- provenance update не меняет sequence;
- размер страницы не меняет итоговый порядок.
### Migration и writers
- детерминированный mixed-type backfill независимо от physical order;
- shared sequence во всех parent/default/existing/future partitions;
- direct update sequence запрещён trigger;
- duplicate и provenance update сохраняют первоначальный ordinal;
- batch input order сохраняется независимо от identity lock order;
- concurrent migration callers, writer wait и partition-manager race;
- partial failure откатывает migration 9, clean retry проходит;
- gaps после duplicate и transaction rollback считаются корректными.
### PostgreSQL Trade History
- start inclusive и end exclusive;
- empty, exact-limit и extra-row pages;
- смена page limit не создаёт skips или duplicates;
- equal timestamp и signed rollover упорядочиваются по sequence;
- default и monthly partitions дают один стабильный результат;
- повреждённая строка и неизвестная schema version не пропускаются;
- DB failure сохраняет cause и не оставляет connection/cursor;
- чтение не меняет Trade, provenance, checkpoint или sequence.
### PostgreSQL Quote/Candle History и Replay snapshot
- Quote start inclusive и end exclusive по `received_at`;
- Candle History start inclusive и end exclusive по `open_time`;
- case-sensitive Candle interval и устойчивые keyset pages;
- строгая проверка Quote prices и Candle OHLCV/revision metadata;
- Candle с `open_time` вне Replay range и `observed_at` внутри включается;
- Candle с `open_time` внутри и `observed_at` вне исключается;
- static branches включают только запрошенные data types;
- один global order для одинакового времени разных типов;
- `max_records + 1` даёт ошибку без частичного plan;
- transaction mode, cleanup, backend cause и `BaseException` проверены.
### Replay lifecycle
- Protocol, slots, initial `CREATED` и read-only properties;
- fail-fast dependency validation и точное начальное время Clock;
- empty plan без вызовов Clock/consumer;
- точный порядок tuple, Clock-before-consumer и event identity;
- одинаковое `replay_at` с разными global sequence;
- последовательная backpressure без параллельной доставки;
- consumer/Clock failure и исходная error identity;
- cancellation без доставки suffix и без проглатывания;
- concurrent/repeated/reentrant `run()` rejection;
- независимые Session и Clock при общем immutable plan;
- отсутствие hidden tasks, retries, cleanup и PostgreSQL connections.
### Deterministic Replay Clock
- начальное UTC-время и normalization разных UTC offsets;
- запрет non-datetime и naive datetime;
- прямой переход с сохранением микросекунд;
- повторный переход на тот же абсолютный момент;
- несколько событий с одинаковым `replay_at`;
- обратный переход через `ReplayClockError` без изменения `now`;
- независимость Clock разных Session;
- read-only `now`, `__slots__` и отсутствие reset/lifecycle/tasks/I/O.
### Consumer и Composition
- runtime-checkable `ReplayConsumerFactoryProtocol` и public exports;
- fail-fast constructor validation, включая class objects и async
implementations;
- exact request до builder и identity request в созданном plan;
- порядок builder → Clock → consumer → Session;
- один Clock по identity для consumer factory и Session;
- новый Clock, consumer и Session при каждом вызове;
- пустой plan без автоматического запуска consumer;
- blocking preparation в caller thread без скрытого `to_thread`;
- ошибки builder/factory по identity, без retry и wrapping;
- отсутствие hidden tasks, lifecycle calls, cache и automatic startup;
- независимые графы для двух concurrent callers.
### PostgreSQL Replay and Failure Verification
- opt-in отключён без точного флага и явного локального DSN;
- destructive reset разрешён только для проверенной `dzentra_test_*` БД;
- local Loopback Trade проходит Runtime, Storage, History и Replay;
- Trade, Quote и Candle revisions воспроизводятся в global order;
- empty snapshot остаётся штатным no-op;
- test-only barrier фиксирует snapshot до concurrent commit без `sleep`;
- первая Session не видит post-snapshot commit, следующая видит его;
- pool с `max_size=1` освобождается до playback и может быть переоткрыт;
- limit/integrity/provider/backend errors не создают consumer или Session;
- consumer failure/cancellation происходят после освобождения DB;
- Replay не меняет Market Data или persistent checkpoint;
- два blocking caller реально участвуют и получают отдельные графы;
- threads, tasks, cursors, transactions и connections закрываются;
- новый target проходит однократно и десять раз с
`ResourceWarning` как error.
---
## 14. Вне scope
- raw exchange documents и точный transport arrival log;
- загрузка рыночной истории до первого запуска persistent storage;
- автоматический historical backfill;
- пользовательские ордера, fills, позиции и private account operations;
- аналитические вычисления и Feature Engineering;
- стратегии, Risk, Portfolio и Order Management;
- полноценный Backtesting;
- wall-clock pacing, speed, pause, seek и resume;
- автоматический Retention Scheduler;
- persistent Quote/Candle production consumers;
- distributed lease, leader election и HA failover;
- Telegram/HTTP public API;
- автоматический запуск Replay вместе с приложением;
- online staged migration global sequence без maintenance window.
---
## 15. Граница доступного периода
После 060.29 база предоставляет запросы и Replay только для данных,
которые действительно находятся в PostgreSQL.
Начальная граница production Trade history:
```text
первый успешный запуск
MARKET_DATA_STORAGE_ENABLED=true
и включённого Production Trade Stream
```
060.28 заполняет bounded downtime gap после уже известного persistent
checkpoint, если нужные Trades ещё доступны в REST API Dzengi. Он не
загружает всю историю до первого checkpoint.
Конечная граница Trade history постоянно продвигается работающим
Runtime. Retention по умолчанию выключен, поэтому программного срока
удаления истории нет. Фактический период всё равно ограничен:
- временем первого production-запуска storage;
- периодами, когда Runtime или база были недоступны;
- глубиной истории, доступной Recovery API;
- ручной очисткой, будущей Retention policy и объёмом диска.
Для Quotes и Candle revisions production consumers пока не подключены.
Historical Access возвращает только строки, фактически записанные через
их repositories; полнота production Quote/Candle history не заявляется.
Каждая Historical page отражает committed-состояние на время
собственного запроса. Последовательная pagination даёт доступ ко всем
фактически сохранённым строкам диапазона только при неизменном dataset
между страницами. Live/Recovery-запись и Retention могут изменить этот
набор; единый межстраничный snapshot и полная биржевая история без
отдельного completeness metadata не гарантируются. Initial backfill
потребует нового согласованного Build.
---
## 16. Acceptance criteria
060.29.0060.29.1 могут быть приняты, если:
1. Read contracts не меняют write-only Storage API.
2. Нет SQL, `psycopg`, repository, Runtime или Bootstrap implementation.
3. Все public value objects immutable и slotted.
4. Query windows aware, UTC и half-open.
5. Cursor строго связан с query scope.
6. `trade_id` не входит в Historical order key.
7. Records и Replay events сохраняют identity Canonical payload.
8. ReplayPlan строго упорядочен и bounded.
9. Clock/Consumer/Session представлены только Protocol-контрактами.
10. Комментарии и docstrings новых файлов написаны по-русски.
11. Целевые unit-тесты проходят.
12. Отдельный read-only review не выявляет findings.
060.29.2 может быть принят, если:
1. Migration 9 атомарно и детерминированно заполняет global sequence.
2. Shared sequence является единственным allocator; registry table нет.
3. Все partitions получают positive `NOT NULL` sequence и keyset indexes.
4. Provenance update и duplicate не меняют уже записанный ordinal.
5. Trade batch сохраняет input order до identity lock sorting.
6. Migration concurrency, blocking и rollback проверены на PostgreSQL.
7. Trade History использует half-open range и forward keyset pagination.
8. Reader строго проверяет строки и сохраняет error cause.
9. Reader не владеет pool lifecycle и не меняет Runtime/Bootstrap.
10. Unit, PostgreSQL target и полная регрессия проходят.
11. Отдельный acceptance read-only review не выявляет findings.
060.29.3 может быть принят, если:
1. Quote History использует `received_at` и forward keyset pagination.
2. Candle History использует `open_time` и case-sensitive interval.
3. Все три reader используют единые строгие PostgreSQL row mappers.
4. Historical Access facade не выполняет I/O и не владеет pool.
5. Replay Builder использует отдельный snapshot query, а не Candle
History pagination.
6. Candle Replay фильтруется по `observed_at`, а не по `open_time`.
7. Snapshot materializes в одной короткой `READ ONLY REPEATABLE READ`
transaction и закрывает все ресурсы до возврата plan.
8. SQL branches статичны, а пользовательские значения параметризованы.
9. Итоговый порядок строго возрастает по
`(replay_at, replay_sequence)` для всех типов.
10. `max_records + 1` даёт `ReplayPlanLimitExceededError` без частичного
результата.
11. Unit, PostgreSQL target и полная регрессия проходят.
12. Отдельный acceptance read-only review не выявляет findings.
060.29.4 может быть принят, если:
1. Clock начинается с переданного aware времени и хранит его в UTC.
2. Clock не читает wall clock и не выполняет I/O или sleep.
3. `advance_to()` разрешает прямой и равный переход.
4. Обратный переход даёт `ReplayClockError` и не меняет `now`.
5. Ошибка типа или naive datetime не меняет состояние.
6. Один Clock не содержит shared Session state или внутренних lock.
7. Нет reset, relative advance, lifecycle и скрытых asyncio tasks.
8. Clock соответствует `MarketDataClockProtocol` и
`ReplayClockProtocol`.
9. В подэтапе нет Replay Session, consumer delivery или composition.
10. Unit и полная регрессия проходят.
11. Отдельный acceptance read-only review не выявляет findings.
060.29.5 может быть принят, если:
1. Concrete `ReplaySession` соответствует `ReplaySessionProtocol` и не
вводит отдельный `ReplayEngine`.
2. Exact `ReplayPlan`, `ReplayClockProtocol` и `ReplayConsumerProtocol`
проверяются до первого запуска.
3. Clock начинается точно в каноническом UTC
`plan.request.time_range.start_time`; reset не выполняется.
4. `run()` переводит `CREATED → RUNNING` до первого `await` и допускает
только один запуск.
5. Для каждого события Clock продвигается до одного последовательного
`await consumer.consume(event)`.
6. Empty plan завершается без вызовов dependency и не меняет Clock.
7. Ошибка переводит Session в `FAILED`, cancellation — в `CANCELLED`, а
исходный объект исключения распространяется без обёртки.
8. Повторный, конкурентный и рекурсивный запуск дают
`ReplaySessionStateError`, не повреждая активный или terminal state.
9. Session не создаёт tasks, locks, I/O, retries, rollback или cleanup.
10. В подэтапе нет Plan Builder composition, выбора consumer, Bootstrap
или автоматического startup.
11. Unit и полная регрессия проходят.
12. Отдельный acceptance read-only review не выявляет findings.
060.29.6 может быть принят, если:
1. `ReplayConsumerFactoryProtocol` является единственной новой границей
выбора concrete consumer.
2. `ReplaySessionFactory` проверяет синхронные dependency instances и не
выполняет I/O или создание объектов графа в конструкторе.
3. `prepare_session()` является синхронной blocking-границей и не
использует скрытый `to_thread` или background task.
4. Builder получает точный исходный request ровно один раз, а результат
обязан быть точным `ReplayPlan` с тем же request по identity.
5. Порядок сборки равен plan → Clock → consumer → Session.
6. Consumer factory и Session получают один Clock по identity через
разные Protocol-границы.
7. Каждый вызов создаёт новые Clock, consumer и Session без cache,
registry, lock и single-flight.
8. Подготовленная Session остаётся в `CREATED`; consumer, lifecycle и
playback не запускаются автоматически.
9. Default/no-op consumer, отдельный ReplayEngine, Result DTO, Bootstrap
и settings не добавлены.
10. Ошибки dependencies и cancellation распространяются без обёртки,
retry и частично возвращённой Session.
11. Unit и полная offline-регрессия проходят.
12. Отдельный acceptance read-only review не выявляет findings.
060.29.7 может быть принят, если:
1. Подэтап остаётся verification-only, если тесты не выявили реальный
production defect.
2. Используется существующий opt-in PostgreSQL harness с проверкой
локального endpoint и имени `dzentra_test_*`.
3. Local Trade проходит Production Runtime, PostgreSQL Storage,
Historical Access, ReplayPlan, Composition и Session.
4. Mixed Trade/Quote/Candle plan воспроизводится в точном global order,
а consumer наблюдает уже продвинутое время Clock.
5. Реальный `REPEATABLE READ` snapshot не видит commit после
зафиксированной точки, а следующая Session видит его полностью.
6. PostgreSQL cursor, transaction и connection освобождены до первого
consumer call; подготовленная Session работает после закрытия pool.
7. Limit, integrity, provider/backend errors не создают consumer или
частичную Session, а pool после контролируемой ошибки остаётся либо
снова становится пригодным к работе.
8. Consumer failure и cancellation сохраняют принятый lifecycle Session
без удержания PostgreSQL resources и изменения durable данных.
9. Два concurrent caller действительно выполняют real preparation и
получают разные Plan, Clock, consumer и Session.
10. Нет незавершённых PostgreSQL resources, threads или asyncio tasks.
11. Обязательный Pyright gate проходит с нулём errors и warnings.
12. Новый target проходит один раз и десять раз, полный PostgreSQL
Storage integration, Replay unit и offline regression проходят.
13. Отдельный acceptance read-only review не выявляет findings.
060.29.8 может быть принят, если:
1. Pyright завершается с `0 errors, 0 warnings`, static pytest gate,
compileall и `pip check` проходят.
2. Расширенный Access/Replay/Storage/migration unit-набор проходит.
3. Двенадцать PostgreSQL Replay-сценариев стабильно проходят десять
последовательных запусков с `ResourceWarning` как ошибкой.
4. Полный PostgreSQL Storage и общий integration-наборы проходят с
`ResourceWarning` как ошибкой.
5. Без opt-in flag и DSN весь PostgreSQL Storage-набор пропускается и не
устанавливает соединение с базой.
6. Fixed stress и полная offline-регрессия проходят.
7. Нет необъяснённых skips, незавершённых tasks, threads, cursors,
transactions или PostgreSQL connections.
8. Replay не изменяет Canonical Market Data, operational checkpoint или
production lifecycle.
9. Итоговые read-only review архитектуры, SQL, тестов и документации не
выявляют findings P0P3.
10. Подэтап не меняет production-код, если матрица не воспроизвела
реальный дефект.
---
## 17. ADR
### ADR-060.29-001 — Read API не расширяет write-only Storage facade
Historical Access получает отдельные DB-neutral contracts и adapters.
### ADR-060.29-002 — Replay использует event-time chronology
Порядок исходных transport packets не заявляется без raw arrival log.
### ADR-060.29-003 — Global immutable replay sequence
Один PostgreSQL sequence без registry table применяется ко всем
Canonical Market Data и не изменяется при provenance update.
### ADR-060.29-004 — Replay materializes bounded snapshot
PostgreSQL transaction завершается до начала виртуального playback.
### ADR-060.29-005 — Canonical payload не заменяется Replay DTO
Replay envelope сохраняет точный `Trade`, `Quote` или `Candle`.
### ADR-060.29-006 — Session является one-shot и caller-owned
Session не создаёт скрытую задачу и не скрывает consumer/cancellation
errors.
### ADR-060.29-007 — Migration 9 использует maintenance window
060.29.2 выбирает атомарную блокирующую migration. Online staged rollout
для большой production database требует отдельного решения.
### ADR-060.29-008 — Candle History и Replay используют разные оси
Публичная Candle History выбирает ревизии по `open_time`. Replay Plan
Builder использует отдельный snapshot query и выбирает те же ревизии по
`observed_at`, то есть в момент, когда они фактически стали известны.
### ADR-060.29-009 — Replay engine находится внутри one-shot Session
060.29.5 не вводит отдельный `ReplayEngine`. Последовательный playback
выполняет `ReplaySession.run()`, а создание plan, fresh Clock и consumer
остаётся явной задачей Composition 060.29.6.
### ADR-060.29-010 — Composition разделяет preparation и playback
060.29.6 использует один синхронный blocking
`ReplaySessionFactory.prepare_session()` без скрытого worker и
автоматического startup. Каждый вызов создаёт новый Clock, обязательный
consumer и `ReplaySession`; caller отдельно владеет `session.run()`.
### ADR-060.29-011 — PostgreSQL acceptance остаётся verification-only
060.29.7 не добавляет production consumer или новые runtime-границы.
Он проверяет уже принятые Storage, Historical, snapshot, Composition и
Session на локальной PostgreSQL. Production-код может измениться только
при воспроизводимом нарушении принятого контракта и только вместе с
минимальным regression-тестом; test hooks в production запрещены.
---
## 18. Test evidence
Итоговая приёмочная проверка 060.29.0060.29.1:
```text
Historical Access + Replay unit target: 111 passed
Full offline regression: 2416 passed, 69 deselected
Historical Access exact-type re-review: clean
Replay exact-type re-review: clean
Combined acceptance read-only review: clean
```
Подэтапы 060.29.0060.29.1 приняты. На этом этапе Build 060.29 оставался
`In Progress`.
Итоговая приёмочная проверка 060.29.2:
```text
Target unit: 227 passed
Full Market Data Storage unit: 290 passed
New 060.29.2 PostgreSQL integration: 12 passed
Full PostgreSQL Storage integration: 63 passed
Full integration with PostgreSQL: 76 passed
Full offline regression: 2461 passed, 81 deselected
Compileall через временный pycache: clean
Migration acceptance read-only review: clean
Reader/writer acceptance read-only review: clean
Повторный целевой набор review: 90 passed
```
Подэтапы 060.29.2060.29.3 приняты. На этом этапе Build 060.29 оставался
`In Progress`.
Итоговая приёмочная проверка 060.29.3:
```text
Historical Access + Replay unit target: 270 passed
Expanded Access/Replay/Storage unit: 446 passed
New 060.29.3 PostgreSQL integration: 2 passed
Full PostgreSQL Storage integration: 65 passed
Full integration with PostgreSQL: 78 passed
Full offline regression: 2581 passed, 83 deselected
Compileall через временный pycache: clean
Повторный целевой unit-набор: 270 passed
Formal acceptance read-only review: clean
```
Подэтапы 060.29.3060.29.4 приняты после отдельных формальных
приёмочных read-only review.
Проверка реализации Clock 060.29.4:
```text
Deterministic Replay Clock unit target: 22 passed
Full Replay unit target: 100 passed
Historical Access + Replay unit target: 292 passed
Full offline regression: 2603 passed, 83 deselected
Compileall через временный pycache: clean
git diff --check: clean
Короткий post-fix read-only review: clean
Formal acceptance read-only review: clean
```
Контракт и реализация 060.29.5 приняты после исправления fail-fast
проверки class objects и чистого повторного read-only review.
Проверка реализации Replay Session 060.29.5:
```text
Replay Session unit target: 34 passed
Full Replay unit target: 134 passed
Historical Access + Replay unit target: 326 passed
Full offline regression: 2637 passed, 83 deselected
Compileall через временный pycache: clean
git diff --check: clean
Короткий implementation read-only review: clean
Post-fix acceptance read-only review: clean
```
Проверка реализации Consumer and Composition Integration 060.29.6:
```text
Replay Session Factory unit target: 43 passed
Full Replay unit target: 177 passed
Historical Access + Replay unit target: 369 passed
Full offline regression: 2680 passed, 83 deselected
Compileall через временный pycache: clean
Tracked и untracked whitespace checks: clean
Implementation read-only review: clean
Formal acceptance read-only review: clean
```
Подэтап 060.29.6 принят. На этом этапе Build 060.29 оставался
`In Progress`.
Проверка реализации PostgreSQL Replay and Failure Verification 060.29.7:
```text
Новый PostgreSQL Replay target: 12 passed
Десятикратный прогон target: 10 × 12 passed
Full PostgreSQL Storage integration: 77 passed
Full integration with PostgreSQL: 90 passed
Historical Access + Replay unit target: 369 passed
PostgreSQL opt-out: 77 skipped
Pyright mandatory gate: 0 errors, 0 warnings
Pyright pytest gate: 1 passed
Full offline regression: 2681 passed, 95 deselected
Compileall через временный pycache: clean
Два повторных read-only review: clean
Production-код в 060.29.7: только static narrowing fix;
контракт и поведение неизменны
```
Подэтап 060.29.7 принят после чистого формального приёмочного read-only
review. На этом этапе Build 060.29 оставался `In Progress` до выполнения
060.29.8.
Финальная приёмочная проверка 060.29.8:
```text
Pyright mandatory gate: 0 errors, 0 warnings
Pyright pytest gate: 1 passed
Expanded Access/Replay/Storage unit: 545 passed
PostgreSQL Replay target repeated: 10 × 12 passed
Full PostgreSQL Storage integration: 77 passed
Full integration with PostgreSQL: 90 passed
PostgreSQL opt-out isolation: 77 skipped
Fixed stress target: 3 passed, 1 deselected
Full offline regression: 2681 passed, 95 deselected
pip check: clean
Compileall через временный pycache: clean
Tracked/untracked whitespace checks: clean
Три независимых read-only review: clean
Production-код в 060.29.8: без изменений
```
В integration и stress наборах `ResourceWarning` считался ошибкой.
Подэтап 060.29.8 и весь Build 060.29 приняты.
---
## 19. Следующий Build
Build 060.29 завершён и принят. Следующий этап —
[Build 060.30](build_060_30_architecture.md), итоговый аудит и
документальное закрытие ветки Trades Feed.