diff --git a/docs/work-items/ORCH-098/01-brd.md b/docs/work-items/ORCH-098/01-brd.md new file mode 100644 index 0000000..4d12454 --- /dev/null +++ b/docs/work-items/ORCH-098/01-brd.md @@ -0,0 +1,143 @@ +--- +work_item: ORCH-098 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 01 — BRD (бизнес-требования): ORCH-098 — FND: машинный журнал уроков (структурированная база отклонений) + +Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: analysis + +## 1. Бизнес-контекст и проблема + +Оркестратор уже автономно проводит задачи через конвейер (ORCH-54), но **развивает** платформу +по-прежнему вручную связка Слава+Стрим: ловим инциденты → формулируем уроки → заводим задачи. +Уроки сегодня живут **свободным текстом** в `memory/` — они не машиночитаемы, по ним нельзя +считать паттерны, нельзя приоритизировать, нельзя автоматически предлагать улучшения. + +ORCH-098 — шаг 1 эпика саморазвития (`docs/epics/self-evolution.md`, **домен 0 «Фундамент», F2**, +ORCH-8). Это **«топливо» вертикали-двигателя** (петля самообучения 8A): формализовать свободный +текст в **машинную структурированную таблицу отклонений конвейера**. Каждый урок — запись с +полями для машинного анализа паттернов. Журнал — фундамент, на котором позже встанут +ретроспективщик (E2), приоритизатор RICE (E3) и Стрим как потребители. + +**Установленные факты-источники сигналов («уроков»)** — из памяти орка (инциденты 06–09.06) и §8A +эпика: +- Провал гейта (BLOCKED / FAILED / REQUEST_CHANGES). +- **Ручное вмешательство человека — самый ценный сигнал** (каждый ручной пинок = дыра автономности). +- Ретраи, откаты деплоя, таймауты агентов. +- Ложные срабатывания гейтов (исторический пример: substring `PASS` в `check_tests_passed`). +- «Деплой SUCCESS, а прод не работает» (урок ET-8); транзиенты (Gitea `405`, Anthropic `Overloaded`). + +**Решение Славы 10.06 (ОБЯЗАТЕЛЬНО учесть на этапе схемы):** схема журнала ДОЛЖНА **с самого +начала** нести поля для будущей **АТРИБУЦИИ** урока (иначе потом переделывать схему на живой +общей прод-БД). Атрибуция (`platform-level` / `project-level` / `both` / `unknown`), целевой +проект и целевой домен улучшения — это §8A эпика «platform-level vs project-level». При автозаписи +поля атрибуции могут быть пустыми/`unknown` (классификацию позже ставит ретроспективщик/Стрим), но +**колонки в схеме должны существовать сразу** — аддитивные, нуллабельные. + +**Связь со слоями наблюдения (§2 эпика):** деградация продукта (слой 3, урок ET-8) — один из типов +урока; журнал должен уметь его хранить с атрибуцией `platform`/`project`. + +## 2. Объём (scope) + +### В объёме +- Аддитивная идемпотентная таблица БД `lessons` для структурированных уроков со всеми полями + контекста, анализа, статуса **и атрибуции** (колонки атрибуции — сразу, нуллабельные). +- Leaf-модуль `src/lessons.py` (never-raise, kill-switch) + helper записи урока. +- **Автозапись** ≥2–3 типов отклонений из кода через best-effort точки врезки в + `stage_engine.py` / `merge_gate.py` / `launcher.py` (провал гейта/откат, HOLD, транзиент-ретрай). +- **Read-only выборка** уроков (HTTP-эндпоинт + блок в `GET /queue`) — для будущего + ретроспективщика и Стрим. +- **Ручная запись** урока (HTTP-эндпоинт / helper) — Стрим/оператор кладёт урок руками. +- Доки (CLAUDE.md / architecture README / ADR) + `CHANGELOG.md`. + +### Вне объёма +- **Анализ паттернов / ретроспективщик (E2)** — отдельная задача-потребитель журнала. +- **Приоритизатор RICE (E3)** — отдельная задача. +- **Автоматическая классификация атрибуции** — её ставит ретроспективщик/человек позже; здесь — + только колонки и возможность проставить значение руками/через update. +- **Банк идей (D4 / идеатор, E5)** — отдельный реестр, НЕ путать с журналом уроков. +- **Слой-3 детекция здоровья продукта** (мониторинг задеплоенного приложения) — отдельная + D4/D5-способность; журнал лишь умеет **хранить** такой урок, когда детектор появится. +- Изменение `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключей / любых + существующих таблиц. +- Миграция исторических уроков из `memory/` (ручной разовый импорт — вне объёма). + +## 3. Заинтересованные стороны +- **Заказчик:** Слава (требование атрибуции 10.06 — нормативно). +- **Прямой потребитель (будущее):** агент-ретроспективщик E2, приоритизатор E3, Стрим (ручной + разбор). +- **Затрагивается:** self-hosting прод-инстанс orchestrator (общая БД и очередь с enduro-trails) — + enduro **не должен быть затронут** (аддитивность, never-raise). +- **Принимает результат:** reviewer/tester конвейера + Слава. + +## 4. Бизнес-требования (BR) + +- **BR-1 — Структурированная таблица уроков.** Аддитивная, идемпотентная (`CREATE TABLE IF NOT + EXISTS`) таблица `lessons` на общей прод-БД с полями: тип отклонения; контекст + (work_item/task/стадия/агент/repo); корневая причина (если известна); предложенное улучшение + (если есть); статус (`new`/`in_progress`/`closed`/`linked`) + связанная задача; timestamp. +- **BR-2 — Поля атрибуции с самого начала.** Схема несёт **сразу** нуллабельные колонки: + `attribution` (`platform`/`project`/`both`/`unknown`), `target_repo` (кого касается: + `orchestrator`/`enduro-trails`/др.), `target_domain` (домен улучшения: + `reliability`/`quality`/`economy`/`features`/`scale`). При автозаписи допустимо пусто/`unknown`. +- **BR-3 — Автозапись ≥2–3 типов отклонений.** Из кода, best-effort, в детерминированных + choke-point: (а) провал гейта / откат на `development` (reviewer REQUEST_CHANGES, tester FAIL, + staging/deploy FAILED), (б) HOLD merge-актора / regression-guard HOLD, (в) транзиент-ретрай + (Gitea-merge `405`/`5xx`, Anthropic `Overloaded`/agent-timeout requeue). Дополнительно желательно + (г) post-deploy `DEGRADED` (урок «деплой OK / прод сломан», слой-3, ET-8) с атрибуцией. +- **BR-4 — Read-only выборка.** HTTP-эндпоинт `GET /lessons` (фильтры: тип/статус/repo/work_item, + лимит) + read-only блок `lessons` в `GET /queue` (сводка). Только чтение. +- **BR-5 — Ручная запись.** HTTP-эндпоинт `POST /lessons` (+ публичный helper) — оператор/Стрим + кладёт урок руками, в т.ч. с проставленной атрибуцией. +- **BR-6 — Обновление урока.** Возможность сменить статус / проставить атрибуцию / привязать + задачу после создания (helper/эндпоинт `POST /lessons/{id}` или поля в `POST /lessons`) — чтобы + ретроспективщик/человек позже классифицировал автозаписанный `unknown`. + +## 5. Нефункциональные требования (NFR) + +- **NFR-1 — never-raise (критично, self-hosting).** Сбой записи/чтения урока **никогда** не роняет + и не тормозит конвейер. Любая ошибка детектора/записи → лог WARNING + продолжение основного + потока. Журнал — наблюдатель, не участник пайплайна. +- **NFR-2 — Kill-switch.** Флаг `lessons_enabled` (env `ORCH_LESSONS_ENABLED`). `False` → + автозапись и эндпоинты инертны (нулевая регрессия, поведение конвейера байт-в-байт прежнее). +- **NFR-3 — Аддитивность / изоляция enduro.** Только новая таблица + новый leaf + новые эндпоинты + + тонкие врезки. `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схема + существующих таблиц — **байт-в-байт не тронуты**. Общая БД: enduro-trails не затронут. +- **NFR-4 — Restart-safe / идемпотентность таблицы.** `CREATE TABLE IF NOT EXISTS` + `_ensure_column` + (паттерн `repo_freeze`/`coverage_baseline`) — безопасно на живой БД, повторный старт без эффекта. +- **NFR-5 — Лёгкость.** Запись — один `INSERT`, чтение — простые `SELECT` (общий хост впритык: + RAM 171Mi free, диск 92%). Никаких фоновых потоков/сканов. +- **NFR-6 — Схема-forward-proof.** Колонки атрибуции добавлены сразу (BR-2), чтобы не + переделывать схему на живой БД, когда появится ретроспективщик. +- **NFR-7 — Self-hosting безопасность.** Модуль только пишет/читает БД и отдаёт JSON — не + деплоит, не рестартит прод, не трогает `main`, не порождает процессы/сеть. + +## 6. Допущения и ограничения +- Журнал уроков — **исключение** из правила «наблюдатель отделён от наблюдаемого» (§2 эпика): это + историческая память петли, не realtime-мониторинг → допустимо в БД орка; запись best-effort. +- Точки автозаписи привязаны к существующим choke-point: `stage_engine._handle_qg_failure_rollbacks` + (откаты), `merge_gate` (HOLD/transient-классификатор ORCH-093), `launcher` (timeout/requeue + транзиентов). Архитектор уточняет точный набор и сигнатуры врезок. +- Набор значений `lesson_type` / `attribution` / `target_domain` — конвенция (строковые слаги), + не enum-констрейнт БД (forward-compatible; новый тип не требует миграции). +- Общая прод-БД с enduro: любое поле repo-scoped, фильтрация на уровне выборки. + +## 7. Критерии успеха +Таблица `lessons` создаётся идемпотентно на старте; автозаписаны ≥2–3 типа отклонений из реального +прогона; `GET /lessons` и `POST /lessons` работают; атрибутивные колонки присутствуют и +проставляемы; kill-switch выключает всё без регрессии; `pytest tests/ -q` зелёный; доки+CHANGELOG +обновлены. Детальные PASS/FAIL — `03-acceptance-criteria.md`. + +## 8. Риски +- Врезка детектора в горячий путь конвейера → риск регрессии при сбое записи. Митигация: NFR-1 + never-raise + kill-switch. +- Рост таблицы со временем (автозапись на каждом откате/ретрае). Митигация: лёгкие строки; + будущая ретенция — вне объёма, отметить в `10-tech-risks.md` (архитектор). +- Недооформленная схема атрибуции → переделка на живой БД. Митигация: BR-2/NFR-6 (колонки сразу). +- Детали и архитектурные развилки (точные точки врезки, индексы, дедуп автозаписей) — задача + архитектора (`06-adr/`, `10-tech-risks.md`). diff --git a/docs/work-items/ORCH-098/02-trz.md b/docs/work-items/ORCH-098/02-trz.md new file mode 100644 index 0000000..a695ed8 --- /dev/null +++ b/docs/work-items/ORCH-098/02-trz.md @@ -0,0 +1,163 @@ +--- +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` по ходу конвейера). Сам журнал — БД-сущность, не + номерной артефакт. diff --git a/docs/work-items/ORCH-098/03-acceptance-criteria.md b/docs/work-items/ORCH-098/03-acceptance-criteria.md new file mode 100644 index 0000000..0a1a2fd --- /dev/null +++ b/docs/work-items/ORCH-098/03-acceptance-criteria.md @@ -0,0 +1,123 @@ +--- +work_item: ORCH-098 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 03 — Критерии приёмки (Acceptance Criteria): ORCH-098 — FND: машинный журнал уроков + +Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: analysis + +Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что +считается провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам. + +--- + +## AC-1 — Аддитивная таблица уроков + +**Условие:** `db.init_db()` создаёт таблицу `lessons` идемпотентно. +- **PASS:** в `src/db.py` есть `CREATE TABLE IF NOT EXISTS lessons (...)` со всеми полями + (`lesson_type`, контекст `work_item_id/task_id/stage/agent/repo`, `root_cause`, `suggestion`, + `status`+`related_task`, `created_at`); повторный `init_db()` не падает и не дублирует; таблица + создаётся на общей прод-БД без изменения существующих таблиц. +- **FAIL:** таблицы нет / создаётся не идемпотентно / отсутствует любое поле из BR-1 / меняется + схема существующей таблицы. + +--- + +## AC-2 — Поля атрибуции присутствуют с самого начала + +**Условие:** схема `lessons` несёт нуллабельные колонки атрибуции (требование Славы 10.06). +- **PASS:** колонки `attribution` (`platform`/`project`/`both`/`unknown`), `target_repo`, + `target_domain` существуют сразу, нуллабельны, допускают пустое/`unknown` при автозаписи и + проставляются позже через update. +- **FAIL:** хотя бы одной из трёх колонок нет в исходной схеме / колонка `NOT NULL` без дефолта / + атрибуцию нельзя проставить после создания записи. + +--- + +## AC-3 — Автозапись ≥2–3 типов отклонений + +**Условие:** из кода автоматически (best-effort, `source="auto"`) пишутся минимум 2–3 типа уроков. +- **PASS:** есть врезки `lessons.record(...)` минимум в двух-трёх точках из: + `stage_engine._handle_qg_failure_rollbacks` (gate-fail/откат), `merge_gate` (HOLD/transient), + `launcher` (timeout/transient-requeue); интеграционный тест подтверждает появление строки в + `lessons` после смоделированного отклонения. +- **FAIL:** автозаписи нет / реализован <2 типов / врезка может бросить исключение в горячий путь. + +--- + +## AC-4 — Read-only выборка + +**Условие:** уроки можно прочитать через эндпоинт и сводку в `GET /queue`. +- **PASS:** `GET /lessons` возвращает `200` с массивом уроков, поддерживает фильтры + (type/status/repo/work_item/limit); `GET /queue` содержит read-only блок `lessons`; ни один + путь чтения не мутирует данные. +- **FAIL:** эндпоинта нет / не фильтрует / чтение мутирует данные / блока в `/queue` нет. + +--- + +## AC-5 — Ручная запись и обновление + +**Условие:** оператор/Стрим кладёт урок руками и может его доклассифицировать. +- **PASS:** `POST /lessons` создаёт урок (`source="manual"`, можно задать атрибуцию); обновление + (`POST /lessons/{id}` или поля) меняет `status`/`attribution`/`target_*`/`related_task` и + стампит `updated_at`. +- **FAIL:** ручной записи нет / нельзя проставить атрибуцию / нельзя обновить автозаписанный урок. + +--- + +## AC-6 — never-raise (сбой журнала не роняет конвейер) + +**Условие:** любая ошибка записи/чтения урока изолирована от пайплайна. +- **PASS:** все публичные функции `src/lessons.py` и все врезки обёрнуты так, что исключение БД/ + любого источника → `logger.warning` + безопасный дефолт (`None`/`[]`/`{}`); юнит-тест с + замоканной падающей БД подтверждает, что вызывающий код (откат/HOLD/retry) не падает. +- **FAIL:** исключение из журнала пробивается в `stage_engine`/`merge_gate`/`launcher`/эндпоинт. + +--- + +## AC-7 — Kill-switch и нулевая регрессия + +**Условие:** `lessons_enabled=False` делает функционал инертным. +- **PASS:** при `False` `record`/`get`/`update`/`snapshot` — no-op (без обращения к БД), эндпоинты + отдают `{"enabled": false}`, врезки не пишут; поведение конвейера и `GET /queue` (помимо нового + блока) — байт-в-байт прежнее; enduro-trails не затронут. +- **FAIL:** при `False` журнал что-то пишет/ломает / меняется поведение конвейера / затронут enduro. + +--- + +## AC-8 — Инварианты конвейера не тронуты + +**Условие:** изменение не касается машины стадий и гейтов. +- **PASS:** `STAGE_TRANSITIONS`, реестр `QG_CHECKS`, функции `check_*`, machine-verdict-ключи и + схема существующих таблиц — **диффом не затронуты**; журнал не влияет на продвижение по стадиям. +- **FAIL:** изменён любой из перечисленных артефактов / журнал участвует в решении гейта. + +--- + +## AC-9 — Тесты, документация, CHANGELOG + +**Условие:** изменение проверено и задокументировано. +- **PASS:** `pytest tests/ -q` зелёный (включая новый `tests/test_lessons.py` с unit+integration); + обновлены `CLAUDE.md` + `docs/architecture/README.md`; в задаче есть `06-adr/` (архитектор); + `CHANGELOG.md` дополнен. +- **FAIL:** тесты падают / нет покрытия новой логики / документация или CHANGELOG не обновлены. + +--- + +## Сводная матрица AC ↔ FR/BR +| AC | Покрывает | +|----|-----------| +| AC-1 | BR-1 / FR-1 | +| AC-2 | BR-2 / FR-1 / NFR-6 | +| AC-3 | BR-3 / FR-2 / FR-3 | +| AC-4 | BR-4 / FR-4 | +| AC-5 | BR-5 / BR-6 / FR-5 | +| AC-6 | NFR-1 / FR-2 | +| AC-7 | NFR-2 / NFR-3 / FR-6 | +| AC-8 | NFR-3 / FR-6 | +| AC-9 | NFR-1…NFR-7 (верификация) | diff --git a/docs/work-items/ORCH-098/04-test-plan.yaml b/docs/work-items/ORCH-098/04-test-plan.yaml new file mode 100644 index 0000000..03e03ec --- /dev/null +++ b/docs/work-items/ORCH-098/04-test-plan.yaml @@ -0,0 +1,91 @@ +work_item: ORCH-098 +stage: analysis +author_agent: analyst +status: ready-for-review +created_at: 2026-06-10 +model_used: claude-opus-4-8 +title: "Журнал уроков: таблица, автозапись отклонений, выборка, ручная запись, never-raise" +framework: pytest +scope: > + Покрывается: создание аддитивной таблицы lessons (идемпотентность, поля атрибуции), + helper записи record(), автозапись из choke-point (gate-fail/HOLD/transient), read-only + выборка get_lessons + snapshot, ручная запись/обновление, kill-switch, never-raise. + Вне покрытия: ретроспективщик (E2), приоритизатор (E3), автоклассификация атрибуции, + слой-3 детекция здоровья продукта. +notes: > + Тесты используют изолированную временную SQLite-БД (фикстура init_db во временном файле). + Полный регресс tests/ должен оставаться зелёным. Self-hosting: журнал never-raise — ни один + тест не должен показать, что сбой записи урока роняет конвейер. + +tests: + - id: TC-01 + type: unit + description: "init_db() создаёт таблицу lessons идемпотентно (двойной вызов не падает, нет дублей); присутствуют все поля BR-1." + module: tests/test_lessons.py + expected: PASS + + - id: TC-02 + type: unit + description: "Схема lessons несёт нуллабельные колонки атрибуции attribution/target_repo/target_domain; запись без них проходит (NULL/unknown), update проставляет их позже." + module: tests/test_lessons.py + expected: PASS + + - id: TC-03 + type: unit + description: "lessons.record() вставляет строку с переданными полями (source=auto/manual), возвращает id; created_at заполняется." + module: tests/test_lessons.py + expected: PASS + + - id: TC-04 + type: unit + description: "never-raise: при замоканной падающей БД record/get/update/snapshot возвращают None/[]/{} и не бросают исключение (logger.warning)." + module: tests/test_lessons.py + expected: PASS + + - id: TC-05 + type: unit + description: "kill-switch: при lessons_enabled=False record/get/update/snapshot инертны (no-op, без обращения к БД)." + module: tests/test_lessons.py + expected: PASS + + - id: TC-06 + type: unit + description: "get_lessons фильтрует по type/status/repo/work_item и соблюдает limit; порядок ORDER BY id DESC." + module: tests/test_lessons.py + expected: PASS + + - id: TC-07 + type: unit + description: "update_lesson меняет status/attribution/target_*/related_task и стампит updated_at; несуществующий id безопасен." + module: tests/test_lessons.py + expected: PASS + + - id: TC-08 + type: integration + description: "Автозапись gate-fail: смоделированный откат на development в _handle_qg_failure_rollbacks создаёт строку lessons type=gate_failure с контекстом (stage/agent/work_item/repo)." + module: tests/test_lessons.py + expected: PASS + + - id: TC-09 + type: integration + description: "Автозапись transient/HOLD: транзиент-ветка merge_gate (или timeout/requeue launcher) пишет урок type=transient_retry/merge_hold; сбой записи не ломает основной путь (never-raise в горячем пути)." + module: tests/test_lessons.py + expected: PASS + + - id: TC-10 + type: integration + description: "GET /lessons возвращает 200 с массивом и фильтрами; GET /queue содержит read-only блок lessons; чтение не мутирует данные." + module: tests/test_lessons.py + expected: PASS + + - id: TC-11 + type: integration + description: "POST /lessons создаёт ручной урок (source=manual, с атрибуцией); POST /lessons/{id} обновляет его; при lessons_enabled=False эндпоинты отдают {enabled:false}." + module: tests/test_lessons.py + expected: PASS + + - id: TC-12 + type: unit + description: "Инварианты конвейера не тронуты: STAGE_TRANSITIONS/QG_CHECKS/machine-verdict-ключи неизменны (структурный анти-регресс по составу реестра)." + module: tests/test_lessons.py + expected: PASS