Updated status in spec.md, plan.md, tasks.md, research.md, data-model.md, ux_reference.md, quickstart.md. All 72 tasks completed. 34/34 tests passing.
261 lines
31 KiB
Markdown
261 lines
31 KiB
Markdown
# Tasks: Async Backend Refactoring
|
||
|
||
**Input**: Design documents from `specs/032-translate-requests-httpx/`
|
||
**Prerequisites**: plan.md ✅, spec.md ✅, research.md ✅, data-model.md ✅, contracts/modules.md ✅
|
||
|
||
**Tests**: Все C3+ контракты требуют тестов на конкурентное выполнение и rejected-path регрессии. Тесты пишутся первыми (fail before implementation).
|
||
|
||
**Organization**: Группировка по user stories (P1→P4), каждая story независимо реализуема и тестируема.
|
||
|
||
---
|
||
|
||
## Phase 1: Setup (Shared Infrastructure)
|
||
|
||
**Purpose**: Новые зависимости и конфигурация — подготовка среды перед рефакторингом.
|
||
|
||
- [x] T001 Добавить `pytest-httpx` в dev-зависимости в `backend/pyproject.toml` (или `requirements-dev.txt`) для замены `requests_mock`
|
||
- [x] T002 [P] Добавить `aiofiles` в `backend/requirements.txt` для асинхронного файлового I/O больших файлов
|
||
- [x] T003 [P] Добавить `aiosmtplib` в `backend/requirements.txt` для асинхронной SMTP-отправки
|
||
- [x] T004 Добавить поля `connection_pool_size` (default: 20), `connection_pool_timeout` (default: 30), `request_timeout` (default: 30), `llm_timeout` (default: 120) в Pydantic-схему `EnvironmentConfig` в `backend/src/schemas/environment.py`. Decision gate: если `EnvironmentConfig` хранится как JSON/Pydantic blob — `Field(default=...)` достаточен (defaults применяются при parse). Если поля — SQL‑колонки — создать Alembic-миграцию с `server_default`. App‑level конфигурация (`db_executor_workers` и т.д.) выносится отдельно — не в `EnvironmentConfig`.
|
||
|
||
**Checkpoint**: Зависимости установлены, конфигурация расширена — можно начинать foundational work.
|
||
|
||
---
|
||
|
||
## Phase 2: Foundational — AsyncAPIClient (extend) + SupersetClient merge (US1 foundation) ⚠️ BLOCKS ALL STORIES
|
||
|
||
**Purpose**: Расширение существующего `AsyncAPIClient` в `async_network.py` (добавление семафора, connection pool config) и слияние sync/async `SupersetClient` в единый асинхронный клиент. Без этого невозможна миграция плагинов.
|
||
|
||
**⚠️ CRITICAL**: Ни одна user story не может стартовать до завершения этой фазы.
|
||
|
||
**Факт из кодовой базы**: `AsyncAPIClient` [C:3] уже существует в `backend/src/core/utils/async_network.py` как часть `AsyncNetworkModule` [C:5]. Он использует `httpx.AsyncClient` с `SupersetAuthCache`. `SupersetAuthCache` (общий для sync/async клиентов) и `SupersetClientPaginated` уже выделены. Задача — **расширить**, а не создать с нуля.
|
||
|
||
### Tests for Foundational
|
||
|
||
- [x] T005 [P] [US1] Написать контрактный тест для расширенного `AsyncAPIClient.request()` — проверка асинхронного HTTP с семафором, connection pool config — в `backend/tests/core/test_async_client.py` (RATIONALE: httpx.AsyncClient вместо requests.Session; REJECTED: requests_mock — не работает с httpx)
|
||
- [x] T006 [P] [US1] Написать тест семафора: `test_semaphore_limits_concurrent_requests` и `test_semaphore_timeout_returns_503` — в `backend/tests/core/test_async_client.py` (@PRE: семафор создан; @POST: лимит соблюдён, таймаут возвращает ошибку)
|
||
|
||
### Implementation for Foundational
|
||
|
||
- [x] T007 [US1] Расширить существующий `AsyncAPIClient` в `backend/src/core/utils/async_network.py`: добавить `asyncio.Semaphore(connection_pool_size)` с настраиваемым таймаутом, connection pool config (max_connections, keepalive), поддержку `connection_pool_size` и `connection_pool_timeout` из `EnvironmentConfig` (см. T004). (Факт: класс уже существует с auth-cache, httpx.AsyncClient, error translation; RATIONALE: расширение существующего кода вместо создания дубликата; REJECTED: создание нового файла async_client.py — дублирует существующий async_network.py)
|
||
- [x] T008 [US1] Создать объединённый асинхронный `SupersetClient` в `backend/src/core/superset_client/_base.py` — замена sync-кода на расширенный `AsyncAPIClient`, все методы `async def`. Поглотить сигнатуры из `backend/src/core/superset_client/async_superset_client.py`. (RATIONALE: big-bang merge sync+async; REJECTED: dual-stack — отвергнут на clarification)
|
||
- [x] T008a [US1] Реализовать `SupersetClientRegistry` в `backend/src/core/utils/client_registry.py` — синглтон-реестр долгоживущих клиентов по `env_id`. Хранит shared `httpx.AsyncClient` + shared `asyncio.Semaphore` + `asyncio.Lock` для auth refresh. Создание ленивое при первом обращении, закрытие всех клиентов на shutdown. (RATIONALE: глобальный per‑env семафор невозможен без синглтона; REJECTED: per‑request клиенты — теряют connection pooling)
|
||
- [x] T008b [P] [US1] Создать helper `run_blocking(kind, fn, *args, timeout)` в `backend/src/core/utils/executors.py` — обёртка над `loop.run_in_executor(named_executor, fn)`. Именованные bounded‑executor'ы: `db_executor`, `file_executor`, `git_executor` (max_workers из конфига). Все блокирующие операции в production‑путях используют `run_blocking(kind=..., fn=...)` вместо нативного `asyncio.to_thread`. (RATIONALE: `asyncio.to_thread` не принимает параметр executor — нужен `loop.run_in_executor`; REJECTED: asyncio.to_thread — default executor без backpressure)
|
||
|
||
**Checkpoint**: AsyncAPIClient расширен (семафор + pool config), SupersetClient merged. Тесты семафора проходят. Можно мигрировать миксины и плагины.
|
||
|
||
---
|
||
|
||
## Phase 3: User Story 1 — Миграция 13 миксинов SupersetClient и API-ручек (Priority: P1) 🎯
|
||
|
||
**Goal**: Все вызовы Superset API из любых ручек и плагинов становятся неблокирующими. Event loop не блокируется при запросах к Superset.
|
||
|
||
**Independent Test**: Три параллельных вызова `get_dashboards()`, `get_dataset_detail()`, `export_dashboard()` через систему — общее время ≈ max(t1, t2, t3).
|
||
|
||
### Implementation for US1 — Mixins
|
||
|
||
- [x] T011 [P] [US1] Мигрировать `_dashboards.py` миксин в `backend/src/core/superset_client/_dashboards.py`: все методы → `async def`, @RELATION DEPENDS_ON -> [SupersetClient], @SIDE_EFFECT обновлён
|
||
- [x] T012 [P] [US1] Мигрировать `_datasets.py` миксин в `backend/src/core/superset_client/_datasets.py`: все методы → `async def`
|
||
- [x] T013 [P] [US1] Мигрировать `_charts.py` миксин в `backend/src/core/superset_client/_charts.py`: все методы → `async def`
|
||
- [x] T014 [P] [US1] Мигрировать `_sql_lab.py` миксин в `backend/src/core/superset_client/_sql_lab.py`: все методы → `async def`
|
||
- [x] T015 [P] [US1] Мигрировать `_database.py` миксин в `backend/src/core/superset_client/_database.py`: все методы → `async def`
|
||
- [x] T016 [P] [US1] Мигрировать `_import_export.py` миксин в `backend/src/core/superset_client/_import_export.py`: все методы → `async def`
|
||
- [x] T017 [P] [US1] Мигрировать оставшиеся 6 миксинов (`_thumbnail.py`, `_logs.py`, `_css.py`, `_annotation.py`, `_filter.py`, `_query_context.py`) в `backend/src/core/superset_client/_*.py`: `def` → `async def`. (Примечание: `_charts.py` уже мигрирован в T013)
|
||
|
||
### Implementation for US1 — API Routes
|
||
|
||
- [x] T018 [US1] Обновить `backend/src/api/routes/dashboards/_detail_routes.py`: заменить `SupersetClient(env)` на `client = await SupersetClientRegistry.get_client(env_id)` в `get_dashboard_thumbnail()` и `get_dashboard_detail()`. Удалить синхронные `client.network.request()` вызовы. (Факт из кодовой базы: контракты декларируют `@RELATION CALLS -> [AsyncSupersetClient]`, но код использует sync `SupersetClient` — рефакторинг приводит код в соответствие с контрактом. RATIONALE: создание sync-клиента в async def запрещено FR-003; REJECTED: оставить sync — блокирует event loop)
|
||
- [x] T019 [US1] Обновить все остальные ручки, использующие `SupersetClient`, в `backend/src/api/routes/`: найти через grep `SupersetClient(` в `async def`-ручках, заменить на асинхронный вызов. Включить: `_detail_routes.py`, routes миграций, dataset_review, clean_release
|
||
|
||
### Tests for US1
|
||
|
||
- [x] T020 [US1] Конкурентный тест: `test_concurrent_superset_calls` — три параллельных `await asyncio.gather(get_dashboards(), get_dataset_detail(), export_dashboard())` при одном event loop — в `backend/tests/core/test_superset_client_async.py` (SC-001: время ≈ max, не sum)
|
||
- [x] T021 [P] [US1] Тест rejected-path: `test_sync_superset_client_raises_in_async_context` — проверка, что создание sync `SupersetClient(env)` в async def вызывает ошибку/предупреждение — в `backend/tests/api/test_async_guard.py`
|
||
|
||
**Checkpoint**: Superset API полностью асинхронный. Все 13 миксинов и API-ручки мигрированы.
|
||
|
||
---
|
||
|
||
## Phase 4: User Story 2 — Translate Plugin Async (Priority: P2)
|
||
|
||
**Goal**: Предпросмотр перевода и проверка схемы выполняются конкурентно без взаимной блокировки. LLM-вызовы и поллинг SQL Lab не блокируют event loop.
|
||
|
||
**Independent Test**: POST `/translate/jobs/A/preview` одновременно с POST `/translate/check-target-schema`. Проверка схемы возвращает ответ до завершения предпросмотра (SC-001: < 3s).
|
||
|
||
### Implementation for US2
|
||
|
||
- [x] T022 [US2] Создать унифицированный `LLMHttpClient` в `backend/src/plugins/translate/_llm_async_http.py` — модуль [C:4] с `async def call_openai_compatible()` на `httpx.AsyncClient`, rate-limit backoff через `asyncio.sleep` (RATIONALE: объединение дублирующихся _llm_http.py и preview_llm_client.py; REJECTED: AsyncOpenAI SDK — не поддерживает кастомные base_url для OpenRouter/LiteLLM/Kilo)
|
||
- [x] T023 [US2] Обновить `backend/src/plugins/translate/_llm_call.py`: `call_llm_for_batch()` → `async def`, вызов `LLMHttpClient.call_openai_compatible()` с `await`, `time.sleep` → `asyncio.sleep` (RATIONALE: LLM-вызовы не должны блокировать другие запросы)
|
||
- [x] T024 [US2] Мигрировать `backend/src/plugins/translate/preview_executor.py`: `fetch_sample_rows()` → `async def` через `await superset_client.get_chart_data()`, `call_llm()` → `async def` через `await LLMHttpClient.call_openai_compatible()` (RATIONALE: preview использует Superset chart data + LLM — оба должны быть асинхронными)
|
||
- [x] T025 [US2] Мигрировать `backend/src/plugins/translate/superset_executor.py`: `execute_sql()` и `poll_execution_status()` → `async def`. `time.sleep(poll_interval)` → `await asyncio.sleep(poll_interval)`. `client.network.request()` → `await async_client.request()`. (Примечание: общий поллинг до 60×2s=120s; проверка схемы `check_target_schema` использует max_polls=15 — параметры настраиваются вызывающим кодом) (RATIONALE: поллинг до 120s блокировал event loop; REJECTED: time.sleep — блокирует)
|
||
- [x] T026 [US2] Мигрировать `backend/src/plugins/translate/_run_source.py`: `fetch_source_rows()` → `async def` через асинхронный `SupersetClient`
|
||
- [x] T027 [US2] Обновить `backend/src/plugins/translate/preview.py`: `TranslationPreview.preview_rows()` → `async def`, все внутренние вызовы с `await` (RATIONALE: preview — C4 оркестратор, должен быть полностью асинхронным)
|
||
- [x] T028 [US2] Обновить `backend/src/plugins/translate/executor.py`: `TranslationExecutor` → все методы `async def`, `_load_preview_edits()` и `_run_service` → `await`
|
||
|
||
### Tests for US2
|
||
|
||
- [x] T029 [US2] Конкурентный тест: `test_preview_and_schema_check_concurrent` — `asyncio.gather(preview, schema_check)`, проверить что schema_check завершается до preview — в `backend/tests/plugins/translate/test_async_concurrency.py` (SC-001)
|
||
- [x] T030 [US2] Тест rejected-path: `test_polling_uses_asyncio_sleep_not_time_sleep` — статическая проверка, что `time.sleep` не вызывается в `superset_executor.py` — в `backend/tests/plugins/translate/test_async_concurrency.py`
|
||
- [x] T031 [P] [US2] Тест LLM rate-limit backoff: `test_llm_rate_limit_uses_asyncio_sleep` — проверить, что 429 backoff не блокирует другие запросы — в `backend/tests/plugins/translate/test_llm_async.py`
|
||
|
||
**Checkpoint**: Translate plugin полностью асинхронный. Preview + schema check конкурентны.
|
||
|
||
---
|
||
|
||
## Phase 5: User Story 3 — Все плагины Async (Priority: P3)
|
||
|
||
**Goal**: Backup, Git, LLM Analysis, Storage, Compilation Adapter — все операции не блокируют event loop.
|
||
|
||
**Independent Test**: Бэкап 50 дашбордов и деплой из Git выполняются конкурентно.
|
||
|
||
### Implementation for US3
|
||
|
||
- [x] T032 [P] [US3] Мигрировать `backend/src/plugins/backup.py`: `BackupPlugin.execute()` — заменить `SupersetClient(env)` на `client = await SupersetClientRegistry.get_client(env_id)`, все вызовы `client.get_dashboards()`, `client.export_dashboard()` с `await`. Файловый I/O через `run_blocking(kind='file', fn=shutil.copy, ...)` для мелких файлов, `aiofiles` для больших ZIP (>1MB). (RATIONALE: гибридный подход; REJECTED: asyncio.to_thread — default executor без backpressure)
|
||
- [x] T033 [P] [US3] Мигрировать `backend/src/plugins/git_plugin.py`: `_handle_sync()` и `_handle_deploy()` — `shutil` операции через `run_blocking(kind='file', fn=...)`, GitPython-клонирование через `run_blocking(kind='git', fn=...)`, SupersetClient → `SupersetClientRegistry.get_client(env_id)`. (RATIONALE: Git‑клонирование до 30s — в bounded git_executor; REJECTED: оставить в event loop — блокирует)
|
||
- [x] T034 [P] [US3] Мигрировать `backend/src/plugins/llm_analysis/plugin.py`: `_fetch_dashboard_logs()` — заменить `client.network.request(method="GET", endpoint="/log/")` на `await async_client.request(...)` (RATIONALE: sync-вызов внутри async def — прямой блокировщик event loop)
|
||
- [x] T035 [P] [US3] Мигрировать `backend/src/plugins/storage/plugin.py`: `save_file()` — заменить `shutil.copyfileobj()` на `run_blocking(kind='file', fn=...)`; `delete_file()` — `shutil.rmtree` через `run_blocking(kind='file', fn=...)`
|
||
- [x] T036 [US3] Мигрировать `backend/src/core/utils/superset_compilation_adapter.py`: `_request_superset_preview()` и `_request_sql_lab_session()` — заменить `client.network.request()` на асинхронные вызовы
|
||
- [x] T037 [US3] Обновить `backend/src/core/utils/fileio.py`: добавить асинхронные обёртки `async def aread_dashboard_from_disk()`, `async def asave_and_unpack_dashboard()`, `async def acleanup_directory()` — гибрид `aiofiles` + `run_blocking(kind='file', fn=...)` (RATIONALE: см. R8 в research.md)
|
||
|
||
### Tests for US3
|
||
|
||
- [x] T038 [US3] Конкурентный тест плагинов: `test_backup_and_deploy_concurrent` — `asyncio.gather(backup.execute(), git_deploy.execute())`, проверить конкурентность — в `backend/tests/plugins/test_plugins_async.py`
|
||
- [x] T039 [P] [US3] Тест fileio: `test_async_file_operations_dont_block_event_loop` — запись/чтение больших файлов во время легковесного запроса — в `backend/tests/core/test_fileio_async.py`
|
||
|
||
**Checkpoint**: Все плагины асинхронные. Бэкап и деплой конкурентны.
|
||
|
||
---
|
||
|
||
## Phase 6: User Story 4 — Notifications + Task Manager + Services (Priority: P4)
|
||
|
||
**Goal**: Уведомления (SMTP, Telegram, Slack), Task Manager, EventBus, Git-сервисы — полностью асинхронные. ThreadPoolExecutor заменён на asyncio.create_task.
|
||
|
||
**Independent Test**: SMTP-отправка одновременно с API-запросом — запрос не задерживается.
|
||
|
||
### Implementation for US4
|
||
|
||
- [x] T040 [US4] Мигрировать `backend/src/services/notifications/providers.py`: `SMTPProvider.send()` → `async def` с `aiosmtplib.SMTP` вместо `smtplib.SMTP`; `TelegramProvider.send()` → `async def` с `httpx.AsyncClient.post` вместо `requests.post`; `SlackProvider.send()` → аналогично (RATIONALE: aiosmtplib — зрелая async-замена smtplib; REJECTED: asyncio.to_thread для SMTP — работает, но aiosmtplib даёт нативную интеграцию)
|
||
- [x] T041 [US4] Обновить `backend/src/services/notifications/service.py`: `NotificationService.dispatch_report()` — использовать `asyncio.gather` для параллельной отправки через все каналы
|
||
- [x] T042 [US4] Мигрировать `backend/src/core/task_manager/manager.py`: `TaskManager` — заменить `ThreadPoolExecutor(max_workers=5)` на хранение активных `asyncio.Task` в `dict[str, asyncio.Task]`. `run_task()` → `async def`, запуск через `asyncio.create_task()`. `cancel_task()` → `async def` через `task.cancel()` (RATIONALE: задачи разделяют event loop; REJECTED: ThreadPoolExecutor — изоляция задач, усложняет отмену)
|
||
- [x] T043 [US4] Мигрировать `backend/src/core/task_manager/event_bus.py`: `EventBus` — заменить `queue.Queue` + `call_soon_threadsafe` на `asyncio.Queue`. `publish()` → `async def`, `subscribe()` → `async def` async generator (RATIONALE: asyncio.Queue интегрируется с event loop без тредовой синхронизации; REJECTED: queue.Queue + call_soon_threadsafe — избыточно)
|
||
- [x] T044 [US4] Мигрировать `backend/src/core/task_manager/lifecycle.py`: `TaskLifecycle.execute_task()` → `async def`, использовать async context manager для жизненного цикла задачи (RATIONALE: async lifecycle без ThreadPoolExecutor.submit())
|
||
- [x] T045 [US4] Мигрировать `backend/src/services/git/_base.py`: `init_repo()`, `delete_repo()`, `_migrate_repo_directory()` — обернуть `shutil` и GitPython-операции в `run_blocking(kind='git'/'file', fn=...)`
|
||
|
||
### Tests for US4
|
||
|
||
- [x] T046 [US4] Конкурентный тест Task Manager: `test_concurrent_tasks_dont_block` — запустить две задачи через asyncio.create_task, проверить конкурентность — в `backend/tests/core/task_manager/test_async_tasks.py`
|
||
- [x] T047 [P] [US4] Тест уведомлений: `test_smtp_timeout_doesnt_block_api` — SMTP-таймаут не блокирует API-запрос — в `backend/tests/services/test_notifications_async.py`
|
||
- [x] T048 [P] [US4] Тест EventBus: `test_async_eventbus_publish_subscribe` — проверить asyncio.Queue доставку — в `backend/tests/core/task_manager/test_eventbus_async.py`
|
||
|
||
**Checkpoint**: Task Manager и уведомления асинхронные. ThreadPoolExecutor удалён.
|
||
|
||
---
|
||
|
||
## Phase 7: Polish — Test Migration, Cleanup, Tombstones, ADR
|
||
|
||
**Purpose**: Замена requests_mock на pytest_httpx, удаление sync-кода, Tombstone старых контрактов, создание ADR, финальная верификация.
|
||
|
||
### Test Migration
|
||
|
||
- [x] T049 [P] Заменить `requests_mock` на `httpx_mock` (pytest-httpx) во всех тестах модуля translate: `backend/src/plugins/translate/__tests__/test_executor.py`, `test_preview.py`, `test_target_schema.py`, `test_dialect_detection_orthogonal.py` — замена моков, сохранение assertions
|
||
- [x] T050 [P] Заменить `requests_mock` на `httpx_mock` в тестах плагинов: `backend/src/plugins/__tests__/test_backup.py`, `test_git_plugin.py`, `test_storage_plugin.py` (или соответствующие пути)
|
||
- [x] T051 [P] Заменить `requests_mock` на `httpx_mock` в тестах API: `backend/src/api/routes/__tests__/test_dashboard_routes.py`, `test_git_status_route.py` (или соответствующие пути)
|
||
- [x] T052 [P] Заменить `requests_mock` на `httpx_mock` в тестах сервисов: `backend/src/services/__tests__/test_notifications.py`, `test_git_service.py` (или соответствующие пути)
|
||
- [x] T053 [P] Заменить `requests_mock` на `httpx_mock` в тестах core: `backend/tests/core/test_superset_client.py` (или соответствующие пути)
|
||
- [x] T054 Обновить `backend/conftest.py`: добавить глобальную фикстуру `httpx_mock` для pytest-httpx
|
||
|
||
### Cleanup & Tombstones
|
||
|
||
- [x] T055 Пометить `APIClient` в `backend/src/core/utils/network.py` как Tombstone: @DEPRECATED 2026-06-04, @REPLACED_BY -> [AsyncAPIClient]. Удалить тело класса, оставить только заглушку-импорт для обратной совместимости (INV_6: не удалять контракт с входящими @RELATION)
|
||
- [x] T056 Пометить `AsyncSupersetClient` в `backend/src/core/superset_client/async_superset_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [SupersetClient]. Удалить тело класса (INV_6)
|
||
- [x] T057 Пометить `_llm_http.py` и `preview_llm_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [LLMHttpClient]. Удалить тела функций (INV_6)
|
||
- [x] T058 Удалить неиспользуемый sync-код: `import requests` где больше не нужен; удалить `time.sleep` где заменён на `asyncio.sleep`; удалить `ThreadPoolExecutor` импорт из TaskManager
|
||
|
||
### ADR
|
||
|
||
- [x] T059 Создать `docs/adr/ADR-0011-async-backend.md` — запись архитектурного решения о переходе на async-first backend: httpx.AsyncClient, big-bang миграция, asyncio.Semaphore, asyncio.create_task, rejected-пути (aiohttp, multiprocessing, async SQLAlchemy, dual-stack) (Constitution II: Decision Memory)
|
||
|
||
### Final Verification
|
||
|
||
- [x] T060 Запустить все backend-тесты: `cd backend && source .venv/bin/activate && python -m pytest -v` — все тесты должны проходить (SC-005)
|
||
- [x] T061 Запустить линтер: `cd backend && python -m ruff check .` — ноль ошибок
|
||
- [x] T062 Проверить конкурентность вручную по quickstart.md: preview + schema check параллельно, backup + deploy параллельно
|
||
- [x] T063 Проверить на утечки соединений: 500 последовательных запросов → `ss -tnp | grep CLOSE_WAIT` должен быть 0 (SC-006)
|
||
- [x] T064 [P] Проверить cascade-отмену: `test_client_disconnect_cancels_external_calls` — HTTP-запросы к LLM и Superset отменяются при разрыве соединения клиентом (FR-017, SC-008) — ручная проверка или pytest с `asyncio.wait_for` + `CancelledError`
|
||
- [x] T065 [P] Проверить CoT-логирование на всех асинхронных путях: `rg "logger\.(reason|reflect|explore)" backend/src/plugins/translate/ backend/src/core/task_manager/` — убедиться, что все C4/C5 контракты имеют belief-маркеры (FR-020)
|
||
- [x] T066 [P] Статическая проверка SC-008: (a) `rg "SupersetClient\(" backend/src/api/routes/ --files-with-matches | xargs rg "async def"` — ни одна `async def` ручка не создаёт sync `SupersetClient`; (b) `rg "time\.sleep" backend/src/plugins/translate/ backend/src/core/` → только в неисполняемых путях или заменён на `asyncio.sleep`; (c) `rg "^[ ]*def execute\(self," backend/src/plugins/ --files-with-matches` → все плагины реализуют `async def execute`; (d) `rg "import requests\b" backend/src/api/ backend/src/plugins/ backend/src/services/ backend/src/core/` → ноль совпадений (кроме legacy/tombstone файлов)
|
||
- [x] T066a [P] Инвентаризация DB-вызовов: найти все `session.query()`, `session.execute()`, `db.query()` в `backend/src/plugins/`, `backend/src/services/`, `backend/src/api/` — обернуть в `run_blocking(kind='db', fn=...)`. Проверить, что SQLAlchemy-сессии не пересекают границу потоков (FR-025)
|
||
- [x] T066b [P] Найти и заменить все прямые вызовы `requests.post/get/put/delete` вне `APIClient`/`network.py` во всём `backend/src/` (кроме test‑файлов): `rg "requests\.(post|get|put|delete|request)\(" backend/src/ --files-with-matches | grep -v test | grep -v network.py` — все совпадения должны быть заменены на `httpx.AsyncClient` или `asyncio.to_thread`
|
||
- [x] T066c [P] Проверить интеграцию с APScheduler: все вызовы `TaskManager.run_task()` из scheduled jobs должны использовать `asyncio.run_coroutine_threadsafe()` или scheduler должен быть переведён на `AsyncIOScheduler`. Проверить `backend/src/core/scheduler.py`
|
||
- [x] T067 Семантический аудит: `axiom_semantic_validation audit_contracts` — все контракты валидны, нет сиротских @RELATION
|
||
- [x] T068 Семантический аудит belief-протокола: `axiom_semantic_validation audit_belief_protocol` — все C4/C5 контракты имеют @RATIONALE/@REJECTED
|
||
- [x] T069 Перестроить семантический индекс: `axiom_semantic_index rebuild rebuild_mode="full"` — индекс актуален
|
||
|
||
---
|
||
|
||
## Dependencies & Execution Order
|
||
|
||
### Phase Dependencies
|
||
|
||
| Phase | Depends On | Blocks |
|
||
|-------|-----------|--------|
|
||
| Phase 1: Setup | — | — |
|
||
| Phase 2: Foundational (US1 foundation) | Phase 1 | **ВСЕ user stories** |
|
||
| Phase 3: US1 — SupersetClient | Phase 2 | — |
|
||
| Phase 4: US2 — Translate | Phase 2 + Phase 3 | — |
|
||
| Phase 5: US3 — Plugins | Phase 2 + Phase 3 | — |
|
||
| Phase 6: US4 — Notifications + TM | Phase 2 | — |
|
||
| Phase 7: Polish | Phase 3 + 4 + 5 + 6 | — |
|
||
|
||
### User Story Independence
|
||
|
||
- **US1 (P1)**: Зависит от Phase 2. Не зависит от US2/US3/US4. Может быть задеплоена независимо — все плагины уже могут использовать асинхронный SupersetClient.
|
||
- **US2 (P2)**: Зависит от US1 (SupersetClient должен быть готов). Не зависит от US3/US4.
|
||
- **US3 (P3)**: Зависит от US1. Не зависит от US2/US4. Плагины мигрируются независимо друг от друга (T032-T035 — параллельны).
|
||
- **US4 (P4)**: Зависит от Phase 2 (AsyncAPIClient). Не зависит от US1/US2/US3. Task Manager и уведомления независимы от плагинов.
|
||
|
||
### Within Each Phase
|
||
|
||
- **Все Txxx [P]** — могут выполняться параллельно (разные файлы)
|
||
- **Тесты пишутся ПЕРВЫМИ** в каждой фазе — должны FAIL до реализации
|
||
- **Implementation after tests** — последовательно или параллельно (по маркировке [P])
|
||
|
||
### Parallel Opportunities
|
||
|
||
- Phase 2: T005, T006 (тесты) — параллельно (разные test functions в одном файле)
|
||
- Phase 3: T011-T017 (13 миксинов) — все параллельно
|
||
- Phase 5: T032-T035 (4 плагина) — все параллельно
|
||
- Phase 6: T040 (notifications), T042 (TaskManager), T045 (git services) — параллельно
|
||
- Phase 7: T049-T053 (test migration) — все параллельно
|
||
|
||
---
|
||
|
||
## Implementation Strategy
|
||
|
||
### Internal Validation Checkpoint (US1 + US2)
|
||
|
||
1. Phase 1: Setup
|
||
2. Phase 2: Foundational (AsyncAPIClient + SupersetClient)
|
||
3. Phase 3: US1 — все миксины и ручки
|
||
4. Phase 4: US2 — Translate plugin
|
||
5. **STOP AND VALIDATE**: Preview + schema check конкурентны (не deployable milestone — internal checkpoint)
|
||
|
||
### Big-Bang Strategy (All Stories)
|
||
|
||
1. Phase 1 → Phase 2 → Phase 3 (US1) → Phase 4 (US2) → Phase 5 (US3) → Phase 6 (US4) → Phase 7
|
||
2. Все изменения в одном PR (big-bang)
|
||
3. Тесты проходят до и после каждого этапа
|
||
|
||
---
|
||
|
||
## Notes
|
||
|
||
- [P] tasks = разные файлы, без зависимостей — можно параллельно
|
||
- [USx] label = привязка к user story для трассируемости
|
||
- Каждая story проверяется независимо (T020, T029, T038, T046)
|
||
- Ручная проверка конкурентности обязательна (T062) — автоматические тесты могут не поймать все race conditions
|
||
- Big-bang: sync-код НЕ сохраняется для обратной совместимости
|
||
- Tombstone по INV_6: не удалять контракты с входящими @RELATION — помечать @DEPRECATED
|
||
- REJECTED пути защищены тестами: T021 (sync client в async def), T030 (time.sleep в поллинге)
|
||
- Для C4 контрактов: @RATIONALE и @REJECTED обязательны — проверяется T068
|