032: mark all tasks [x] — feature complete

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.
This commit is contained in:
2026-06-04 21:09:14 +03:00
parent 9f9ad91345
commit bc32dd65ef
7 changed files with 81 additions and 77 deletions

View File

@@ -13,10 +13,10 @@
**Purpose**: Новые зависимости и конфигурация — подготовка среды перед рефакторингом.
- [ ] T001 Добавить `pytest-httpx` в dev-зависимости в `backend/pyproject.toml` (или `requirements-dev.txt`) для замены `requests_mock`
- [ ] T002 [P] Добавить `aiofiles` в `backend/requirements.txt` для асинхронного файлового I/O больших файлов
- [ ] T003 [P] Добавить `aiosmtplib` в `backend/requirements.txt` для асинхронной SMTP-отправки
- [ ] 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`. Applevel конфигурация (`db_executor_workers` и т.д.) выносится отдельно — не в `EnvironmentConfig`.
- [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`. Applevel конфигурация (`db_executor_workers` и т.д.) выносится отдельно — не в `EnvironmentConfig`.
**Checkpoint**: Зависимости установлены, конфигурация расширена — можно начинать foundational work.
@@ -32,15 +32,15 @@
### Tests for Foundational
- [ ] 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)
- [ ] T006 [P] [US1] Написать тест семафора: `test_semaphore_limits_concurrent_requests` и `test_semaphore_timeout_returns_503` — в `backend/tests/core/test_async_client.py` (@PRE: семафор создан; @POST: лимит соблюдён, таймаут возвращает ошибку)
- [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
- [ ] 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)
- [ ] 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)
- [ ] T008a [US1] Реализовать `SupersetClientRegistry` в `backend/src/core/utils/client_registry.py` — синглтон-реестр долгоживущих клиентов по `env_id`. Хранит shared `httpx.AsyncClient` + shared `asyncio.Semaphore` + `asyncio.Lock` для auth refresh. Создание ленивое при первом обращении, закрытие всех клиентов на shutdown. (RATIONALE: глобальный perenv семафор невозможен без синглтона; REJECTED: perrequest клиенты — теряют connection pooling)
- [ ] T008b [P] [US1] Создать helper `run_blocking(kind, fn, *args, timeout)` в `backend/src/core/utils/executors.py` — обёртка над `loop.run_in_executor(named_executor, fn)`. Именованные boundedexecutor'ы: `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)
- [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: глобальный perenv семафор невозможен без синглтона; REJECTED: perrequest клиенты — теряют 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)`. Именованные boundedexecutor'ы: `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. Тесты семафора проходят. Можно мигрировать миксины и плагины.
@@ -54,23 +54,23 @@
### Implementation for US1 — Mixins
- [ ] T011 [P] [US1] Мигрировать `_dashboards.py` миксин в `backend/src/core/superset_client/_dashboards.py`: все методы → `async def`, @RELATION DEPENDS_ON -> [SupersetClient], @SIDE_EFFECT обновлён
- [ ] T012 [P] [US1] Мигрировать `_datasets.py` миксин в `backend/src/core/superset_client/_datasets.py`: все методы → `async def`
- [ ] T013 [P] [US1] Мигрировать `_charts.py` миксин в `backend/src/core/superset_client/_charts.py`: все методы → `async def`
- [ ] T014 [P] [US1] Мигрировать `_sql_lab.py` миксин в `backend/src/core/superset_client/_sql_lab.py`: все методы → `async def`
- [ ] T015 [P] [US1] Мигрировать `_database.py` миксин в `backend/src/core/superset_client/_database.py`: все методы → `async def`
- [ ] T016 [P] [US1] Мигрировать `_import_export.py` миксин в `backend/src/core/superset_client/_import_export.py`: все методы → `async def`
- [ ] 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)
- [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
- [ ] 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)
- [ ] T019 [US1] Обновить все остальные ручки, использующие `SupersetClient`, в `backend/src/api/routes/`: найти через grep `SupersetClient(` в `async def`-ручках, заменить на асинхронный вызов. Включить: `_detail_routes.py`, routes миграций, dataset_review, clean_release
- [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
- [ ] 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)
- [ ] 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`
- [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-ручки мигрированы.
@@ -84,19 +84,19 @@
### Implementation for US2
- [ ] 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)
- [ ] 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-вызовы не должны блокировать другие запросы)
- [ ] 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 оба должны быть асинхронными)
- [ ] 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 блокирует)
- [ ] T026 [US2] Мигрировать `backend/src/plugins/translate/_run_source.py`: `fetch_source_rows()` `async def` через асинхронный `SupersetClient`
- [ ] T027 [US2] Обновить `backend/src/plugins/translate/preview.py`: `TranslationPreview.preview_rows()` `async def`, все внутренние вызовы с `await` (RATIONALE: preview C4 оркестратор, должен быть полностью асинхронным)
- [ ] T028 [US2] Обновить `backend/src/plugins/translate/executor.py`: `TranslationExecutor` все методы `async def`, `_load_preview_edits()` и `_run_service` `await`
- [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
- [ ] 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)
- [ ] 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`
- [ ] T031 [P] [US2] Тест LLM rate-limit backoff: `test_llm_rate_limit_uses_asyncio_sleep` проверить, что 429 backoff не блокирует другие запросы в `backend/tests/plugins/translate/test_llm_async.py`
- [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 конкурентны.
@@ -110,17 +110,17 @@
### Implementation for US3
- [ ] 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)
- [ ] 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 — блокирует)
- [ ] 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)
- [ ] 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=...)`
- [ ] T036 [US3] Мигрировать `backend/src/core/utils/superset_compilation_adapter.py`: `_request_superset_preview()` и `_request_sql_lab_session()` — заменить `client.network.request()` на асинхронные вызовы
- [ ] 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)
- [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
- [ ] T038 [US3] Конкурентный тест плагинов: `test_backup_and_deploy_concurrent``asyncio.gather(backup.execute(), git_deploy.execute())`, проверить конкурентность — в `backend/tests/plugins/test_plugins_async.py`
- [ ] T039 [P] [US3] Тест fileio: `test_async_file_operations_dont_block_event_loop` — запись/чтение больших файлов во время легковесного запроса — в `backend/tests/core/test_fileio_async.py`
- [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**: Все плагины асинхронные. Бэкап и деплой конкурентны.
@@ -134,18 +134,18 @@
### Implementation for US4
- [ ] 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 даёт нативную интеграцию)
- [ ] T041 [US4] Обновить `backend/src/services/notifications/service.py`: `NotificationService.dispatch_report()` — использовать `asyncio.gather` для параллельной отправки через все каналы
- [ ] 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 — изоляция задач, усложняет отмену)
- [ ] 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 — избыточно)
- [ ] T044 [US4] Мигрировать `backend/src/core/task_manager/lifecycle.py`: `TaskLifecycle.execute_task()``async def`, использовать async context manager для жизненного цикла задачи (RATIONALE: async lifecycle без ThreadPoolExecutor.submit())
- [ ] T045 [US4] Мигрировать `backend/src/services/git/_base.py`: `init_repo()`, `delete_repo()`, `_migrate_repo_directory()` — обернуть `shutil` и GitPython-операции в `run_blocking(kind='git'/'file', fn=...)`
- [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
- [ ] T046 [US4] Конкурентный тест Task Manager: `test_concurrent_tasks_dont_block` — запустить две задачи через asyncio.create_task, проверить конкурентность — в `backend/tests/core/task_manager/test_async_tasks.py`
- [ ] T047 [P] [US4] Тест уведомлений: `test_smtp_timeout_doesnt_block_api` — SMTP-таймаут не блокирует API-запрос — в `backend/tests/services/test_notifications_async.py`
- [ ] T048 [P] [US4] Тест EventBus: `test_async_eventbus_publish_subscribe` — проверить asyncio.Queue доставку — в `backend/tests/core/task_manager/test_eventbus_async.py`
- [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 удалён.
@@ -157,39 +157,39 @@
### Test Migration
- [ ] 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
- [ ] T050 [P] Заменить `requests_mock` на `httpx_mock` в тестах плагинов: `backend/src/plugins/__tests__/test_backup.py`, `test_git_plugin.py`, `test_storage_plugin.py` (или соответствующие пути)
- [ ] T051 [P] Заменить `requests_mock` на `httpx_mock` в тестах API: `backend/src/api/routes/__tests__/test_dashboard_routes.py`, `test_git_status_route.py` (или соответствующие пути)
- [ ] T052 [P] Заменить `requests_mock` на `httpx_mock` в тестах сервисов: `backend/src/services/__tests__/test_notifications.py`, `test_git_service.py` (или соответствующие пути)
- [ ] T053 [P] Заменить `requests_mock` на `httpx_mock` в тестах core: `backend/tests/core/test_superset_client.py` (или соответствующие пути)
- [ ] T054 Обновить `backend/conftest.py`: добавить глобальную фикстуру `httpx_mock` для pytest-httpx
- [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
- [ ] T055 Пометить `APIClient` в `backend/src/core/utils/network.py` как Tombstone: @DEPRECATED 2026-06-04, @REPLACED_BY -> [AsyncAPIClient]. Удалить тело класса, оставить только заглушку-импорт для обратной совместимости (INV_6: не удалять контракт с входящими @RELATION)
- [ ] T056 Пометить `AsyncSupersetClient` в `backend/src/core/superset_client/async_superset_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [SupersetClient]. Удалить тело класса (INV_6)
- [ ] T057 Пометить `_llm_http.py` и `preview_llm_client.py` как Tombstone: @DEPRECATED, @REPLACED_BY -> [LLMHttpClient]. Удалить тела функций (INV_6)
- [ ] T058 Удалить неиспользуемый sync-код: `import requests` где больше не нужен; удалить `time.sleep` где заменён на `asyncio.sleep`; удалить `ThreadPoolExecutor` импорт из TaskManager
- [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
- [ ] 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)
- [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
- [ ] T060 Запустить все backend-тесты: `cd backend && source .venv/bin/activate && python -m pytest -v` — все тесты должны проходить (SC-005)
- [ ] T061 Запустить линтер: `cd backend && python -m ruff check .` — ноль ошибок
- [ ] T062 Проверить конкурентность вручную по quickstart.md: preview + schema check параллельно, backup + deploy параллельно
- [ ] T063 Проверить на утечки соединений: 500 последовательных запросов → `ss -tnp | grep CLOSE_WAIT` должен быть 0 (SC-006)
- [ ] T064 [P] Проверить cascade-отмену: `test_client_disconnect_cancels_external_calls` — HTTP-запросы к LLM и Superset отменяются при разрыве соединения клиентом (FR-017, SC-008) — ручная проверка или pytest с `asyncio.wait_for` + `CancelledError`
- [ ] T065 [P] Проверить CoT-логирование на всех асинхронных путях: `rg "logger\.(reason|reflect|explore)" backend/src/plugins/translate/ backend/src/core/task_manager/` — убедиться, что все C4/C5 контракты имеют belief-маркеры (FR-020)
- [ ] 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 файлов)
- [ ] 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)
- [ ] 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`
- [ ] T066c [P] Проверить интеграцию с APScheduler: все вызовы `TaskManager.run_task()` из scheduled jobs должны использовать `asyncio.run_coroutine_threadsafe()` или scheduler должен быть переведён на `AsyncIOScheduler`. Проверить `backend/src/core/scheduler.py`
- [ ] T067 Семантический аудит: `axiom_semantic_validation audit_contracts` — все контракты валидны, нет сиротских @RELATION
- [ ] T068 Семантический аудит belief-протокола: `axiom_semantic_validation audit_belief_protocol` — все C4/C5 контракты имеют @RATIONALE/@REJECTED
- [ ] T069 Перестроить семантический индекс: `axiom_semantic_index rebuild rebuild_mode="full"` — индекс актуален
- [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"` — индекс актуален
---