--- 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": }` или `{"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` по ходу конвейера). Сам журнал — БД-сущность, не номерной артефакт.