167 lines
17 KiB
Markdown
167 lines
17 KiB
Markdown
---
|
||
work_item: ORCH-020
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-17
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-020 — Оценка задачи, запускаемая Plane-статусом «Оценка»
|
||
|
||
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||
> Архитектурное обоснование/решения (выбор механизма оценки эвристика vs LLM vs гибрид, точные
|
||
> сигнатуры врезок, индексы, формулы маппинга, сглаживание массовой нагрузки, Plane-группа статуса
|
||
> «Оценка») — задача архитектора (`06-adr`).
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Вводится **новый операторский Plane-статус «Оценка»** — триггер механизма оценки (по образцу
|
||
action-статусов **STOP**/ORCH-090 и **Confirm Deploy**/ORCH-059). Перевод issue в «Оценка»
|
||
(в т.ч. **массово** через Plane multi-select) запускает **новый leaf-модуль оценки**
|
||
(`src/estimator.py`, never-raise), который прогнозирует **стоимость / время / токены / сложность
|
||
(story points `{1,2,3,5,8}`)** на основе истории завершённых задач (агрегаты `src/usage.py`).
|
||
Прогноз: (a) пишется в Plane-поле `estimate_point`, (b) публикуется Plane-комментом, (c) добавляется
|
||
пунктом «Оценка» (время/токены/стоимость) в общую Telegram-карточку, (d) сохраняется в **новой
|
||
аддитивной таблице** `task_estimates` (леджер прогноз↔факт, ключ `work_item_id`). По завершении
|
||
оценки оркестратор **возвращает issue в статус `Backlog`**. По завершении самой задачи (переход в
|
||
`done`) **факт** пишется в Plane-поле `point`. Пере-оценка — повтор перевода в «Оценка»
|
||
(идемпотентно).
|
||
|
||
**Инвариант (NFR-1/NFR-3):** оценка — наблюдатель/продюсер, **не** Quality Gate и **не** переход
|
||
стадии. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схемы существующих
|
||
таблиц — байт-в-байт; горячий путь `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — не
|
||
трогается. Статус «Оценка» не добавляет ребра в машину стадий.
|
||
|
||
## 2. Задействованные модули / пути
|
||
| Путь | Действие |
|
||
|------|----------|
|
||
| `src/plane_sync.py` | **изменить** — (1) `_PLANE_NAME_TO_KEY += {"Оценка": "estimate"}`; ключ `estimate` **НЕ добавлять** в `_DEFAULT_STATES` (fail-closed, как `stop`/`confirm_deploy`); (2) новые write-хелперы `set_issue_estimate_point(work_item, value)`, `set_issue_point(work_item, value)`, `set_issue_backlog(work_item)` (все через guard `_guard_allows_write`, ORCH-117); (3) read-хелпер текущих полей `estimate_point`/`point`. fail-safe при отсутствии estimate-конфига |
|
||
| `src/webhooks/plane.py` | **изменить** — в `handle_issue_updated` добавить **fail-closed ветку** `estimate_state = proj_states.get("estimate")` → `handle_estimate(data, project_id)` (распознаётся как **отдельный** жест, не алиасит stop/to_analyse/confirm_deploy/approved/rejected). Новый `handle_estimate`: резолв issue (pipeline-задачи может не быть), guard `estimator.applies(repo)`, guard «нет активного job» (BR-T6), запуск оценки, затем `set_issue_backlog` |
|
||
| `src/estimator.py` | **создать** — leaf: `estimate(work_item_id, issue|description, repo)` → прогноз `{tokens,seconds,cost_usd,story_points}`; маппинг величин → story-point bucket `{1,2,3,5,8}` (чистая функция); расчёт факта из `usage.py`; `applies(repo)`, `should_estimate(task|None)` (анти-disruption), `snapshot()`; never-raise |
|
||
| `src/db.py` | **изменить** — аддитивная таблица `task_estimates` (`CREATE TABLE IF NOT EXISTS` в `init_db()`) + хелперы `record_estimate`/`set_actual`/`get_estimate`/`estimates_snapshot`; существующие таблицы/колонки не трогать |
|
||
| `src/usage.py` | **переиспользовать** (read-only) — `task_usage_summary`/`agent_cost_totals`/тайминги для **факта**; при необходимости тонкий read-only агрегат «история похожих задач» |
|
||
| `src/notifications.py` | **изменить** — пункт «Оценка» (время · токены · стоимость) в рендере общей карточки; never-raise, пустой прогноз → пункт опускается |
|
||
| `src/main.py` | **изменить** — (опц.) `POST /estimate?work_item=<id>` / `POST /estimate/backlog` как программное удобство; **read-only блок `estimator` в `GET /queue`** |
|
||
| `src/config.py` | **изменить** — флаги (см. §7) |
|
||
| `tests/test_orch020_estimator.py` | **создать** — покрытие (см. `04-test-plan.yaml`) |
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-T1 — Статус «Оценка» как триггер (BR-T1, BR-T5)
|
||
`_PLANE_NAME_TO_KEY["Оценка"] = "estimate"`; ключ `estimate` **отсутствует** в `_DEFAULT_STATES`.
|
||
В `handle_issue_updated` — отдельная ветка: `estimate_state = proj_states.get("estimate")`;
|
||
`if estimate_state and new_state == estimate_state: await handle_estimate(...)`. Доска без статуса →
|
||
`estimate_state is None` → ветка инертна (fail-closed, зеркало `stop`/`confirm_deploy`). Ветка не
|
||
должна аннулировать/перехватывать STOP/`to_analyse`/`confirm_deploy`/approved/rejected (UUID
|
||
«Оценка» отличен от всех; порядок ветки выбирает архитектор, инвариант — взаимоисключение жестов).
|
||
|
||
### FR-T2 — Обработчик `handle_estimate` (BR-T1, BR-T6)
|
||
`handle_estimate(data, project_id)`: резолвит `plane_id`/`work_item_id`; `repo` определяется по
|
||
проекту. Guard-цепочка (все — no-op-with-log при невыполнении, never-raise):
|
||
1. `estimator.applies(repo)` — kill-switch + скоуп (False → no-op);
|
||
2. **анти-disruption (BR-T6):** если у issue есть pipeline-задача с **активным** job
|
||
(`has_active_job_for_task`) → no-op + лог (не выдёргивать in-flight работу). Issue без задачи
|
||
(бэклог) или с терминальной/idle-задачей → оценка допустима.
|
||
Далее: `estimator.estimate(...)` → запись прогноза (FR-T3) → **`set_issue_backlog(work_item)`**
|
||
(BR-T2). Контракт never-raise: любая ошибка логируется, вебхук-флоу не падает.
|
||
|
||
### FR-T3 — Прогноз задачи (BR-1, BR-2, BR-3)
|
||
`estimator.estimate(work_item_id, description|issue, repo)` возвращает `{forecast_tokens,
|
||
forecast_seconds, forecast_cost_usd, story_points}`, `story_points ∈ {1,2,3,5,8}`. База — история
|
||
похожих **завершённых** задач (средние токены/время/стоимость из `usage.py`-агрегатов); пустая
|
||
история → bootstrap-дефолт. Маппинг величин → bucket — чистая функция (пороги — `06-adr`).
|
||
never-raise: сбой → безопасный дефолт + warning.
|
||
|
||
### FR-T4 — Семантика story points (BR-3)
|
||
Шкала фиксированная: `1` docs/label/config · `2` небольшой фикс · `3` средняя · `5` сложная
|
||
(код+тесты) · `8` эпик/разбивать. Значения вне набора не выдаются.
|
||
|
||
### FR-T5 — Авто-возврат в Backlog + анти-loop (BR-T2, BR-T6)
|
||
После оценки `handle_estimate` зовёт `set_issue_backlog(work_item)` → issue возвращается в `Backlog`.
|
||
Это **не** создаёт цикла: `Backlog`-UUID не совпадает ни с одной триггер-веткой `handle_issue_updated`
|
||
(`stop`/`to_analyse`/`confirm_deploy`/`approved`/`rejected`/`estimate`) → входящий webhook «state →
|
||
Backlog» = no-op-эхо. Возврат best-effort: сбой записи статуса не роняет флоу (прогноз уже записан).
|
||
|
||
### FR-T6 — Массовость и пере-оценка (BR-T3, BR-T4)
|
||
Массовый перевод N задач в «Оценка» = N независимых `issue.updated`-вебхуков → N вызовов
|
||
`handle_estimate` (никакого спец-batch-кода). Пере-оценка = повторный перевод: `estimate`
|
||
идемпотентно **перезаписывает** прогноз в `task_estimates` (UPSERT по `work_item_id`) и
|
||
`estimate_point`; дублей строк нет.
|
||
|
||
### FR-T7 — Запись прогноза и факта в Plane (BR-7, BR-8, NFR-6, NFR-7)
|
||
- Прогноз story points → `set_issue_estimate_point` → поле issue `estimate_point`.
|
||
- По завершении задачи (переход в `done`, врезка в существующий done-путь): из `usage.py` считается
|
||
факт (токены/время/стоимость) → маппится в story-point bucket → `set_issue_point` → поле `point`;
|
||
`estimate_point` не перезаписывается.
|
||
- Все записи через `plane_sync` под guard ORCH-117; отсутствие estimate-конфига/поля → best-effort
|
||
пропуск + лог (не падать).
|
||
|
||
### FR-T8 — Отображение (BR-9)
|
||
- **Plane-коммент** с прогнозом (стоимость/время/токены/story points) — `plane_sync.add_comment`.
|
||
- **Telegram-карточка** — пункт **«Оценка»**: время · токены · стоимость (`notifications`).
|
||
Обе поверхности — best-effort, не блокируют конвейер.
|
||
|
||
### FR-T9 — Леджер прогноз↔факт (BR-10)
|
||
`task_estimates` хранит прогноз (на момент оценки) и факт (на момент `done`) + дельту, ключ
|
||
`work_item_id` (т.к. на момент оценки `task_id` может быть `NULL` — issue на бэклоге). Фундамент
|
||
калибровки (ORCH-8); авто-уточнение модели в объём не входит.
|
||
|
||
### FR-T10 — leaf-инварианты (NFR-2, NFR-3)
|
||
`applies(repo)` = `estimator_enabled` ∧ скоуп `estimator_repos` (пусто → self-hosting only),
|
||
проверяется локально и ПЕРВЫМ (без сети). Выключено → весь модуль инертен (нулевая регрессия:
|
||
статус «Оценка» не обрабатывается, ничего не пишется). read-only блок `estimator` в `GET /queue`
|
||
(флаг/скоуп/счётчики прогнозов/записей/возвратов-в-Backlog).
|
||
|
||
## 4. Изменения API
|
||
| Метод/путь | Назначение |
|
||
|------------|-----------|
|
||
| **Plane-статус «Оценка»** (не HTTP-эндпоинт) | **Основной триггер**: перевод issue в статус → `handle_estimate`. Массовость — multi-select Plane. |
|
||
| `POST /estimate?work_item=<id>` *(опц.)* | Программно произвести/обновить прогноз одной задачи (то же ядро, что статус-триггер) — удобство/диагностика, не основной путь |
|
||
| `POST /estimate/backlog` *(опц.)* | Программно оценить backlog-задачи проекта — удобство; основной массовый путь — статус «Оценка» |
|
||
| `GET /estimate?work_item=<id>` *(опц.)* | Прочитать текущий прогноз vs факт из `task_estimates` |
|
||
| `GET /queue` | **+ read-only блок `estimator`**; existing-поля не меняются |
|
||
|
||
Существующие эндпоинты/контракты не изменяются. Webhook-контракт `issue.updated` не меняется —
|
||
добавляется лишь распознавание ещё одного целевого статуса.
|
||
|
||
## 5. Изменения схемы БД
|
||
**Новая аддитивная таблица** `task_estimates` (`CREATE TABLE IF NOT EXISTS`, без правки существующих):
|
||
`work_item_id` (ключ/UPSERT-цель) · `task_id` (нуллабелен до старта пайплайна) · `repo` · прогноз
|
||
(`forecast_tokens`, `forecast_seconds`, `forecast_cost_usd`, `forecast_story_points`) · факт
|
||
(`actual_tokens`, `actual_seconds`, `actual_cost_usd`, `actual_story_points`) · дельта (`delta_*`
|
||
или вычисляемая) · `source` (`status`/`manual`/`api`) · `estimate_count` (число пере-оценок,
|
||
опц.) · `created_at` · `updated_at`. Точные типы/индексы/уникальность (UNIQUE по `work_item_id`
|
||
для идемпотентного UPSERT) — `06-adr`. Существующие таблицы (`tasks`/`agent_runs`/`jobs`/…) — **не
|
||
изменяются** (NFR-8).
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
**Нет.** Оценка — наблюдатель/продюсер, не Quality Gate; статус «Оценка» — операторский side-триггер,
|
||
не ребро `STAGE_TRANSITIONS`. `QG_CHECKS` / `check_*` / machine-verdict-ключи / `STAGE_TRANSITIONS` —
|
||
**не трогаются**. Новых номерных артефактов pipeline (`NN-*.md`) и новых вердикт-парсеров нет (оценка
|
||
публикуется в Plane/Telegram/`task_estimates`, не во frontmatter-гейтах).
|
||
|
||
## 7. Совместимость / регресс
|
||
- **Флаги** (`config.py`, дефолты безопасные): `estimator_enabled` (kill-switch, env
|
||
`ORCH_ESTIMATOR_ENABLED`), `estimator_repos` (CSV, env `ORCH_ESTIMATOR_REPOS`; **пусто →
|
||
self-hosting only**). Доп. тюнинг (bootstrap-дефолты, пороги bucket, целевой возврат-статус,
|
||
сглаживание массовой нагрузки) — конфиг-ключи на усмотрение `06-adr`.
|
||
- **Откат** = `ORCH_ESTIMATOR_ENABLED=false` → модуль инертен: статус «Оценка» не обрабатывается
|
||
(`applies`=False до сети), ни записи в Plane, ни строки карточки, ни обращений к таблице; конвейер
|
||
байт-в-байт до ORCH-020. Доп. откат «на уровне доски» — не создавать статус «Оценка» (fail-closed,
|
||
BR-T5).
|
||
- **Область раската:** по умолчанию self-hosting `orchestrator`; `enduro-trails` не затронут (скоуп
|
||
`estimator_repos` пуст + на его доске статуса «Оценка» нет → fail-closed).
|
||
- **never-raise / fail-safe:** все публичные функции и врезки изолированы (`try/except` → warning +
|
||
безопасный дефолт). Сбой оценки/записи в Plane/возврата статуса/рендера карточки — не роняет
|
||
конвейер (NFR-2/6/7).
|
||
- **Анти-disruption / анти-loop:** активный job → no-op (BR-T6); возврат в Backlog — no-op-эхо
|
||
(FR-T5). Машина стадий и in-flight задачи не затрагиваются.
|
||
- **Горячий путь не тронут:** `resolve_agent_model`/`resolve_agent_effort`/`_spawn` — без изменений
|
||
(NFR-3).
|
||
- **Инфра-предусловия (NFR-7):** (a) статус **«Оценка»** на доске проекта (онбординг ORCH-009 →
|
||
23-й статус; группа — `06-adr`/`07-infra-requirements.md`); (b) estimate-система Plane
|
||
(`1/2/3/5/8`) для `estimate_point`; их отсутствие → fail-closed/best-effort пропуск, не падение.
|