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