# Задание A (Antigravity): слой состояния и данных ## Дата поступления 2026-08-21 ## База Проверочный HEAD на момент выдачи: **`50fde5f`**. Перед началом выполнить `git fetch`, зафиксировать фактический `BASE_SHA` и не считать этот SHA актуальным автоматически. ## Ветка `antigravity/state-layer` --- ## ГРАНИЦА РАБОТ — читать первым Параллельно выполняется **Задание B (Codex)** — переработка интерфейса. Чтобы задания не конфликтовали, разделение проходит **по файлам**, а не по смыслу. **Ваша зона (можно менять):** ``` src/antigravity_provider/router/state_store.py src/antigravity_provider/router/unified_health.py src/antigravity_provider/router/scheduler.py src/antigravity_provider/router/event_bus.py src/antigravity_provider/router/quota_collector.py src/antigravity_provider/router/model_registry.py src/antigravity_provider/router/account_identity.py src/antigravity_provider/router/router_engine.py src/antigravity_provider/router/router_config.py src/antigravity_provider/router/session_affinity.py src/antigravity_provider/router/health_tracker.py src/antigravity_provider/router/profile_manager.py src/antigravity_provider/router/auto_assigner.py src/antigravity_provider/router/adapters/** src/antigravity_provider/router/*_oauth.py src/antigravity_provider/*.py scripts/**, installer/**, config/** tests/** (кроме tests/test_ui_*.py) ``` **Чужая зона (НЕ трогать):** ``` src/antigravity_provider/router/ui/** ← весь UI, включая views, components, theme, wizard src/antigravity_provider/router/hermes_hub_app.py tests/test_ui_*.py ``` Если для вашей задачи потребовалось изменить файл из чужой зоны — **это сигнал, что контракт спроектирован неверно**. Вместо правки UI расширьте ViewModel или событие. --- ## P0. Контракт ViewModel публикуется первым До любых других изменений зафиксировать и запушить контракт, против которого Codex будет писать интерфейс: `docs/UI_STATE_CONTRACT.md`. Описать точно, по фактическому коду: - `HubSnapshot` — состав, гарантии консистентности, поле версии/seq; - `ProfileViewModel` — все поля, какие опциональны, что означает каждое состояние `auth_state`/`health_state`; - `QuotaSnapshot` и `QuotaBucket` — полная схема (`id`, `label`, `used_percent`, `remaining_percent`, `used`, `limit`, `reset_at`, `unit`, `scope`, `model_family`), какие поля реально заполняются **для каждого провайдера отдельно**, и признак `is_estimated`; - `AgentViewModel`, `RolePipeline`, `PipelineNode`, `ProviderSummary`, `SystemReadiness`; - перечень событий `event_bus` с полезной нагрузкой: что именно приходит при изменении квоты одного аккаунта, при добавлении аккаунта, при смене маршрута. **Критично:** для каждого поля указать, реально ли backend его отдаёт, или это заглушка. Codex обязан отличать «данных нет» от «данные есть». Раздел «Backend gaps» обязателен. Этот документ — интерфейс между двумя заданиями. После публикации менять его только с явной пометкой в отчёте. --- ## Область задачи ### 1. Единый источник состояния Завершить `HubSnapshot` как единственный источник для UI. Ни один view не должен иметь возможности самостоятельно инициировать сканирование — не потому, что это запрещено правилом, а потому, что данные ему приходят готовыми. Убрать вызовы `scan_all()` из кода, доступного UI: сервис отдаёт снапшот, обновляет его сам. ### 2. Централизованный планировщик обновлений На базе существующего `HermesRefreshScheduler`: - интервалы на провайдера; - дедупликация одновременных запросов; - защита от устаревшего ответа (поздний ответ не перезаписывает более свежее состояние — использовать `seq`); - раздельные операции: обновить один аккаунт / одного провайдера / всё. ### 3. Событийная модель вместо полного пересбора Изменение квоты одного аккаунта обязано порождать точечное событие с идентификатором аккаунта, а не сигнал «всё изменилось». То же для добавления/удаления аккаунта, смены авторизации, смены активного маршрута. **Отдельно:** OAuth — самостоятельная state machine. Не подключать OAuth listener, таймауты авторизации и жизненный цикл PKCE к общему планировщику. После успешной авторизации: сохранить аккаунт → событие `ACCOUNT_ADDED` → точечное обновление, **без** глобального пересканирования. ### 4. Мульти-корзинные квоты Довести `quota_collector` до реального сбора там, где провайдер это отдаёт. Требования: - не сводить разные лимиты к одному проценту; - корзины привязаны к семейству моделей, если у провайдера лимиты раздельные (Antigravity: Gemini и не-Gemini — проверить фактическим кодом, что именно доступно); - каждый снапшот честно помечен `is_estimated` и источником; запрет на `*_api` для локально вычисленных значений остаётся в силе; - если провайдер не отдаёт данных — отдавать отсутствие данных, а не число. Роутер должен понимать, что переключение модели может сменить квотный пул. ### 5. Реестр моделей и маршрутизация Довести `model_registry` и capability-роутинг: выбор модели с учётом возможностей, стоимости и остатка квоты. Учитывать `preferred_models` профиля и приоритет роли. ### 6. Долги, оставшиеся открытыми - **Мёртвые модули.** `capability_matrix`, `lifecycle_supervisor`, `skill_registry` сейчас лишь реэкспортируются из `router/__init__.py` — ни один кодовый путь их не использует. Реэкспорт не равен интеграции. Либо подключить по-настоящему, либо удалить и записать причину. - **`HKCU` в тестах.** `APPDATA`/`USERPROFILE` теперь в песочнице, но `HermesHubSetup.cs` пишет в реестр двумя вызовами `Registry.CurrentUser`, а реестр переменными окружения не перенаправляется. Вынести реальный запуск установщика в явный integration-режим либо параметризовать ключ реестра. - **Комментарии `router_profiles.yaml`** стираются при сохранении (5 строк → 0). Оценить `ruamel.yaml`; если замена YAML-стека велика — зафиксировать отдельным долгом и не расширять scope. - **Сериализация Antigravity.** `_AGY_INVOCATION_LOCK` вернул корректность ценой параллелизма: вызовы профилей со своим auth снова идут по одному. Оценить отказ от глобальной записи `gemini:antigravity` в пользу изоляции только через `USERPROFILE` — это снимет и лок, и гонку. Если не делать — записать как осознанный долг. ### 7. Релизная инфраструктура - выложить ассет `hermes-hub-0.1.1.zip` с суммой из манифеста (сейчас `package_url` → HTTP 404) и `HermesHubSetup.exe` в GitHub Release; - `dist/checksums.txt` — только из `scripts/build_dist.py`, вручную не править; - порядок публикации: build → checksum → upload → verify → publish manifest → verify feed. Манифест не должен рекламировать несуществующий артефакт. **Тег `v0.1.1` не создавать.** --- --- ## P0-bis. Найдено при прогоне с установленными UI-зависимостями Обнаружено 2026-08-21 при проверке PHASE 1 Codex. Эти дефекты **не видны в обычном прогоне**: без `customtkinter` соответствующие тесты пропускаются, поэтому suite зелёный, а код не исполняется. ### 1. Подмена `sys.path` заставляет тесты грузить УСТАНОВЛЕННУЮ копию плагина `hermes_hub_app.py:41` при импорте делает `sys.path.insert(0, ...)` для `%LOCALAPPDATA%\hermes\pluginsntigravity-provider\src`. Как только любой тест импортирует этот модуль, путь установленной копии оказывается **впереди** репозитория, и последующие импорты `antigravity_provider.*` резолвятся в развёрнутую — то есть более старую — версию. Проявление: `tests/test_p0_release_gate.py::test_p0_5_b1_non_router_error_fallback` и `::test_n2_error_formatter_deduplication` падают с `ImportError: format_antigravity_error` **только в полном прогоне**, а по отдельности проходят. Проверено: функция есть в `src/antigravity_provider/runtime.py` и отсутствует в установленной копии. Последствия шире тестов: в проде приложение может исполнять развёрнутый плагин вместо ожидаемого кода, а после обновления — смесь версий. Пакет `antigravity_provider` — namespace-типа (нет `__init__.py`), поэтому его `__path__` спокойно объединяет оба каталога. **Нужно:** определить единственный источник импорта. Либо приложение не трогает `sys.path` и полагается на установленный пакет, либо repo-режим явно вытесняет установленный. Смешанное состояние недопустимо. Добавить тест, фиксирующий, из какого каталога загружен `antigravity_provider`. ### 2. `test_oauth_lifecycle.py` — два теста падают при реальном исполнении ``` FAILED test_e_repeated_open_browser_invariance FAILED test_f_copy_before_open_browser assert wizard.oauth_url is not None E assert None is not None ``` Падают и по отдельности, и в полном прогоне — от порядка не зависят. То есть заявленная в `249a888` «immediate URL readiness» этими тестами не подтверждена. Разобраться: дефект в продукте или в тесте (например, мок `start_profile_oauth` не покрывает путь `_init_antigravity_oauth`). Пока не выяснено — считать функцию непроверенной. ### 3. Заглушка в `conftest.py` слишком широкая `pytest_collection_modifyitems` пропускает тест, если в его имени встречается `ui`, `view` или `wizard`. Под это попадают и статические проверки, которым GUI не нужен, — из-за чего оба дефекта выше жили незамеченными. Заменить на явную маркировку (`@pytest.mark.gui`) либо на проверку реального импорта GUI-модулей. **Критерий приёмки:** полный `pytest` зелёный **и** в окружении без UI-зависимостей, **и** в окружении с установленными `customtkinter`/`pillow`/`psutil`. Сейчас второй вариант даёт 4 падения. ## Критерии приёмки 1. `docs/UI_STATE_CONTRACT.md` опубликован до остальных изменений, содержит раздел «Backend gaps» и для каждого поля — признак реальности данных. 2. Ни один файл из чужой зоны не изменён (`git diff --name-only BASE_SHA..HEAD -- src/antigravity_provider/router/ui src/antigravity_provider/router/hermes_hub_app.py` пуст). 3. `scan_all()` недоступен из UI-слоя; сервис обновляет снапшот сам. 4. Изменение квоты одного аккаунта порождает событие с идентификатором аккаунта; тест это проверяет. 5. Поздний ответ не перезаписывает более свежее состояние; тест на гонку `seq`. 6. OAuth-сессия не управляется общим планировщиком; успешная авторизация даёт точечное обновление, тест это фиксирует. 7. Мульти-корзинные квоты: для каждого провайдера в контракте указано, какие корзины реально доступны; ни одна локально вычисленная величина не помечена источником провайдера. 8. Мёртвые модули либо интегрированы (есть вызывающий код вне `__init__.py`), либо удалены. 9. Тесты установщика не оставляют записей в `HKCU` при обычном прогоне. 10. `package_url` из манифеста отдаёт HTTP 200, sha256 совпадает с загруженным артефактом. 11. Полный `pytest` зелёный **в окружении без UI-зависимостей**; `ruff check .` чисто; release gate PASSED на финальном коммите. 12. Отчёт содержит `BASE_SHA`, `FINAL_SHA`, `origin/main`, `git status`, точный результат тестов (`X passed / Y skipped / Z failed`) и список того, что осталось. ## Порядок сдачи Никаких функциональных коммитов после последнего запуска release gate. Ревьюеру передать точный `FINAL_COMMIT_SHA` и не продолжать разработку поверх него до вердикта.