diff --git a/docs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md b/docs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md new file mode 100644 index 0000000..43d7721 --- /dev/null +++ b/docs/work-items/ORCH-017/06-adr/ADR-001-telegram-approve-links.md @@ -0,0 +1,117 @@ +# ADR-001: Прямые ссылки в Telegram-уведомлении об апруве BRD (формат и Plane-URL) + +Work Item: **ORCH-017** · Repo: `orchestrator` · Стадия: architecture +Тип: per-work-item ADR (НЕ сквозной — реестр гейтов/стадий/компонентов не меняется). + +## Статус +Accepted + +## Контекст +BRD (`01-brd.md`) и ТЗ (`02-trz.md`) требуют добавить в пингующее уведомление об апруве +BRD (`notify_approve_requested(task_id)` в `src/notifications.py`) две кликабельные ссылки: +на документ `01-brd.md` в Gitea и на Plane-issue. ТЗ намеренно оставило за стадией +architecture три развилки (открытые вопросы `01-brd.md` §8): + +1. **§8.1 — формат ссылок:** HTML-`` в тексте (минимум) **vs** inline-кнопки + (`reply_markup` в `send_telegram`). +2. **§8.4 — формат Plane-URL:** полный путь `.../projects/{project_id}/issues/{issue_id}/` + **vs** короткий `.../browse//`. +3. **§8.3 — внешний web-URL Plane:** в конфиге есть только внутренний `plane_api_url` + (`http://localhost:8091`), непригодный для браузерной ссылки. + +Жёсткое ограничение контекста — **self-hosting**: правка живёт в инструменте, который сейчас +обслуживает другие проекты из общего прод-контейнера. Любое расширение blast radius +(особенно правка разделяемой функции `send_telegram`, которой пользуется и живой трекер +PR #21/#22) — групповой риск. Поэтому из равноценных вариантов выбирается тот, что меняет +меньше кода и не трогает общие точки. + +Фактическое состояние кода, проверенное на ветке: +- `send_telegram(text, disable_notification=False)` (`src/notifications.py:42`) шлёт + `parse_mode="HTML"` — HTML-`` работает без изменения сигнатуры. +- Эталон branch-view ссылки на доки — `src/usage.py:455-458`: + `base = (gitea_public_url or gitea_url).rstrip('/')`, `owner = gitea_owner`, + URL `{base}/{owner}/{repo}/src/branch/{branch}/`. +- Plane-issue uuid надёжно лежит в `tasks.plane_issue_id`; `project_id` берётся через + `projects.get_project_by_repo(repo).plane_project_id`. +- В `plane_sync.py` строки `.../workspaces/{slug}/projects/{pid}/issues/{id}/` — это **API** + путь (`{plane_api_url}/api/v1/...`), НЕ браузерный. Браузерный роут Plane — + `{web_base}/{workspace_slug}/projects/{project_id}/issues/{issue_id}` (без `/api/v1`, + без сегмента `/workspaces/`). + +## Решение + +### Р-1 (§8.1) — HTML-ссылки в тексте. Inline-кнопки отклонены. +Ссылки встраиваются как `подпись` в текст того же одного сообщения. +**`send_telegram` НЕ трогаем** (сигнатура без `reply_markup`). Inline-кнопки потребовали бы +правки разделяемой функции, которой пользуется живой трекер, — это рост blast radius без +бизнес-выгоды для одной точки уведомления. Расширение до кнопок — **вне объёма ORCH-017**; +при реальной потребности заводится отдельный work item. + +### Р-2 (§8.4) — полный путь Plane-issue по uuid. Короткий `browse/` отклонён. +Формат: +``` +{plane_web_base}/{workspace_slug}/projects/{project_id}/issues/{plane_issue_id}/ +``` +Источники: `plane_web_base` (Р-3), `workspace_slug = settings.plane_workspace_slug`, +`project_id = get_project_by_repo(repo).plane_project_id`, `plane_issue_id = tasks.plane_issue_id`. +Короткий `browse/` отклонён: он опирается на совпадение `work_item_id` с Plane-identifier, +которое не гарантировано из-за zero-padding (`ORCH-017` в БД vs `ORCH-17` как identifier). +uuid в `plane_issue_id` — детерминированный и уже в наличии источник. + +### Р-3 (§8.3) — новая настройка `ORCH_PLANE_WEB_URL` + loopback-guard. +В `src/config.py` добавляется `plane_web_url: str = ""` (env `ORCH_PLANE_WEB_URL`). +База резолвится как: +```python +plane_web_base = (settings.plane_web_url or settings.plane_api_url).rstrip("/") +``` +**Loopback-guard (разрешение конфликта AC-2 ↔ AC-3):** дефолт-фоллбэк `plane_api_url` равен +`http://localhost:8091` и снаружи хоста не кликается. Поэтому: если итоговый `plane_web_base` +указывает на loopback/локальный хост (`localhost`, `127.0.0.1`, `0.0.0.0`, `[::1]`) **или** +пуст — **Plane-ссылка опускается целиком** (а не вставляется битой). Так одновременно: +AC-2 (не выпускаем localhost-ссылку), AC-3 (цепочка фоллбэка соблюдена как попытка), +AC-6/NFR-1 (никаких исключений, сообщение уходит без отсутствующей ссылки). + +### Р-4 — graceful degradation как контракт построения ссылок. +Чтение `repo/branch/plane_issue_id` из `tasks` — один SELECT в `try/except`. Каждая из двух +ссылок строится независимо; при нехватке данных конкретная ссылка опускается, призыв +«Переведите задачу в статус Approved …» и само сообщение сохраняются всегда. Динамические +подписи — через `html.escape`; URL формируются только из доверенных конфиг/БД-значений. + +### Р-5 — инвариант «одно сообщение, без дублей». +Порядок действий в `notify_approve_requested` сохраняется: `mark_brd_review_started` → +`update_task_tracker(task_id)` → один `send_telegram(msg)` (пингующий, не silent). Живой +трекер не дублируется. Реестр `QG_CHECKS`, стадии, `:approved:`-handler, +`check_analysis_approved` — без изменений (правка — отображение, не управление конвейером). + +## Затронутые модули (для стадии development) +| Модуль | Изменение | +|--------|-----------| +| `src/notifications.py` | `notify_approve_requested`: SELECT `repo/branch/plane_issue_id`; сборка двух ссылок (Р-2/Р-3/Р-4); встраивание в текст. | +| `src/config.py` | `Settings.plane_web_url: str = ""` (env `ORCH_PLANE_WEB_URL`). | +| `src/projects.py` | (чтение) `get_project_by_repo(repo).plane_project_id`. | +| `src/usage.py` | (референс, НЕ править) паттерн branch-view URL. | +| `.env.example`, `CHANGELOG.md`, env-карта (`CLAUDE.md`/`INFRA.md`) | документация в том же PR. | + +Без изменений API и схемы БД. Все требуемые поля уже есть в `tasks`. + +## Последствия +**Плюсы:** +- Минимальный blast radius: разделяемая `send_telegram` не тронута → нулевой риск для живого + трекера и прочих уведомлений; безопасно для self-hosting. +- Детерминированная Plane-ссылка (uuid), не зависит от zero-padding identifier. +- Loopback-guard снимает противоречие AC-2/AC-3 и исключает «битые localhost-ссылки» в проде. +- Деплой штатный: не требует рестарта прод-контейнера сверх обычного деплоя; деплой ORCH + идёт через обязательный `deploy-staging` (8501). + +**Минусы / ограничения:** +- Нет inline-кнопок (по дизайну отклонено) — UX чуть менее «кнопочный»; при необходимости + отдельный work item. +- Plane-ссылка появится только после задания `ORCH_PLANE_WEB_URL` на хосте (`.env`/`.env.staging`) + — см. `07-infra-requirements.md`. До этого момента graceful degradation: уведомление уходит + только с BRD-ссылкой. +- Корректность браузерного роута Plane (`/{workspace}/projects/{id}/issues/{id}/`) зависит от + версии Plane; риск зафиксирован в `10-tech-risks.md`. + +## Открытые вопросы, переданные дальше +- **Значение `ORCH_PLANE_WEB_URL`** подтверждает Owner/INFRA при деплое (см. `07-infra-requirements.md`). + Это конфиг-параметр, а не блокер архитектуры. diff --git a/docs/work-items/ORCH-017/07-infra-requirements.md b/docs/work-items/ORCH-017/07-infra-requirements.md new file mode 100644 index 0000000..bf517fa --- /dev/null +++ b/docs/work-items/ORCH-017/07-infra-requirements.md @@ -0,0 +1,38 @@ +# 07-Infra Requirements — ORCH-017 + +Work Item: **ORCH-017** · Repo: `orchestrator` +Опирается на ADR-001 (Р-3). Меняется только env-карта; топология контейнеров/портов — без изменений. + +## 1. Новая env-переменная +| Ключ | env | Дефолт | Назначение | +|------|-----|--------|------------| +| `plane_web_url` | `ORCH_PLANE_WEB_URL` | `""` (пусто) | Внешний **браузерный** базовый URL Plane для кликабельной ссылки на issue из Telegram. НЕ путать с внутренним `ORCH_PLANE_API_URL` (`http://localhost:8091`), который пригоден только для API. | + +### Семантика резолва (ADR-001 Р-3) +``` +plane_web_base = (ORCH_PLANE_WEB_URL or ORCH_PLANE_API_URL).rstrip("/") +``` +- Если `plane_web_base` пуст **или** указывает на loopback (`localhost`, `127.0.0.1`, + `0.0.0.0`, `[::1]`) — Plane-ссылка **опускается** (graceful degradation, NFR-1). Без + заданного `ORCH_PLANE_WEB_URL` уведомление уходит только с BRD-ссылкой — это нормально. + +## 2. Что требуется от Owner / INFRA +1. **Подтвердить значение `ORCH_PLANE_WEB_URL`** — внешний адрес Plane UI (тот, по которому + Слава открывает Plane в браузере). Это единственный внешний вход, требующий решения Owner. +2. Прописать ключ в `.env` (prod-хост) и `.env.staging` (staging-песочница). В git значение + НЕ коммитится — канон секретов/настроек (`.env.example` — образец без значения). +3. Браузерный роут issue, который будет собран: + `{ORCH_PLANE_WEB_URL}/{ORCH_PLANE_WORKSPACE_SLUG}/projects/{plane_project_id}/issues/{plane_issue_id}/`. + Проверить на одной задаче, что он открывается в текущей версии Plane (см. риск R-3 в + `10-tech-risks.md`). + +## 3. Переиспользуемые (без изменений) настройки +- `ORCH_GITEA_PUBLIC_URL` / `ORCH_GITEA_URL`, `ORCH_GITEA_OWNER` — для BRD-ссылки. +- `ORCH_PLANE_WORKSPACE_SLUG` — workspace в Plane-URL. + +## 4. Топология / деплой +- Контейнеры, порты, сети — **без изменений**. Новый ключ читается из `.env` при старте + (`pydantic Settings`, `env_prefix=ORCH_`). +- Деплой self (ORCH) — штатный, через обязательный `deploy-staging` (8501) перед прод-деплоем + (`orchestrator`, 8500). Рестарт прода сверх обычного деплоя НЕ требуется. +- Документировать ключ в env-карте: `CLAUDE.md` и/или `docs/operations/INFRA.md` (в том же PR). diff --git a/docs/work-items/ORCH-017/10-tech-risks.md b/docs/work-items/ORCH-017/10-tech-risks.md new file mode 100644 index 0000000..1da6cc8 --- /dev/null +++ b/docs/work-items/ORCH-017/10-tech-risks.md @@ -0,0 +1,19 @@ +# 10-Tech Risks — ORCH-017 + +Work Item: **ORCH-017** · Repo: `orchestrator` +Опирается на ADR-001. Шкала: вероятность × влияние. + +| ID | Риск | Вер. | Влияние | Митигация | +|----|------|------|---------|-----------| +| R-1 | **Self-hosting: уведомление роняет поток.** Исключение при построении ссылок (нет данных в `tasks`, неконсистентный реестр проектов) прерывает `notify_approve_requested` и тормозит конвейер всех проектов. | Низк. | Выс. | NFR-1/ADR Р-4: один SELECT в `try/except`, каждая ссылка строится независимо и опускается при нехватке данных; сообщение и призыв отправляются всегда. Тест на ветви degradation (`tests/test_notify_approve_links.py`). | +| R-2 | **Битый/непубличный Plane-URL.** Фоллбэк на `plane_api_url=localhost:8091` дал бы некликабельную ссылку снаружи хоста (нарушение AC-2). | Сред. | Сред. | ADR Р-3 loopback-guard: при пустом/loopback базовом URL Plane-ссылка опускается, а не вставляется битой. Значение `ORCH_PLANE_WEB_URL` подтверждает Owner/INFRA (`07-infra-requirements.md`). | +| R-3 | **Несовпадение браузерного роута Plane.** Формат `/{workspace}/projects/{id}/issues/{id}/` зависит от версии Plane; иной роут → ссылка ведёт в никуда (открывается, но не на ту issue). | Низк. | Сред. | Проверить роут на одной реальной задаче после задания `ORCH_PLANE_WEB_URL` (acceptance в staging). uuid `plane_issue_id` детерминирован — ошибка может быть только в шаблоне пути, не в идентификаторе. | +| R-4 | **Поломка HTML-разметки сообщения.** Неэкранированная динамическая подпись (напр. символы `<`/`&` в `work_item_id`/title) ломает `parse_mode="HTML"` → Telegram отвергает сообщение. | Низк. | Сред. | NFR-3/ADR Р-4: `html.escape` на всех подписях; URL только из доверенных конфиг/БД-значений. Тест на спецсимволы. | +| R-5 | **Регрессия «дубль-сообщения».** Случайное добавление второго `send_telegram` или повторная отправка трекера как нового сообщения. | Низк. | Низк. | ADR Р-5: инвариант «один `send_telegram`», порядок действий зафиксирован; регресс-тесты `tests/test_telegram_tracker.py`, `tests/test_notify_done_regression.py`. | +| R-6 | **Zero-padding identifier.** Короткий `browse/` промахнулся бы по issue (`ORCH-017` vs `ORCH-17`). | — | — | Снят на корню: ADR Р-2 использует uuid `plane_issue_id`, короткий формат отклонён. | + +## Сводно +Изменение косметическое и изолированное: нет правок реестра гейтов/стадий, схемы БД, API и +разделяемой `send_telegram`. Главный класс риска — self-hosting-устойчивость (R-1) — закрыт +graceful-degradation контрактом ADR Р-4. Внешний незакрытый вход — значение `ORCH_PLANE_WEB_URL` +(R-2/R-3), проверяется в staging до прод-деплоя.