164 lines
13 KiB
Markdown
164 lines
13 KiB
Markdown
---
|
||
work_item: ORCH-098
|
||
stage: analysis
|
||
author_agent: analyst
|
||
status: ready-for-review
|
||
created_at: 2026-06-10
|
||
model_used: claude-opus-4-8
|
||
---
|
||
|
||
# 02 — ТЗ (TRZ): ORCH-098 — FND: машинный журнал уроков
|
||
|
||
Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: analysis
|
||
|
||
> ТЗ описывает **конкретные изменения к реализации**, выведенные из BRD и фактического кода.
|
||
> Архитектурное обоснование/решения (точные сигнатуры врезок, индексы, дедуп, ретенция) — задача
|
||
> архитектора (`06-adr`).
|
||
|
||
## 1. Сводка изменения
|
||
|
||
Ввести **машинный журнал уроков** — аддитивную таблицу `lessons` + чистый leaf-модуль
|
||
`src/lessons.py` (never-raise, kill-switch) по образцу `serial_gate.py` / `coverage_gate.py` /
|
||
`metrics.py`. Модуль несёт: helper записи урока (`record`), read-only выборку (`get_lessons`),
|
||
обновление (`update_lesson`), `snapshot()` для `GET /queue`. Автозапись ≥2–3 типов отклонений —
|
||
тонкими best-effort врезками в существующие choke-point `stage_engine.py` / `merge_gate.py` /
|
||
`launcher.py`. Два новых HTTP-эндпоинта (`GET /lessons`, `POST /lessons`) в `main.py`. Схема несёт
|
||
**сразу** нуллабельные колонки атрибуции (требование Славы 10.06). Конвейер (`STAGE_TRANSITIONS` /
|
||
`QG_CHECKS` / `check_*` / machine-verdict) — **не тронут**; enduro — не затронут.
|
||
|
||
## 2. Задействованные модули / пути
|
||
| Путь | Действие |
|
||
|------|----------|
|
||
| `src/db.py` | изменить — `CREATE TABLE IF NOT EXISTS lessons` в `init_db()`; helper'ы `record_lesson` / `get_lessons` / `update_lesson` / `lessons_snapshot` |
|
||
| `src/lessons.py` | **создать** — leaf: `record(...)`, `get(...)`, `update(...)`, `snapshot()`, константы `LessonType`/`Attribution`/`Domain`, `applies()`, never-raise |
|
||
| `src/config.py` | изменить — флаг `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`) + опц. `lessons_query_limit_default` |
|
||
| `src/stage_engine.py` | изменить — best-effort врезка `lessons.record(...)` в `_handle_qg_failure_rollbacks` (откаты gate-fail) и в ветку post-deploy `DEGRADED` → freeze |
|
||
| `src/merge_gate.py` | изменить — best-effort врезка в HOLD/regression-guard HOLD и в транзиент-классификатор (`_classify_merge_response == "transient"` / merge-retry-исчерпан) |
|
||
| `src/agents/launcher.py` | изменить — best-effort врезка при timeout-kill / транзиент-requeue агента |
|
||
| `src/main.py` | изменить — эндпоинты `GET /lessons`, `POST /lessons` (+опц. `POST /lessons/{id}`); блок `lessons` в `GET /queue` |
|
||
| `tests/test_lessons.py` | **создать** — unit + integration (см. `04-test-plan.yaml`) |
|
||
| `CLAUDE.md`, `docs/architecture/README.md`, `CHANGELOG.md` | изменить — документация |
|
||
|
||
## 3. Функциональные требования
|
||
|
||
### FR-1 — Таблица `lessons` (BR-1, BR-2)
|
||
Аддитивная идемпотентная таблица в `db.init_db()` (паттерн `repo_freeze`/`coverage_baseline`):
|
||
|
||
```sql
|
||
CREATE TABLE IF NOT EXISTS lessons (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
||
updated_at TEXT,
|
||
-- тип отклонения (slug-конвенция, не enum-констрейнт)
|
||
lesson_type TEXT NOT NULL,
|
||
-- контекст
|
||
work_item_id TEXT,
|
||
task_id INTEGER,
|
||
stage TEXT,
|
||
agent TEXT,
|
||
repo TEXT,
|
||
-- анализ
|
||
root_cause TEXT,
|
||
suggestion TEXT,
|
||
-- статус
|
||
status TEXT NOT NULL DEFAULT 'new', -- new|in_progress|closed|linked
|
||
related_task TEXT,
|
||
-- АТРИБУЦИЯ (BR-2, Слава 10.06) — нуллабельные, заполняются позже
|
||
attribution TEXT, -- platform|project|both|unknown
|
||
target_repo TEXT, -- кого касается (orchestrator|enduro-trails|…)
|
||
target_domain TEXT, -- reliability|quality|economy|features|scale
|
||
-- учёт
|
||
source TEXT, -- auto|manual
|
||
detail TEXT -- свободный JSON/текст (payload детектора)
|
||
);
|
||
CREATE INDEX IF NOT EXISTS idx_lessons_type_status ON lessons (lesson_type, status);
|
||
CREATE INDEX IF NOT EXISTS idx_lessons_repo ON lessons (repo);
|
||
```
|
||
Колонки атрибуции создаются **сразу** и нуллабельны (NFR-6). На уже созданной таблице новые
|
||
колонки добавляются `_ensure_column` (forward-safe). Никакого `enum`-констрейнта — значения суть
|
||
конвенция строковых слагов (forward-compatible).
|
||
|
||
### FR-2 — Helper записи `lessons.record(...)` (BR-3, BR-5; NFR-1)
|
||
Сигнатура (уточняет архитектор), напр.:
|
||
`record(lesson_type, *, work_item_id=None, task_id=None, stage=None, agent=None, repo=None,
|
||
root_cause=None, suggestion=None, status="new", related_task=None, attribution=None,
|
||
target_repo=None, target_domain=None, source="auto", detail=None) -> int | None`.
|
||
- При `lessons_enabled is False` → немедленный no-op (`None`), без обращения к БД.
|
||
- Оборачивает `db.record_lesson` в `try/except` → при любой ошибке `logger.warning` + `None`
|
||
(**never-raise**, NFR-1). Возвращает `id` вставленной строки при успехе.
|
||
- `source="auto"` для детекторов, `source="manual"` для ручной записи.
|
||
|
||
### FR-3 — Автозапись отклонений (BR-3)
|
||
Минимум 2–3 типа, best-effort (каждая врезка обёрнута/делегирует в never-raise `record`):
|
||
- **FR-3a — gate-fail / rollback** — в `stage_engine._handle_qg_failure_rollbacks`: при откате на
|
||
`development` (reviewer `REQUEST_CHANGES`, tester `check_tests_passed` FAIL, staging FAILED,
|
||
deploy FAILED) → `record("gate_failure", stage=…, agent=…, work_item_id=…, repo=…,
|
||
root_cause=reason)`. Тип откатной причины → в `detail`/`root_cause`.
|
||
- **FR-3b — merge HOLD / regression-guard HOLD** — в `merge_gate` (путь HOLD `_handle_merge_verify`
|
||
/ `main_regressed_alerts_total` инкремент) → `record("merge_hold", …, root_cause=…)`.
|
||
- **FR-3c — транзиент-ретрай** — в `merge_gate._classify_merge_response`-ветке `"transient"`
|
||
(Gitea `405`/`5xx`) и/или `launcher` timeout-kill / транзиент-requeue (Anthropic `Overloaded`) →
|
||
`record("transient_retry", …, detail=<код/причина>)`.
|
||
- **FR-3d (желательно) — post-deploy DEGRADED** — в ветке `stage_engine`, где post-deploy
|
||
`DEGRADED`/rollback ведёт к `set_repo_freeze` (ORCH-088/021) → `record("deploy_degraded", …,
|
||
attribution=None|"unknown", target_repo=repo)` — урок «деплой OK / прод сломан» (слой-3, ET-8),
|
||
атрибуцию проставит ретроспективщик/человек позже.
|
||
|
||
Дедуп/частота автозаписи (чтобы не плодить дубли на ретраях) — решение архитектора (например,
|
||
ключ `work_item_id+stage+lesson_type` в окне); если не реализуется в v1 — отметить в `10-tech-risks.md`.
|
||
|
||
### FR-4 — Read-only выборка (BR-4)
|
||
`db.get_lessons(*, lesson_type=None, status=None, repo=None, work_item_id=None, limit=N) ->
|
||
list[dict]` (параметризованный `SELECT … ORDER BY id DESC LIMIT ?`). `lessons.get(...)` —
|
||
never-raise обёртка → `[]` при ошибке. `lessons.snapshot()` — лёгкая сводка (счётчики по
|
||
типу/статусу, последние N) для `GET /queue`, never-raise → `{}`.
|
||
|
||
### FR-5 — Ручная запись + обновление (BR-5, BR-6)
|
||
- `POST /lessons` (тело JSON) → `lessons.record(..., source="manual")`. Возвращает `{id}`.
|
||
- `POST /lessons/{id}` (или поля в `POST /lessons`) → `lessons.update(id, status=…,
|
||
attribution=…, target_repo=…, target_domain=…, related_task=…, root_cause=…, suggestion=…)` →
|
||
`db.update_lesson` (`UPDATE … SET … updated_at=datetime('now')`). Позволяет ретроспективщику/
|
||
человеку классифицировать автозаписанный `unknown`. never-raise.
|
||
|
||
### FR-6 — Kill-switch + изоляция (NFR-2, NFR-3)
|
||
`lessons_enabled=False` → `record`/`get`/`update`/`snapshot` инертны, эндпоинты возвращают
|
||
`{"enabled": false}` (паттерн `metrics_endpoint_enabled`), врезки no-op. Поведение конвейера —
|
||
байт-в-байт прежнее. enduro не затронут (общая БД, аддитивная таблица).
|
||
|
||
## 4. Изменения API
|
||
Новые эндпоинты в `src/main.py` (стиль `GET /queue` / `POST /coverage/baseline`):
|
||
- **`GET /lessons`** — read-only выборка. Query: `type`, `status`, `repo`, `work_item`, `limit`
|
||
(дефолт из конфига). Ответ: `{"enabled": bool, "lessons": [ {…строка…} ]}`. Всегда `200`.
|
||
- **`POST /lessons`** — ручная запись. Тело: `lesson_type` (обяз.) + опциональные поля контекста/
|
||
анализа/атрибуции. Ответ: `{"id": <int>}` или `{"enabled": false}`.
|
||
- **(опц.) `POST /lessons/{id}`** — обновление статуса/атрибуции/привязки задачи. Ответ `{"ok": bool}`.
|
||
- `GET /queue` — добавить read-only ключ `"lessons": lessons.snapshot()` (рядом с `serial_gate`/
|
||
`coverage`/`bug_fast_track`). Существующие ключи — без изменений.
|
||
|
||
`GET /health` / `GET /status` / `GET /metrics` / прочие эндпоинты — **байт-в-байт прежние**.
|
||
|
||
## 5. Изменения схемы БД
|
||
**Новая аддитивная таблица `lessons`** (FR-1) + два индекса, всё `IF NOT EXISTS` / `_ensure_column`.
|
||
Существующие таблицы (`tasks`/`jobs`/`agent_runs`/`events`/`job_deps`/`repo_freeze`/
|
||
`coverage_baseline`/`tracker_messages`) — **не тронуты**. Колонки атрибуции — сразу, нуллабельные
|
||
(BR-2/NFR-6). Restart-safe, идемпотентно, безопасно на живой общей прод-БД (enduro не затронут).
|
||
|
||
## 6. Требования к новым/изменённым QG checks
|
||
**Нет.** Журнал уроков — наблюдатель, **не** Quality Gate. `QG_CHECKS` / `check_*` /
|
||
machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
|
||
`coverage_status:`) — байт-в-байт не тронуты. Журнал не влияет на продвижение по стадиям.
|
||
|
||
## 7. Совместимость / регресс
|
||
- **Kill-switch** `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`): `False` → полная
|
||
инертность, нулевая регрессия.
|
||
- **never-raise** на всех публичных функциях и врезках (NFR-1) — сбой журнала не роняет конвейер.
|
||
- **Аддитивно**: только новая таблица + leaf + эндпоинты + тонкие врезки; ничего существующего не
|
||
переписывается.
|
||
- **Изоляция enduro**: общая БД, новая таблица; репо-скоуп через поле/фильтр выборки.
|
||
- **Обратимость**: выключение флага возвращает прод к доресурсному поведению мгновенно.
|
||
- **Self-hosting безопасность** (NFR-7): модуль не деплоит/не рестартит прод/не трогает `main`/без
|
||
процессов/сети.
|
||
- **Артефакты pipeline:** задача создаёт/обновляет стандартный пакет (`01`–`04` + `06-adr` от
|
||
архитектора, `12`/`13`/`14`/`15`/`17`/`18` по ходу конвейера). Сам журнал — БД-сущность, не
|
||
номерной артефакт.
|