build 043: finalize Quotes Feed architecture verification
This commit is contained in:
853
docs/migrations/build_040.md
Normal file
853
docs/migrations/build_040.md
Normal file
@@ -0,0 +1,853 @@
|
||||
# Build 040 — Восстановление принудительного REST refresh котировки на канонической модели Quote
|
||||
|
||||
**Статус:** Завершён
|
||||
**Проект:** Dzentra
|
||||
**Подсистема:** Market Data Acquisition / Exchange Integration / Market Data Runtime
|
||||
**Тип изменения:** Восстановление runtime-контракта после миграции legacy price/snapshot API
|
||||
**Язык документа:** Русский
|
||||
|
||||
---
|
||||
|
||||
## 1. Цель Build 040
|
||||
|
||||
Цель Build 040 — восстановить принудительный REST refresh текущей рыночной котировки для `MarketDataRunner` после завершения Build 039 и удаления legacy price/snapshot API.
|
||||
|
||||
В Build 039 были удалены legacy-сущности и методы:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
MarketPriceSnapshot
|
||||
get_price()
|
||||
get_market_snapshot()
|
||||
get_fresh_market_snapshot()
|
||||
refresh_price_cache()
|
||||
refresh_market_snapshot_cache()
|
||||
_get_real_price()
|
||||
```
|
||||
|
||||
После завершения миграции был обнаружен один оставшийся активный consumer удалённого метода:
|
||||
|
||||
```text
|
||||
src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
который продолжал вызывать:
|
||||
|
||||
```python
|
||||
ExchangeService().refresh_market_snapshot_cache
|
||||
```
|
||||
|
||||
Таким образом, целью Build 040 являлось не изменение runtime-архитектуры и не новый рефакторинг, а минимальное восстановление разорванного контракта на основе канонической модели:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.quote.Quote
|
||||
```
|
||||
|
||||
При этом необходимо было сохранить существующую логику работы старого бота без изменений.
|
||||
|
||||
---
|
||||
|
||||
## 2. Исходное состояние
|
||||
|
||||
После Build 039 проект находился в следующем состоянии:
|
||||
|
||||
1. все основные consumers текущей рыночной цены были переведены на `Quote`;
|
||||
|
||||
2. legacy-модели:
|
||||
|
||||
```text
|
||||
TickerPrice
|
||||
MarketPriceSnapshot
|
||||
```
|
||||
|
||||
были удалены;
|
||||
|
||||
3. legacy-методы получения и обновления price/snapshot были удалены;
|
||||
|
||||
4. полный regression suite успешно проходил:
|
||||
|
||||
```text
|
||||
606 passed
|
||||
```
|
||||
|
||||
5. контрольный grep по основным legacy-контрактам был пустым;
|
||||
|
||||
6. при дополнительном анализе runtime-компонентов был обнаружен оставшийся вызов:
|
||||
|
||||
```python
|
||||
ExchangeService().refresh_market_snapshot_cache
|
||||
```
|
||||
|
||||
в:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
Метод уже отсутствовал в `ExchangeService`, поэтому существовал разорванный runtime-контракт, который не проявлялся в предыдущем полном наборе тестов.
|
||||
|
||||
---
|
||||
|
||||
## 3. Архитектурный принцип Build 040
|
||||
|
||||
Build 040 сохраняет утверждённый архитектурный поток:
|
||||
|
||||
```text
|
||||
Exchange / Market Data Sources
|
||||
↓
|
||||
Market Data Acquisition
|
||||
↓
|
||||
Canonical Quote
|
||||
↓
|
||||
Quote Store / Market Price Cache
|
||||
↓
|
||||
ExchangeService
|
||||
↓
|
||||
MarketDataRunner / Trading Consumers
|
||||
```
|
||||
|
||||
Канонической моделью текущей котировки остаётся:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
из:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.quote
|
||||
```
|
||||
|
||||
Build 040 не вводит новую параллельную price/snapshot-модель и не восстанавливает удалённые legacy-контракты.
|
||||
|
||||
Вместо этого добавляется минимальный публичный compatibility-метод:
|
||||
|
||||
```python
|
||||
refresh_quote_cache()
|
||||
```
|
||||
|
||||
который использует существующий канонический acquisition pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 4. Главное требование сохранения поведения
|
||||
|
||||
Основным требованием Build 040 являлось:
|
||||
|
||||
> Не менять логику работы текущих файлов и существующего runtime.
|
||||
|
||||
Поэтому перед реализацией была восстановлена фактическая старая реализация удалённого метода:
|
||||
|
||||
```python
|
||||
refresh_market_snapshot_cache()
|
||||
```
|
||||
|
||||
из истории Git.
|
||||
|
||||
Это позволило определить реальный контракт старого метода без предположений.
|
||||
|
||||
Старая реализация выполняла:
|
||||
|
||||
```text
|
||||
нормализация runtime_key
|
||||
↓
|
||||
принудительное получение свежего REST snapshot
|
||||
↓
|
||||
проверка ценовых полей
|
||||
↓
|
||||
запись результата в MarketPriceCache
|
||||
↓
|
||||
возврат свежего snapshot
|
||||
```
|
||||
|
||||
Критически важно:
|
||||
|
||||
- cache перед REST-запросом не читался;
|
||||
- всегда выполнялось принудительное получение свежих данных;
|
||||
- результат сохранялся в cache соответствующего runtime;
|
||||
- возвращался свежий результат;
|
||||
- ошибки не подавлялись самим refresh-методом;
|
||||
- mock-режим сохранялся.
|
||||
|
||||
---
|
||||
|
||||
## 5. Восстановление старого контракта через Git
|
||||
|
||||
Для определения фактического поведения удалённого метода была использована история Git.
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
git log -S"def refresh_market_snapshot_cache" \
|
||||
--oneline \
|
||||
-- src/integrations/exchange/service.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
a996f2f feat: add market data architecture and complete migration through build 039
|
||||
e97dcd3 07.4.3.16 — Production Execution Pricing Layer
|
||||
```
|
||||
|
||||
Старая реализация была извлечена командой:
|
||||
|
||||
```bash
|
||||
git show a996f2f^:app/src/integrations/exchange/service.py \
|
||||
| grep -n -A45 -B10 "def refresh_market_snapshot_cache"
|
||||
```
|
||||
|
||||
Восстановленный контракт подтвердил, что новый метод должен быть принудительным refresh-механизмом и не должен использовать обычную cache-first логику `get_quote()`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Почему get_quote() не подходит для REST fallback
|
||||
|
||||
Существующий метод:
|
||||
|
||||
```python
|
||||
ExchangeService.get_quote()
|
||||
```
|
||||
|
||||
реализует cache-first поведение:
|
||||
|
||||
```text
|
||||
валидация символа
|
||||
↓
|
||||
чтение MarketPriceCache
|
||||
↓
|
||||
если Quote свежий
|
||||
↓
|
||||
возврат cached Quote
|
||||
↓
|
||||
иначе REST acquisition
|
||||
```
|
||||
|
||||
Такое поведение корректно для обычных consumers, но не подходит для:
|
||||
|
||||
```python
|
||||
MarketDataRunner._rest_fallback_once()
|
||||
```
|
||||
|
||||
После отказа WebSocket runner должен проверить именно доступность REST-механизма.
|
||||
|
||||
Если использовать `get_quote()`, возможна ситуация:
|
||||
|
||||
```text
|
||||
WebSocket отключился
|
||||
↓
|
||||
в cache остаётся свежий WebSocket Quote
|
||||
↓
|
||||
get_quote() возвращает cached Quote
|
||||
↓
|
||||
REST-запрос фактически не выполняется
|
||||
↓
|
||||
runner ошибочно устанавливает REST state = AVAILABLE
|
||||
```
|
||||
|
||||
Поэтому Build 040 вводит отдельный принудительный refresh-контракт.
|
||||
|
||||
---
|
||||
|
||||
## 7. Новый публичный контракт refresh_quote_cache()
|
||||
|
||||
В `ExchangeService` добавлен метод:
|
||||
|
||||
```python
|
||||
def refresh_quote_cache(
|
||||
self,
|
||||
symbol: str | None = None,
|
||||
*,
|
||||
runtime_key: str | None = None,
|
||||
) -> Quote:
|
||||
```
|
||||
|
||||
Его ответственность:
|
||||
|
||||
```text
|
||||
нормализовать runtime_key
|
||||
↓
|
||||
определить symbol
|
||||
↓
|
||||
если exchange disabled
|
||||
получить mock Quote
|
||||
иначе
|
||||
валидировать symbol
|
||||
↓
|
||||
принудительно получить свежий Quote через REST acquisition
|
||||
↓
|
||||
сохранить тот же Quote в MarketPriceCache
|
||||
↓
|
||||
вернуть тот же Quote
|
||||
```
|
||||
|
||||
Метод не читает cache перед получением свежей котировки.
|
||||
|
||||
---
|
||||
|
||||
## 8. Сохранение mock-поведения
|
||||
|
||||
Старый метод:
|
||||
|
||||
```python
|
||||
refresh_market_snapshot_cache()
|
||||
```
|
||||
|
||||
через:
|
||||
|
||||
```python
|
||||
get_fresh_market_snapshot()
|
||||
```
|
||||
|
||||
поддерживал режим:
|
||||
|
||||
```text
|
||||
exchange_enabled = False
|
||||
```
|
||||
|
||||
В этом режиме создавались mock-данные, которые затем сохранялись в cache.
|
||||
|
||||
Новый метод сохраняет эквивалентное поведение через:
|
||||
|
||||
```python
|
||||
mock_quote(symbol_to_use)
|
||||
```
|
||||
|
||||
При выключенной бирже:
|
||||
|
||||
- REST acquisition не выполняется;
|
||||
- validation через exchange reference data не требуется;
|
||||
- создаётся канонический `Quote`;
|
||||
- тот же объект сохраняется в `MarketPriceCache`;
|
||||
- тот же объект возвращается consumer.
|
||||
|
||||
---
|
||||
|
||||
## 9. Принудительное получение свежего Quote
|
||||
|
||||
В real-режиме новый метод использует:
|
||||
|
||||
```python
|
||||
self._get_fresh_quote(
|
||||
validation.normalized_symbol,
|
||||
)
|
||||
```
|
||||
|
||||
Этот метод вызывает существующий канонический pipeline:
|
||||
|
||||
```text
|
||||
DzengiQuoteDocumentSource
|
||||
↓
|
||||
DzengiQuoteDocumentHandler
|
||||
↓
|
||||
QuotesFeed
|
||||
↓
|
||||
QuoteFeedRegistry
|
||||
↓
|
||||
QuoteAcquisitionService
|
||||
↓
|
||||
Quote
|
||||
```
|
||||
|
||||
Таким образом, Build 040 не добавляет новый способ получения котировок и не дублирует Market Data Acquisition.
|
||||
|
||||
---
|
||||
|
||||
## 10. Почему QuoteAcquisitionService не изменялся
|
||||
|
||||
В ходе анализа было подтверждено, что:
|
||||
|
||||
```python
|
||||
QuoteAcquisitionService
|
||||
```
|
||||
|
||||
намеренно отвечает только за получение канонической модели через зарегистрированный feed.
|
||||
|
||||
Он не должен выполнять:
|
||||
|
||||
- runtime isolation;
|
||||
- cache management;
|
||||
- fallback orchestration;
|
||||
- validation торгового runtime;
|
||||
- выбор `runtime_key`;
|
||||
- compatibility-поведение старого бота.
|
||||
|
||||
Поэтому Build 040 не изменяет:
|
||||
|
||||
```text
|
||||
app/src/market_data/acquisition/service.py
|
||||
app/src/market_data/acquisition/feeds/quotes_feed.py
|
||||
```
|
||||
|
||||
Compatibility и runtime semantics остаются в:
|
||||
|
||||
```text
|
||||
ExchangeService
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Изменение MarketDataRunner
|
||||
|
||||
В:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
был изменён только один runtime-вызов.
|
||||
|
||||
До Build 040:
|
||||
|
||||
```python
|
||||
ExchangeService().refresh_market_snapshot_cache
|
||||
```
|
||||
|
||||
После Build 040:
|
||||
|
||||
```python
|
||||
ExchangeService().refresh_quote_cache
|
||||
```
|
||||
|
||||
При этом не изменялись:
|
||||
|
||||
- `asyncio.to_thread`;
|
||||
- передаваемый symbol;
|
||||
- `runtime_key`;
|
||||
- `last_rest_state`;
|
||||
- `last_rest_error_key`;
|
||||
- WebSocket-first поведение;
|
||||
- cooldown;
|
||||
- retry-логика;
|
||||
- journal events;
|
||||
- event titles;
|
||||
- lifecycle runner;
|
||||
- exception handling.
|
||||
|
||||
---
|
||||
|
||||
## 12. Runtime isolation
|
||||
|
||||
Новый метод сохраняет существующее разделение runtime-контекстов через:
|
||||
|
||||
```python
|
||||
runtime_key
|
||||
```
|
||||
|
||||
Перед использованием ключ нормализуется существующим методом:
|
||||
|
||||
```python
|
||||
self._runtime_key(runtime_key)
|
||||
```
|
||||
|
||||
Котировка сохраняется через:
|
||||
|
||||
```python
|
||||
MarketPriceCache.set_quote(
|
||||
quote,
|
||||
runtime_key=normalized_runtime_key,
|
||||
)
|
||||
```
|
||||
|
||||
Это сохраняет независимость runtime-контекстов, включая:
|
||||
|
||||
```text
|
||||
auto
|
||||
debug_auto
|
||||
default
|
||||
```
|
||||
|
||||
и другие существующие значения, если они используются текущим runtime.
|
||||
|
||||
---
|
||||
|
||||
## 13. Сохранение identity канонического Quote
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```python
|
||||
Quote
|
||||
```
|
||||
|
||||
является immutable-моделью:
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True, slots=True)
|
||||
```
|
||||
|
||||
Build 040 не создаёт копию полученного объекта.
|
||||
|
||||
Последовательность:
|
||||
|
||||
```text
|
||||
fresh Quote
|
||||
↓
|
||||
MarketPriceCache.set_quote(quote)
|
||||
↓
|
||||
return quote
|
||||
```
|
||||
|
||||
В cache сохраняется тот же экземпляр, который возвращается из метода.
|
||||
|
||||
Это зафиксировано тестами.
|
||||
|
||||
---
|
||||
|
||||
## 14. Изменённые production-файлы
|
||||
|
||||
В рамках Build 040 изменены только два production-файла:
|
||||
|
||||
```text
|
||||
app/src/integrations/exchange/service.py
|
||||
app/src/integrations/exchange/market_data_runner.py
|
||||
```
|
||||
|
||||
### 14.1. service.py
|
||||
|
||||
Добавлен новый публичный метод:
|
||||
|
||||
```python
|
||||
refresh_quote_cache()
|
||||
```
|
||||
|
||||
Существующие методы не подвергались сопутствующему рефакторингу.
|
||||
|
||||
В частности, не изменялись:
|
||||
|
||||
```text
|
||||
get_quote()
|
||||
get_execution_snapshot()
|
||||
_get_fresh_quote()
|
||||
_load_quote_via_acquisition()
|
||||
```
|
||||
|
||||
### 14.2. market_data_runner.py
|
||||
|
||||
Изменена только ссылка на удалённый legacy refresh-метод.
|
||||
|
||||
Остальная логика runner сохранена.
|
||||
|
||||
---
|
||||
|
||||
## 15. Изменённые тестовые файлы
|
||||
|
||||
В рамках Build 040 изменены:
|
||||
|
||||
```text
|
||||
app/tests/unit/integrations/exchange/test_service_quote.py
|
||||
app/tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Добавлены тесты нового refresh-контракта и REST fallback runner.
|
||||
|
||||
---
|
||||
|
||||
## 16. Тесты refresh_quote_cache()
|
||||
|
||||
Тестами зафиксировано следующее поведение:
|
||||
|
||||
- принудительное получение свежего `Quote`;
|
||||
- отсутствие cache-first поведения;
|
||||
- замена существующей cache-записи новым объектом;
|
||||
- сохранение того же экземпляра `Quote`;
|
||||
- использование правильного `runtime_key`;
|
||||
- нормализация runtime key;
|
||||
- использование default runtime key;
|
||||
- сохранение `ExchangeError` для невалидного символа;
|
||||
- отсутствие REST acquisition при невалидном символе;
|
||||
- сохранение exception semantics acquisition pipeline;
|
||||
- отсутствие обновления cache при ошибке;
|
||||
- сохранение mock-поведения.
|
||||
|
||||
---
|
||||
|
||||
## 17. Тесты REST fallback MarketDataRunner
|
||||
|
||||
Тестами зафиксировано поведение:
|
||||
|
||||
```python
|
||||
MarketDataRunner._rest_fallback_once()
|
||||
```
|
||||
|
||||
При успешном refresh:
|
||||
|
||||
```text
|
||||
refresh_quote_cache() вызывается
|
||||
↓
|
||||
передаётся текущий symbol
|
||||
↓
|
||||
передаётся context.runtime_key
|
||||
↓
|
||||
last_rest_state = AVAILABLE
|
||||
↓
|
||||
last_rest_error_key = None
|
||||
```
|
||||
|
||||
При ошибке сохраняется существующее поведение runner:
|
||||
|
||||
```text
|
||||
last_rest_state = UNAVAILABLE
|
||||
```
|
||||
|
||||
и существующая обработка ошибки не изменяется.
|
||||
|
||||
---
|
||||
|
||||
## 18. Целевая проверка Build 040
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/integrations/exchange/test_service_quote.py \
|
||||
tests/unit/integrations/exchange/test_market_data_runner.py
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
18 passed in 0.11s
|
||||
```
|
||||
|
||||
Все целевые тесты Build 040 успешно пройдены.
|
||||
|
||||
---
|
||||
|
||||
## 19. Связанные регрессионные тесты
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/integrations/exchange/test_service_execution_quote.py \
|
||||
tests/unit/integrations/exchange/test_service_symbol_runtime_status.py \
|
||||
tests/unit/integrations/exchange/test_market_cache.py \
|
||||
tests/unit/integrations/exchange/test_market_stream.py \
|
||||
tests/unit/storage/test_quote_store.py
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
107 passed in 1.81s
|
||||
```
|
||||
|
||||
Связанные компоненты работают без регрессий.
|
||||
|
||||
---
|
||||
|
||||
## 20. Полный regression suite
|
||||
|
||||
После завершения Build 040 выполнен полный набор тестов:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
614 passed in 2.03s
|
||||
```
|
||||
|
||||
Все тесты успешно пройдены.
|
||||
|
||||
По сравнению с Build 039 количество тестов увеличилось:
|
||||
|
||||
```text
|
||||
606 → 614
|
||||
```
|
||||
|
||||
Добавлено:
|
||||
|
||||
```text
|
||||
8 тестов
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 21. Финальный архитектурный grep
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
-E "refresh_market_snapshot_cache|refresh_price_cache|MarketPriceSnapshot|TickerPrice" \
|
||||
src tests
|
||||
```
|
||||
|
||||
Финальный результат:
|
||||
|
||||
```text
|
||||
<пусто>
|
||||
```
|
||||
|
||||
Это подтверждает отсутствие следующих legacy-контрактов:
|
||||
|
||||
```text
|
||||
refresh_market_snapshot_cache
|
||||
refresh_price_cache
|
||||
MarketPriceSnapshot
|
||||
TickerPrice
|
||||
```
|
||||
|
||||
в:
|
||||
|
||||
```text
|
||||
src
|
||||
tests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 22. Что намеренно не изменялось
|
||||
|
||||
В Build 040 намеренно не изменялись:
|
||||
|
||||
```text
|
||||
Market Data Acquisition architecture
|
||||
Quote model
|
||||
QuoteAcquisitionService
|
||||
QuotesFeed
|
||||
Quote Store
|
||||
MarketPriceCache contract
|
||||
get_quote()
|
||||
get_execution_snapshot()
|
||||
WebSocket adapter
|
||||
market_stream.py
|
||||
MarketDataRunner lifecycle
|
||||
MarketDataRunner retry logic
|
||||
runtime state transitions
|
||||
journal events
|
||||
event titles
|
||||
Telegram UI
|
||||
trading strategies
|
||||
execution logic
|
||||
diagnostics logic
|
||||
```
|
||||
|
||||
Также не выполнялись:
|
||||
|
||||
- переносы файлов;
|
||||
- переименования каталогов;
|
||||
- изменение утверждённой структуры проекта;
|
||||
- добавление новых abstraction layers;
|
||||
- добавление override-параметров «на будущее»;
|
||||
- удаление дополнительных legacy-компонентов вне границ Build 040.
|
||||
|
||||
---
|
||||
|
||||
## 23. Критерии завершения Build 040
|
||||
|
||||
Build 040 считается завершённым, поскольку выполнены все критерии:
|
||||
|
||||
- [x] найден оставшийся активный вызов удалённого `refresh_market_snapshot_cache`;
|
||||
- [x] фактический старый контракт восстановлен из Git;
|
||||
- [x] новый контракт реализован на канонической модели `Quote`;
|
||||
- [x] добавлен `refresh_quote_cache()`;
|
||||
- [x] новый метод не использует cache-first поведение;
|
||||
- [x] fresh Quote принудительно получается через существующий acquisition pipeline;
|
||||
- [x] runtime isolation сохранена;
|
||||
- [x] mock-поведение сохранено;
|
||||
- [x] exception semantics сохранена;
|
||||
- [x] тот же экземпляр `Quote` сохраняется и возвращается;
|
||||
- [x] `MarketDataRunner` переключён на новый метод;
|
||||
- [x] WebSocket-first логика не изменена;
|
||||
- [x] lifecycle runner не изменён;
|
||||
- [x] целевые тесты проходят;
|
||||
- [x] связанные регрессионные тесты проходят;
|
||||
- [x] полный regression suite проходит;
|
||||
- [x] финальный legacy grep пуст.
|
||||
|
||||
---
|
||||
|
||||
## 24. Итоговое состояние после Build 040
|
||||
|
||||
После завершения Build 040 runtime-поток получения котировки имеет следующий вид:
|
||||
|
||||
```text
|
||||
WebSocket available
|
||||
↓
|
||||
DzengiWebSocketQuoteAdapter
|
||||
↓
|
||||
Quote
|
||||
↓
|
||||
MarketPriceCache
|
||||
```
|
||||
|
||||
При отказе WebSocket:
|
||||
|
||||
```text
|
||||
WebSocket unavailable
|
||||
↓
|
||||
MarketDataRunner._rest_fallback_once()
|
||||
↓
|
||||
ExchangeService.refresh_quote_cache()
|
||||
↓
|
||||
Quote Acquisition Pipeline
|
||||
↓
|
||||
Fresh REST Quote
|
||||
↓
|
||||
MarketPriceCache
|
||||
↓
|
||||
REST state = AVAILABLE
|
||||
```
|
||||
|
||||
При ошибке REST refresh:
|
||||
|
||||
```text
|
||||
REST acquisition error
|
||||
↓
|
||||
ExchangeError
|
||||
↓
|
||||
MarketDataRunner existing error handling
|
||||
↓
|
||||
REST state = UNAVAILABLE
|
||||
```
|
||||
|
||||
Таким образом, Build 040 восстанавливает разорванный fallback-контракт, не изменяя существующую runtime-логику.
|
||||
|
||||
---
|
||||
|
||||
## 25. Зафиксированный принцип для следующих Builds
|
||||
|
||||
Build 040 подтверждает принцип, зафиксированный после Build 039:
|
||||
|
||||
> При миграции нельзя считать новую функцию эквивалентной старой только потому, что они решают похожую задачу.
|
||||
|
||||
Перед заменой legacy-компонента необходимо восстановить и сравнить фактическое поведение:
|
||||
|
||||
```text
|
||||
порядок операций
|
||||
чтение cache
|
||||
запись cache
|
||||
принудительность refresh
|
||||
тип возвращаемого значения
|
||||
identity результата
|
||||
runtime isolation
|
||||
mock-поведение
|
||||
fallback-поведение
|
||||
exception semantics
|
||||
side effects
|
||||
граничные условия
|
||||
```
|
||||
|
||||
Если старый контракт доступен в истории Git, его необходимо использовать как источник фактического поведения вместо предположений.
|
||||
|
||||
---
|
||||
|
||||
## 26. Статус Build
|
||||
|
||||
```text
|
||||
Build 040: COMPLETED
|
||||
Target tests: 18 passed
|
||||
Related regression tests: 107 passed
|
||||
Full regression suite: 614 passed
|
||||
Legacy grep: empty
|
||||
Canonical quote model: Quote
|
||||
REST fallback contract: refresh_quote_cache()
|
||||
```
|
||||
|
||||
Build 040 полностью завершён.
|
||||
Reference in New Issue
Block a user