diff --git a/docs/work-items/ORCH-017/01-brd.md b/docs/work-items/ORCH-017/01-brd.md new file mode 100644 index 0000000..f4e1f2d --- /dev/null +++ b/docs/work-items/ORCH-017/01-brd.md @@ -0,0 +1,91 @@ +# 01-BRD — ORCH-017: Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве + +Work Item: **ORCH-017** +Repo: `orchestrator` · Branch: `feature/ORCH-017-brd-plane-telegram` +Тип: косметическая правка (UX уведомлений). Парная с ORCH-016. + +## 1. Бизнес-контекст и проблема +Когда оркестратор завершает стадию `analysis` и просит подтвердить BRD, в Telegram уходит +отдельное «пингующее» уведомление (`notify_approve_requested` в `src/notifications.py`). +Сейчас в этом сообщении **нет ссылок**: владелец (Слава) вынужден вручную зайти в Plane, +найти нужную issue, открыть комментарий аналитика, оттуда перейти к BRD-документу. Это +лишние ручные шаги на каждой задаче. + +Текущий текст уведомления: +> 📋 {WI}: BRD/ТЗ/AC готовы. Переведите задачу в статус Approved в Plane для продолжения. + +## 2. Цель +В **этом же** уведомлении дать две прямые кликабельные ссылки, чтобы весь сценарий +прохождения апрува выполнялся из Telegram, без ручной навигации в Plane: +1. **Ссылка на BRD** — открывает `01-brd.md` в Gitea (прочитать документ). +2. **Ссылка на Plane-issue** — открывает задачу в Plane (перевести в Approved / отклонить с комментом). + +## 3. Целевой сценарий (Слава) +Получил уведомление → кликнул «📄 BRD» → прочитал → кликнул «✅ Задача» → перевёл в +Approved (или отклонил с комментарием). Всё из Telegram. + +## 4. Объём (Scope) +### В объёме (выбранный по умолчанию минимальный вариант — см. §8 открытые вопросы) +- Доработка **только** функции `notify_approve_requested(task_id)` в `src/notifications.py` + (стадия `analysis`, запрос статуса Approved). +- Формирование двух ссылок и встраивание их в текст того же отдельного уведомления. +- Формат — HTML-ссылки в тексте (`label`), т.к. `send_telegram` уже шлёт + `parse_mode="HTML"`. Альтернатива (inline-кнопки) — открытый вопрос §8. +- Новая конфиг-настройка для внешнего web-URL Plane (см. §6, риск №1). +- Обновление документации (`CLAUDE.md` env-карта при необходимости, `CHANGELOG.md`, + `.env.example`) в том же PR. + +### Вне объёма (НЕ трогать) +- Логика апрува: `:approved:`-handler, `check_analysis_approved`, переходы стадий. +- Живой Telegram-трекер (`update_task_tracker` / `render_task_tracker`, PR #21/#22) — его + текст и поведение не меняем; новое уведомление остаётся ОТДЕЛЬНЫМ сообщением, дубли + трекера не создаём. +- Содержимое комментариев в Plane (это смежная задача ORCH-016). +- Ссылки в других уведомлениях (deploy-failed, agent-failed, error) — вне объёма по + умолчанию (см. открытый вопрос §8.2). + +## 5. Заинтересованные стороны +- **Owner / получатель уведомления:** Слава. +- **Поставщик данных:** оркестратор (БД `tasks`: repo, branch, work_item_id, plane_issue_id). + +## 6. Функциональные требования +| # | Требование | +|---|------------| +| FR-1 | Уведомление об апруве BRD содержит кликабельную ссылку на документ `docs/work-items//01-brd.md` в Gitea. | +| FR-2 | То же уведомление содержит кликабельную ссылку на соответствующую Plane-issue. | +| FR-3 | Существующий текст-призыв («Переведите задачу в статус Approved …») сохраняется. | +| FR-4 | Уведомление остаётся ОДНИМ отдельным пингующим сообщением (без дублей, без второго сообщения). | +| FR-5 | Ссылка на BRD строится на внешнем `gitea_public_url` (фоллбэк `gitea_url`), формат branch-view: `{base}/{owner}/{repo}/src/branch/{branch}/docs/work-items/{WI}/01-brd.md`. Переиспользовать существующий паттерн из `src/usage.py`. | +| FR-6 | Ссылка на Plane-issue строится на внешнем web-URL Plane + workspace + project + issue. | + +## 7. Нефункциональные требования +| # | Требование | +|---|------------| +| NFR-1 | **Никогда не ронять оркестратор** из-за уведомления: построение ссылок обёрнуто в защиту, при отсутствии данных (нет branch / нет plane_issue_id / не задан web-URL) — сообщение всё равно отправляется, просто без соответствующей ссылки (graceful degradation). | +| NFR-2 | Не нарушать self-hosting: правка не требует рестарта прод-контейнера сверх обычного деплоя; не меняет реестр гейтов/стадий. | +| NFR-3 | Сохранить `parse_mode="HTML"`; экранировать динамические подписи (`html.escape`), URL формировать из доверенных конфиг-значений. | + +## 8. Открытые вопросы (требуют решения Owner; в документах принят безопасный дефолт) +1. **Формат ссылок.** Дефолт BRD: HTML-ссылки в тексте (минимальная правка). Альтернатива — + inline-кнопки «📄 Открыть BRD» / «✅ К задаче в Plane», что требует доработки `send_telegram` + (параметр `reply_markup`/`inline_keyboard`). → решение к стадии architecture. +2. **Охват.** Дефолт: только BRD-апрув (`notify_approve_requested`). Альтернатива — все точки, + требующие решения Славы (напр. согласование макета ORCH-14). → если «все точки», объём + расширяется, нужен отдельный перечень событий. +3. **Внешний web-URL Plane.** В конфиге сейчас только внутренний `plane_api_url` + (`http://localhost:8091`) — он НЕ годится для браузерной ссылки. Дефолт: завести новую + env-настройку `ORCH_PLANE_WEB_URL` (внешний адрес Plane) с фоллбэком на `plane_api_url`. + Точное значение URL должен подтвердить Owner/INFRA. +4. **Формат Plane-ссылки.** `…/{workspace}/projects/{project_id}/issues/{issue_id}/` (надёжно, + issue_id есть в `tasks.plane_issue_id`) vs короткий `…/{workspace}/browse//` + (зависит от соответствия `work_item_id` ↔ Plane identifier, что не гарантировано из-за + zero-padding ORCH-017 vs ORCH-17). → решение к стадии architecture. + +## 9. Зависимости и связки +- **PR #14** — `gitea_public_url`: переиспользуем для кликабельных ссылок на доки. +- **PR #21/#22** — живой Telegram-трекер: новое сообщение остаётся отдельным, трекер не трогаем. +- **ORCH-016** — единые коммент-артефакты в Plane (парная задача про навигацию к документам). + +## 10. Критерий бизнес-успеха +Слава из одного Telegram-уведомления одним кликом открывает BRD и одним кликом — задачу в +Plane, не заходя в Plane вручную и не ища комментарий. diff --git a/docs/work-items/ORCH-017/02-trz.md b/docs/work-items/ORCH-017/02-trz.md new file mode 100644 index 0000000..eac7f8d --- /dev/null +++ b/docs/work-items/ORCH-017/02-trz.md @@ -0,0 +1,87 @@ +# 02-ТЗ — ORCH-017: Прямые ссылки в Telegram-уведомлении об апруве BRD + +Work Item: **ORCH-017** · Repo: `orchestrator` +Опирается на 01-brd.md. Уточняет конкретные изменения кода/конфигурации. + +> Примечание по канону: ТЗ фиксирует ТРЕБОВАНИЯ к изменениям, а не готовое +> архитектурное решение. Выбор формата (текст vs inline-кнопки) и точного формата +> Plane-URL — за стадией architecture (см. открытые вопросы 01-brd.md §8). Если по +> ходу разработки ТЗ окажется неполным/неверным — возврат на стадию Анализ, без +> правок ТЗ задним числом. + +## 1. Задействованные модули `src/` +| Модуль | Роль в задаче | +|--------|---------------| +| `src/notifications.py` | **Основной.** Функция `notify_approve_requested(task_id)` (≈ строки 547–566) — единственная точка отправки пингующего уведомления об апруве BRD. Сюда добавляются ссылки. | +| `src/config.py` | Класс `Settings`. Добавить настройку внешнего web-URL Plane (`plane_web_url`, env `ORCH_PLANE_WEB_URL`) с дефолтом-фоллбэком. | +| `src/projects.py` | (Чтение) `get_project_by_repo(repo)` → `plane_project_id` для построения Plane-URL. | +| `src/usage.py` | (Референс, не править) Эталонный паттерн branch-view ссылки на доки (`{base}/{owner}/{repo}/src/branch/{branch}/`), строки ≈483–503 — переиспользовать тот же формат. | +| `src/db.py` | (Чтение) Таблица `tasks`: поля `work_item_id`, `repo`, `branch`, `plane_issue_id`. Источник данных для ссылок. | + +## 2. Источники данных (из `tasks` по `task_id`) +- `work_item_id` — путь к BRD-документу и (опц.) идентификатор issue. +- `repo`, `branch` — построение Gitea branch-view URL. +- `plane_issue_id` — uuid issue в Plane для прямой ссылки. +- `project_id` — через `projects.get_project_by_repo(repo).plane_project_id`. + +`notify_approve_requested` сейчас принимает только `task_id` и тянет лишь `work_item_id` +через `_get_work_item_id`. Требуется дополнительно прочитать `repo`, `branch`, +`plane_issue_id` из `tasks` (один SELECT, в защищённом try/except). + +## 3. Требуемые изменения + +### 3.1 `src/notifications.py` +- Построить **BRD-ссылку** (FR-1/FR-5): + `{base}/{owner}/{repo}/src/branch/{branch}/docs/work-items/{work_item_id}/01-brd.md`, + где `base = (settings.gitea_public_url or settings.gitea_url).rstrip('/')`, + `owner = settings.gitea_owner`. Если нет `base`/`repo`/`branch`/`work_item_id` — ссылку + опустить (NFR-1). +- Построить **Plane-ссылку** (FR-2/FR-6): + `{plane_web_base}/{workspace_slug}/projects/{project_id}/issues/{plane_issue_id}/` + (точный формат — решение architecture, см. 01-brd §8.4). Если нет данных — опустить. +- Встроить обе ссылки в текст того же сообщения (FR-3/FR-4), формат HTML-`` по умолчанию. + Сохранить существующий призыв «Переведите задачу в статус Approved …». +- Сохранить вызов как **одно** `send_telegram(msg)` (пингующее, не silent). Порядок + существующих действий не менять: старт BRD-часов (`mark_brd_review_started`) → + `update_task_tracker(task_id)` → `send_telegram(msg)`. +- Динамические подписи экранировать `html.escape` (NFR-3). + +### 3.2 `src/config.py` +- Добавить в `Settings` поле `plane_web_url: str = ""` (env `ORCH_PLANE_WEB_URL`). +- Семантика фоллбэка: `plane_web_base = (settings.plane_web_url or settings.plane_api_url).rstrip('/')`. + +### 3.3 Опционально (если выбран вариант inline-кнопок — открытый вопрос 01-brd §8.1) +- Расширить `send_telegram(text, disable_notification=False, reply_markup=None)`: + при наличии `reply_markup` прокидывать его в payload `sendMessage`. Обратная + совместимость — обязательна (текущие вызовы без аргумента работают как раньше). +- ⚠️ Это РАСШИРЯЕТ объём; включается только по явному решению Owner на стадии architecture. + +## 4. Изменения API +Нет. Публичные HTTP-эндпоинты (`/webhook/*`, `/status`, `/queue`, `/health`) не затрагиваются. + +## 5. Изменения схемы БД +Нет. Все нужные поля (`repo`, `branch`, `work_item_id`, `plane_issue_id`) уже существуют в `tasks`. + +## 6. Изменения конфигурации / окружения +- Новая env-переменная `ORCH_PLANE_WEB_URL` (внешний web-адрес Plane). Прописать в + `.env.example` (канон секретов/настроек), описать в env-карте (`CLAUDE.md` / + `docs/operations/INFRA.md`). Реальное значение задаётся в `.env`/`.env.staging` на хосте. +- Существующие `ORCH_GITEA_PUBLIC_URL`, `ORCH_GITEA_OWNER`, `ORCH_PLANE_WORKSPACE_SLUG` + переиспользуются как есть. + +## 7. Требования к новым QG checks +Нет. Реестр `QG_CHECKS`, стадии и машинные вердикты не меняются (правка — отображение, +не управление конвейером). + +## 8. Артефакты pipeline, которые должны быть обновлены в ЭТОМ PR +- `CHANGELOG.md` — запись о фиче. +- `.env.example` — новая `ORCH_PLANE_WEB_URL`. +- При добавлении настройки — env-карта в `CLAUDE.md` / `docs/operations/INFRA.md`. +- ADR (стадия architecture): `docs/work-items/ORCH-017/06-adr/ADR-001-*.md` — фиксирует выбор + формата (текст vs кнопки) и формат Plane-URL. + +## 9. Ограничения +- Не трогать `:approved:`-handler и `check_analysis_approved` (только текст/формат уведомления). +- Не плодить сообщения: одно отдельное пингующее сообщение; живой трекер (PR #21/#22) не дублировать. +- Соблюдать self-hosting: не ронять/не рестартить прод сверх штатного деплоя; обязательная + страховка `deploy-staging` (8501) перед прод-деплоем орка. diff --git a/docs/work-items/ORCH-017/03-acceptance-criteria.md b/docs/work-items/ORCH-017/03-acceptance-criteria.md new file mode 100644 index 0000000..d26d3f2 --- /dev/null +++ b/docs/work-items/ORCH-017/03-acceptance-criteria.md @@ -0,0 +1,64 @@ +# 03-Acceptance Criteria — ORCH-017 + +Work Item: **ORCH-017** · Repo: `orchestrator` +Каждый критерий формулирует условие PASS/FAIL. Источник — 01-brd.md / 02-trz.md. + +## AC-1 — Ссылка на BRD присутствует в уведомлении +- **PASS:** Текст, сформированный `notify_approve_requested`, содержит кликабельную ссылку + на `docs/work-items//01-brd.md` вида + `{gitea_public_url|gitea_url}/{owner}/{repo}/src/branch/{branch}/docs/work-items/{WI}/01-brd.md`. +- **FAIL:** Ссылки на BRD нет, либо она ведёт не на `01-brd.md`/не на нужный WI. + +## AC-2 — Ссылка на Plane-issue присутствует в уведомлении +- **PASS:** Тот же текст содержит кликабельную ссылку на issue в Plane, построенную на + внешнем web-URL Plane + workspace + project + `plane_issue_id` (или согласованный браузер-формат). +- **FAIL:** Ссылки на issue нет, либо она указывает на внутренний `localhost`/неверную issue. + +## AC-3 — Базовый URL берётся из внешних настроек +- **PASS:** BRD-ссылка использует `gitea_public_url`, при его пустоте — `gitea_url`; Plane-ссылка + использует `plane_web_url` (env `ORCH_PLANE_WEB_URL`), при пустоте — `plane_api_url`. +- **FAIL:** Захардкожен хост, либо ссылка нерабочая снаружи деплой-хоста. + +## AC-4 — Существующий призыв сохранён +- **PASS:** Текст по-прежнему содержит призыв перевести задачу в статус Approved (смысл строки + «Переведите задачу в статус Approved … для продолжения» сохранён). +- **FAIL:** Призыв удалён/искажён. + +## AC-5 — Одно отдельное пингующее сообщение, без дублей +- **PASS:** `notify_approve_requested` отправляет ровно одно сообщение через `send_telegram` + (пингующее, не silent). Живой трекер (`update_task_tracker`) обновляется как раньше и не + дублируется новым сообщением. +- **FAIL:** Появляется второе/дубль-сообщение, либо трекер шлётся повторно как новое сообщение. + +## AC-6 — Graceful degradation (никогда не ронять оркестратор) +- **PASS:** При отсутствии `branch` / `plane_issue_id` / незаданном Plane web-URL функция НЕ + бросает исключение: уведомление уходит с доступными ссылками (или без отсутствующей), орк жив. +- **FAIL:** Отсутствие данных приводит к исключению/падению потока уведомлений. + +## AC-7 — HTML-безопасность +- **PASS:** Сохранён `parse_mode="HTML"`; динамические подписи экранируются (`html.escape`), + URL валиден и не ломает разметку сообщения. +- **FAIL:** Сообщение приходит с битой HTML-разметкой или с неэкранированным пользовательским текстом. + +## AC-8 — Логика апрува не затронута +- **PASS:** `:approved:`-handler, `check_analysis_approved`, переходы стадий и реестр `QG_CHECKS` + без изменений; правка касается только текста/формата уведомления. +- **FAIL:** Изменена логика гейта/перехода стадий. + +## AC-9 — Документация обновлена в том же PR +- **PASS:** Обновлены `CHANGELOG.md` и `.env.example` (новая `ORCH_PLANE_WEB_URL`); если добавлена + настройка — отражено в env-карте (`CLAUDE.md`/`docs/operations/INFRA.md`); заведён ADR на + выбранный формат. (Reviewer проверяет доку → нет обновления = REQUEST_CHANGES.) +- **FAIL:** Код изменён, документация — нет. + +## AC-10 — Тесты зелёные +- **PASS:** Новые/затронутые тесты (`tests/test_notify_approve_links.py` и существующие + `tests/test_telegram_tracker.py`, `tests/test_notify_done_regression.py`) проходят; `pytest tests/ -q` зелёный. +- **FAIL:** Любой связанный тест падает. + +--- +### Зависит от решений Owner (open questions 01-brd §8) +- Если выбран вариант **inline-кнопок** — AC-1/AC-2 считаются выполненными при наличии кнопок + «📄 Открыть BRD» / «✅ К задаче в Plane» с теми же URL; дополнительно AC: обратная совместимость + `send_telegram` (старые вызовы без `reply_markup` работают). +- Если охват расширен до **всех точек решения** — AC-1/AC-2 проверяются для каждой такой точки. diff --git a/docs/work-items/ORCH-017/04-test-plan.yaml b/docs/work-items/ORCH-017/04-test-plan.yaml new file mode 100644 index 0000000..ab66a42 --- /dev/null +++ b/docs/work-items/ORCH-017/04-test-plan.yaml @@ -0,0 +1,99 @@ +work_item: ORCH-017 +title: "Прямые ссылки на BRD и Plane-таску в Telegram-уведомлении об апруве" +notes: > + Тесты изолируют сеть: send_telegram/httpx мокируются, проверяется СФОРМИРОВАННЫЙ текст + (и/или reply_markup, если выбран вариант кнопок), а не реальная отправка. БД tasks + наполняется фикстурой (work_item_id, repo, branch, plane_issue_id). Маппинг на критерии — в поле acceptance. + +tests: + - id: TC-01 + type: unit + description: "notify_approve_requested формирует текст с кликабельной ссылкой на 01-brd.md (Gitea branch-view)" + module: tests/test_notify_approve_links.py + setup: "task в tasks с work_item_id=ORCH-017, repo=orchestrator, branch=feature/ORCH-017-..., gitea_public_url задан; send_telegram замокан" + expected: PASS + acceptance: [AC-1, AC-3] + + - id: TC-02 + type: unit + description: "Текст содержит ссылку на Plane-issue с внешним web-URL + workspace + project + plane_issue_id" + module: tests/test_notify_approve_links.py + setup: "plane_web_url(ORCH_PLANE_WEB_URL) и workspace заданы; project резолвится по repo; plane_issue_id в tasks" + expected: PASS + acceptance: [AC-2, AC-3] + + - id: TC-03 + type: unit + description: "При пустом gitea_public_url BRD-ссылка строится на gitea_url (фоллбэк); при пустом plane_web_url — на plane_api_url" + module: tests/test_notify_approve_links.py + expected: PASS + acceptance: [AC-3] + + - id: TC-04 + type: unit + description: "Сохранён призыв перевести задачу в статус Approved (подстрока 'Approved' присутствует)" + module: tests/test_notify_approve_links.py + expected: PASS + acceptance: [AC-4] + + - id: TC-05 + type: unit + description: "send_telegram вызван ровно один раз (пингующее сообщение), без disable_notification=True" + module: tests/test_notify_approve_links.py + setup: "mock send_telegram, assert call_count == 1 и аргумент disable_notification не True" + expected: PASS + acceptance: [AC-5] + + - id: TC-06 + type: unit + description: "Graceful: branch=None / plane_issue_id=None — функция не бросает исключение, сообщение всё равно отправляется" + module: tests/test_notify_approve_links.py + setup: "task без branch и без plane_issue_id; убедиться что send_telegram всё равно вызван, отсутствующая ссылка опущена" + expected: PASS + acceptance: [AC-6] + + - id: TC-07 + type: unit + description: "Plane web-URL не задан и plane_api_url пуст — Plane-ссылка опускается, BRD-ссылка остаётся, орк не падает" + module: tests/test_notify_approve_links.py + expected: PASS + acceptance: [AC-6] + + - id: TC-08 + type: unit + description: "Сохранён parse_mode=HTML; динамические подписи экранированы, HTML-разметка ссылок валидна" + module: tests/test_notify_approve_links.py + expected: PASS + acceptance: [AC-7] + + - id: TC-09 + type: unit + description: "Регрессия трекера: update_task_tracker по-прежнему работает (silent edit), новое сообщение его не дублирует" + module: tests/test_telegram_tracker.py + expected: PASS + acceptance: [AC-5, AC-8] + + - id: TC-10 + type: integration + description: "Поток analysis-approved: _handle_analysis_approved_flow при готовых артефактах вызывает notify_approve_requested; БД tasks даёт корректные repo/branch/plane_issue_id для ссылок" + module: tests/test_analysis_approve_flow_links.py + setup: "замокать сетевые вызовы Plane/Gitea/Telegram; убедиться, что check_analysis_approved/переходы стадий не изменены" + expected: PASS + acceptance: [AC-1, AC-2, AC-8] + + # Условные тесты — включаются ТОЛЬКО если Owner выбрал вариант inline-кнопок (01-brd §8.1) + - id: TC-11 + type: unit + description: "(Условный) Вариант кнопок: payload содержит reply_markup.inline_keyboard с кнопками '📄 Открыть BRD' и '✅ К задаче в Plane' с верными url" + module: tests/test_notify_approve_links.py + expected: PASS + condition: "only if inline-buttons variant chosen" + acceptance: [AC-1, AC-2] + + - id: TC-12 + type: unit + description: "(Условный) Обратная совместимость send_telegram: вызовы без reply_markup работают как раньше (payload без поля reply_markup)" + module: tests/test_telegram_tracker.py + expected: PASS + condition: "only if inline-buttons variant chosen" + acceptance: [AC-5]