86 lines
6.3 KiB
Markdown
86 lines
6.3 KiB
Markdown
---
|
||
work_item: ORCH-020
|
||
stage: architecture
|
||
author_agent: architect
|
||
status: proposed
|
||
created_at: 2026-06-17
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 08 — Требования к данным: ORCH-020 — Оценка задачи, запускаемая статусом «Оценка»
|
||
|
||
Work Item: **ORCH-020** · Repo: **orchestrator** · Стадия: architecture
|
||
|
||
> When-applicable / информационный (гейтом не парсится). Одна **новая аддитивная** таблица; существующие
|
||
> таблицы (`tasks`/`agent_runs`/`jobs`/…) — **не изменяются** (NFR-8).
|
||
|
||
## Изменения схемы БД
|
||
|
||
**Новая аддитивная таблица `task_estimates`** (`CREATE TABLE IF NOT EXISTS` в `db.init_db()`, паттерн
|
||
`coverage_baseline`/`lessons`/`transition_lease`; идемпотентно, restart-safe на общей прод-БД):
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS task_estimates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
work_item_id TEXT NOT NULL UNIQUE, -- ключ/UPSERT-цель (issue может не иметь task на момент оценки)
|
||
task_id INTEGER, -- FK tasks.id; НУЛЛАБЕЛЕН до старта пайплайна
|
||
repo TEXT,
|
||
-- Прогноз (на момент перевода в «Оценка»):
|
||
forecast_tokens INTEGER,
|
||
forecast_seconds INTEGER,
|
||
forecast_cost_usd REAL,
|
||
forecast_story_points INTEGER, -- из {1,2,3,5,8}
|
||
-- Факт (на момент перехода задачи в `done`):
|
||
actual_tokens INTEGER,
|
||
actual_seconds INTEGER,
|
||
actual_cost_usd REAL,
|
||
actual_story_points INTEGER, -- из {1,2,3,5,8}
|
||
-- Метаданные:
|
||
source TEXT, -- 'status' | 'manual' | 'api'
|
||
estimate_count INTEGER NOT NULL DEFAULT 1, -- число пере-оценок (инкремент при UPSERT)
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_task_estimates_repo ON task_estimates (repo);
|
||
CREATE INDEX IF NOT EXISTS idx_task_estimates_task_id ON task_estimates (task_id);
|
||
```
|
||
|
||
- **`UNIQUE(work_item_id)`** — несущий инвариант идемпотентной пере-оценки (BR-T4/AC-T4): повторный перевод
|
||
в «Оценка» делает **UPSERT** (`INSERT … ON CONFLICT(work_item_id) DO UPDATE …`), обновляя одну строку и
|
||
инкрементируя `estimate_count`; дублей строк нет.
|
||
- **Дельта** прогноз↔факт **не хранится отдельной колонкой** — вычисляется на чтение из forecast/actual
|
||
(избегаем рассинхрона; калибровке достаточно обеих величин). При желании реализатор может добавить
|
||
материализованные `delta_*` — не обязательно (BR-10 требует «обе величины + дельту»; вычисляемая дельта
|
||
это удовлетворяет).
|
||
- **Индексы:** по `repo` (выборка/снапшот по проекту) и `task_id` (связь с задачей). По `work_item_id`
|
||
индекс создаётся автоматически (UNIQUE).
|
||
|
||
## Новые/изменённые сущности
|
||
|
||
- **Хелперы `db.py`** (каждый открывает/закрывает свою connection, паттерн `coverage_baseline`/`lessons`;
|
||
leaf `estimator`/вызывающие оборачивают в never-raise):
|
||
- `record_estimate(work_item_id, repo, task_id=None, forecast_*=…, source='status') -> int` — UPSERT
|
||
прогноза по `work_item_id`; инкремент `estimate_count`, стамп `updated_at`.
|
||
- `set_actual(work_item_id, actual_tokens, actual_seconds, actual_cost_usd, actual_story_points,
|
||
task_id=None) -> bool` — запись факта; **не трогает** forecast-поля.
|
||
- `get_estimate(work_item_id) -> dict | None` — текущая строка прогноз/факт.
|
||
- `estimates_snapshot(limit=…) -> dict` — read-only для блока `estimator` в `GET /queue`.
|
||
- **Read-only агрегат истории** `db.completed_task_stats(repo, track) -> {n, mean_tokens, mean_cost_usd,
|
||
mean_seconds}` — поверх `agent_runs` (токены/стоимость, как `task_usage_summary`) и `tasks`
|
||
(`stage='done'`, время = `updated_at − created_at` с отсечкой `estimator_wall_cap_s`). **Только чтение**
|
||
существующих таблиц; новых колонок не вводит.
|
||
|
||
## Совместимость данных / миграции
|
||
|
||
- **Аддитивность (NFR-8):** только новая таблица + новые read/write-хелперы; **ни одной** правки
|
||
существующих таблиц/колонок/индексов. `STAGE_TRANSITIONS`/`QG_CHECKS`/machine-verdict-ключи независимы
|
||
от данных оценки.
|
||
- **Идемпотентность миграции:** `CREATE TABLE IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS` — no-op на уже
|
||
созданной таблице; безопасно на живой общей прод-БД (enduro не затронут — таблица общая, но писать в неё
|
||
будет только self-hosting-скоуп; строки enduro не появляются, пока репо вне `estimator_repos`).
|
||
- **Restart-safe:** строки `task_estimates` переживают рестарт; прогноз, сделанный на бэклоге (с
|
||
`task_id=NULL`), сохраняется до старта пайплайна и связывается с `task_id` позже (best-effort).
|
||
- **Влияние на общую прод-БД:** таблица малая (одна строка на оценённый issue), индексы лёгкие; нагрузка на
|
||
hot-path **нулевая** (claim/queue не читают `task_estimates`). Откат (`ORCH_ESTIMATOR_ENABLED=false`)
|
||
оставляет таблицу пустой/неиспользуемой — безвредно.
|