13 KiB
work_item, stage, author_agent, status, created_at, model_used
| work_item | stage | author_agent | status | created_at | model_used |
|---|---|---|---|---|---|
| ORCH-098 | analysis | analyst | ready-for-review | 2026-06-10 | 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):
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(reviewerREQUEST_CHANGES, testercheck_tests_passedFAIL, 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"(Gitea405/5xx) и/илиlaunchertimeout-kill / транзиент-requeue (AnthropicOverloaded) →record("transient_retry", …, detail=<код/причина>). - FR-3d (желательно) — post-deploy DEGRADED — в ветке
stage_engine, где post-deployDEGRADED/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(envORCH_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по ходу конвейера). Сам журнал — БД-сущность, не номерной артефакт.