From 9f62df02ebb60da5d85b4f157bf346f73571fc71 Mon Sep 17 00:00:00 2001 From: claude-bot Date: Wed, 10 Jun 2026 10:10:08 +0300 Subject: [PATCH] architect(ET): auto-commit from architect run_id=574 --- docs/architecture/README.md | 1 + .../adr/adr-0033-lessons-journal.md | 92 +++++++ .../06-adr/ADR-001-lessons-journal.md | 244 ++++++++++++++++++ .../ORCH-098/07-infra-requirements.md | 45 ++++ .../ORCH-098/08-data-requirements.md | 76 ++++++ docs/work-items/ORCH-098/10-tech-risks.md | 39 +++ 6 files changed, 497 insertions(+) create mode 100644 docs/architecture/adr/adr-0033-lessons-journal.md create mode 100644 docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md create mode 100644 docs/work-items/ORCH-098/07-infra-requirements.md create mode 100644 docs/work-items/ORCH-098/08-data-requirements.md create mode 100644 docs/work-items/ORCH-098/10-tech-risks.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index a54c24c..a80007b 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -20,6 +20,7 @@ - **Plane Sync** (`src/plane_sync.py`) — синхронизация статусов/комментариев в Plane. Резолв статусов проекта `get_project_states` (ORCH-10) кэширует `{logical_key→uuid}` per-project; **ORCH-068** добавляет в кэш-запись `{uuid→group}` (для терминал-исключения F-2) и **TTL** `ORCH_PLANE_STATES_TTL_S` (дефолт 300с; `0` → прежний lifetime-кэш) — устаревший набор статусов самозалечивается без рестарта процесса через существующий `reload_project_states()` (баг кэша после появления нового Plane-статуса). Форма возврата `get_project_states` неизменна (обратная совместимость). - **FS ownership detect** (`src/fs_normalize.py`, ORCH-057 — [adr-0031](adr/adr-0031-legacy-ownership-normalization.md)) — чистый **never-raise** leaf (паттерн `serial_gate`/`preflight`), закрывает пробел ORCH-040: при миграции на `user: "1000:1000"` legacy `root:root` файлы в `/repos` ломали создание worktree под uid 1000 (`ensure_worktree` → сырой `fatal: … Permission denied`, агент не стартовал). Три слоя: (1) **D1** — `src/git_worktree.py::ensure_worktree` классифицирует класс «нет прав» (`Permission denied`/`could not create leading directories`/`insufficient permission`/`EACCES`/`EPERM`) и поднимает actionable `RuntimeError` с причиной + лечащей командой (не-прав-ошибки сохраняют прежний контракт — меняется только формулировка, не факт сбоя); (2) **D2** — `scan_ownership(roots, target_uid=os.getuid())` обходит `/repos/_wt`, `/.git/{objects,worktrees}`, `data/runs` с ранним выходом при первом `st_uid != target_uid` + TTL-кэш; (3) **D3** — best-effort вызов на старте `main.lifespan` → WARNING + Telegram при mismatch (claim **НЕ** блокируется — внятный ранний отказ даёт D1 в точке launch, знающей repo; preflight-блок отвергнут как repo-слепой → регресс enduro). Опц. `normalize()` chown'ит только при `CAP_CHOWN` (под uid 1000 — no-op; init-контейнер/root-entrypoint отвергнуты — реинтродукция root-контекста + self-deploy compose). Фактическая нормализация = **операторская процедура** под root на хосте (`INFRA.md` «Миграция uid»). Условность `applies(repo)` first: `fs_normalize_enabled` (kill-switch) + `fs_normalize_repos` (CSV, пусто → self-hosting only). Наблюдаемость — блок `fs_ownership` в `GET /queue`; опц. `POST /fs-normalize/check`. `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схема БД — не тронуты. Детали — `docs/work-items/ORCH-057/06-adr/ADR-001-legacy-ownership-normalization.md`. - **Metrics endpoint** (`src/metrics.py` + `GET /metrics`, ORCH-099 — [adr-0030](adr/adr-0030-metrics-endpoint.md)) — лёгкий **read-only** leaf-сборщик (`build_metrics() -> dict`, never-raise по разделам, паттерн `serial_gate.snapshot()`) + тонкий эндпоинт (стиль `GET /queue`). Отдаёт JSON-«сырьё» о самом орке (стадии задач / очередь jobs / agent-liveness / стоимость-токены) как **стабильный машинный контракт для sidecar F1b** (`watchdog/`, отдельная задача — наблюдатель отделён от наблюдаемого). Только чтение существующих `tasks`/`jobs`/`agent_runs` + in-memory-снапшотов (`worker.breaker`); два read-only helper'а в `db.py` (`get_running_agents`/`agent_cost_totals`). Логику мониторинга (пороги/алерты/история/Telegram) НЕ несёт — это F1b. Контракт ниже (§ «Сырьё-эндпоинт `/metrics`»). Kill-switch `metrics_endpoint_enabled` (дефолт `True`). `STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД — не тронуты. +- **Lessons journal** (`src/lessons.py` + таблица `lessons`, ORCH-098 — design, [adr-0034](adr/adr-0034-lessons-journal.md)) — машинный журнал уроков (структурированная база отклонений конвейера); шаг 1 эпика саморазвития (домен 0 «Фундамент», F2; топливо петли самообучения 8A), фундамент для будущих ретроспективщика (E2)/приоритизатора RICE (E3)/Стрим. Чистый **observer-leaf** (never-raise, паттерн `serial_gate`/`coverage_gate`/`metrics`): `record()`/`get()`/`update()`/`snapshot()`. **Аддитивная идемпотентная таблица `lessons`** (`CREATE TABLE IF NOT EXISTS` в `init_db()`, restart-safe) с полями контекста (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализа (`root_cause`/`suggestion`), статуса (`status`/`related_task`) и **атрибуции — сразу и нуллабельно** (`attribution`/`target_repo`/`target_domain`, требование Славы 10.06 / NFR-6, заполняется позже ретроспективщиком/человеком) + `source`/`detail`; без `enum`-констрейнтов (слаги forward-compatible). **Автозапись 4 типов** (`source="auto"`, best-effort, дедуп в окне; `transient_retry` — только на исчерпании бюджета ретраев) тонкими врезками: `gate_failure` (`stage_engine._handle_qg_failure_rollbacks`), `merge_hold` (`merge_gate._handle_merge_verify` HOLD), `transient_retry` (merge-retry/launcher transient budget-exhaustion), `deploy_degraded` (post-deploy `DEGRADED → set_repo_freeze`, урок слоя-3 «деплой OK / прод сломан», ET-8). Эндпоинты `GET /lessons` (read-only, фильтры), `POST /lessons` (ручная запись), `POST /lessons/{id}` (update/доклассификация), + read-only ключ `lessons` в `GET /queue`. **Расхождение с гейт-шаблоном:** журнал observer-only → **НЕ скоупится по репо** (kill-switch `lessons_enabled` only, без `lessons_repos`); репо-разрез — на выборке (`repo`-колонка/фильтр), enduro не затронут (общая БД, аддитивная таблица). `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы существующих таблиц — байт-в-байт не тронуты (журнал не участвует в решении гейта). Kill-switch `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`). Детали — `docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md`. - **Sidecar-watchdog F1b** (`watchdog/` + сервис `orchestrator-watchdog`, ORCH-100 — [adr-0033](adr/adr-0033-sidecar-watchdog.md)) — **мозг мониторинга в ОТДЕЛЬНОМ контейнере** (наблюдатель отделён от наблюдаемого, C-1): код в репо орка (`watchdog/`), рантайм — свой образ (`watchdog/Dockerfile`, `python:3.12-slim`, **stdlib-only**) + сервис в `docker-compose.yml` (`network_mode: host`, read-only `docker.sock`, `mem_limit: 128m`). На каждом тике собирает 4 источника: `GET /metrics` орка (F1a/ORCH-099), хост (диск/inode/память/CPU, stdlib), статусы контейнеров через read-only `docker.sock` (GET-only, без `docker` SDK), пинг Plane/Gitea/Anthropic. Каждый сигнал → **обобщённая чистая** `decide(signal_active, prev, now, cooldown)` (генерализация `disk_watchdog.decide_action`, per-signal in-memory `AlertState`) → алерт в **собственный** Telegram-канал sidecar (`WATCHDOG_TG_*`, **НЕ** импорт `src/notifications.py`). Особый сигнал `orch_down` — `/metrics` не отвечает (наблюдатель жив, наблюдаемый лёг). Диск: штатные 85% остаются за `disk_watchdog` (ORCH-063, нулевой дубль), sidecar — `orch_down` + opt-in потолок 97% (default off). never-raise, kill-switch `WATCHDOG_ENABLED`, строго read-only к наблюдаемому; `src/**`/`STAGE_TRANSITIONS`/`QG_CHECKS`/схема БД орка — не тронуты. Подробнее ниже (§ «Sidecar-watchdog F1b»). Детали — `docs/work-items/ORCH-100/06-adr/ADR-001-sidecar-watchdog.md`. ## Сырьё-эндпоинт `/metrics` для sidecar (ORCH-099 — design) diff --git a/docs/architecture/adr/adr-0033-lessons-journal.md b/docs/architecture/adr/adr-0033-lessons-journal.md new file mode 100644 index 0000000..25ac65f --- /dev/null +++ b/docs/architecture/adr/adr-0033-lessons-journal.md @@ -0,0 +1,92 @@ +--- +work_item: ORCH-098 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# adr-0033: Машинный журнал уроков — таблица `lessons` + observer-leaf (ORCH-098) + +## Статус +Proposed + +## Контекст + +Оркестратор автономно ведёт задачи по конвейеру (ORCH-54), но **развивается** вручную: инциденты → +уроки → задачи. Уроки живут свободным текстом в `memory/` — не машиночитаемы: нельзя считать +паттерны, приоритизировать, предлагать улучшения. ORCH-098 — шаг 1 эпика саморазвития (домен 0 +«Фундамент», F2): «топливо» петли самообучения 8A. Нужна **структурированная таблица отклонений +конвейера**, на которой позже встанут ретроспективщик (E2), приоритизатор RICE (E3) и Стрим. + +Нормативное требование Славы (10.06): схема ДОЛЖНА **сразу** нести поля **атрибуции** урока +(`platform`/`project`/`both`/`unknown` + целевой репо + домен улучшения), иначе позже придётся +переделывать схему на живой общей прод-БД. + +**Кросс-каттинговость** (почему сквозной ADR): новый компонент `src/lessons.py` + аддитивная +таблица на **общей прод-БД** (self-hosting, разделяемой с enduro-trails) + врезки автозаписи в +несколько горячих choke-point'ов (`stage_engine`/`merge_gate`/`launcher`) + новый раздел контракта +`GET /queue`. Фундамент для будущих задач-потребителей → регистрируется глобально. + +## Решение + +Журнал уроков — **observer (наблюдатель), НЕ Quality Gate**. Аддитивная таблица + чистый leaf, +по образцу `serial_gate`/`coverage_gate`/`metrics`/`bug_fast_track`. + +1. **Таблица `lessons`** (`db.init_db()`, `CREATE TABLE IF NOT EXISTS` + 3 индекса, идемпотентно, + restart-safe) — поля контекста (`work_item_id`/`task_id`/`stage`/`agent`/`repo`), анализа + (`root_cause`/`suggestion`), статуса (`status`/`related_task`), **атрибуции сразу и нуллабельно** + (`attribution`/`target_repo`/`target_domain`) + `source`/`detail`. Без `enum`-констрейнтов + (слаги forward-compatible). Будущие колонки — `_ensure_column`. + +2. **Leaf `src/lessons.py`** (never-raise, импортирует только `config`+`db`): `record()` / `get()` / + `update()` / `snapshot()`. **Расхождение с гейт-шаблоном: журнал НЕ скоупится по репо** — он + observer-only и не *действует* ни на один репо; единственный регулятор — глобальный kill-switch + `lessons_enabled`. Запись урока про enduro ценна и **не затрагивает** пайплайн enduro (чистая + память орка); репо-разрез — на выборке (`repo`-колонка/фильтр). + +3. **Автозапись 4 типов** (`source="auto"`, best-effort, дедуп в окне; `transient_retry` — только на + исчерпании бюджета ретраев): `gate_failure` (`stage_engine._handle_qg_failure_rollbacks`), + `merge_hold` (`merge_gate._handle_merge_verify` HOLD), `transient_retry` (merge-retry/launcher + transient budget-exhaustion), `deploy_degraded` (post-deploy `DEGRADED → set_repo_freeze`, урок + слоя-3 «деплой OK / прод сломан», ET-8). Каждая врезка — одиночный вызов в защитном `try/except`. + +4. **Эндпоинты** `GET /lessons` (read-only, фильтры), `POST /lessons` (ручная запись, + `source="manual"`), `POST /lessons/{id}` (update — доклассификация `unknown`), + read-only ключ + `"lessons": snapshot()` в `GET /queue`. При выключенном флаге → `{"enabled": false}`. + +**Инвариант (нерушимый):** `STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи +(`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/`coverage_status:`) / +схемы существующих таблиц — **байт-в-байт не тронуты**. Журнал не влияет на продвижение по стадиям. + +## Композиция с существующими механизмами +- **Self-hosting (общая БД):** аддитивная таблица; enduro не затронут (NFR-3). +- **serial-gate (ORCH-088) / post-deploy (ORCH-021):** детектор `deploy_degraded` врезан рядом с + `set_repo_freeze`, не меняя freeze-логику. +- **merge-gate (ORCH-043/071/093):** `merge_hold`/`transient_retry` читают исход актора, не меняя + классификатор/ретрай. +- **metrics (ORCH-099):** журнал — историческая память петли (best-effort запись), `/metrics` — + realtime-сырьё для sidecar; разные роли, оба observer-only. + +## Условность и откат +- Флаг `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, дефолт `True`; kill-switch) + + `lessons_dedup_window_s` / `lessons_query_limit_default`. `False` → полная инертность, нулевая + регрессия, конвейер байт-в-байт прежний. +- **never-raise** на всех публичных функциях и врезках (NFR-1) — сбой журнала не роняет конвейер. +- Откат — флаг в `false` (мгновенно) или revert диффа; таблица не касается существующих. + +## Последствия +- **+** Машиночитаемые уроки — фундамент E2/E3/Стрим; атрибуция forward-proof (без передела живой БД). +- **+** Нулевая регрессия; проверенный additive-observer-leaf шаблон → низкий риск; enduro изолирован. +- **−** Рост таблицы (митигейшн: лёгкие строки + дедуп + budget-exhaustion; ретенция — будущее). +- **−** Дедуп-запрос в `record()` (один indexed-SELECT, только `auto`). + +## Ссылки +- Локальный ADR: `docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md` +- BRD/TRZ/AC: `docs/work-items/ORCH-098/01-brd.md`, `02-trz.md`, `03-acceptance-criteria.md` +- Data/Infra/Risks: `docs/work-items/ORCH-098/08-data-requirements.md`, `07-infra-requirements.md`, + `10-tech-risks.md` +- Эпик: `docs/epics/self-evolution.md` (домен 0 «Фундамент», F2; петля 8A) +- Сверено по коду: `src/serial_gate.py`, `src/coverage_gate.py`, `src/db.py`, `src/stage_engine.py`, + `src/merge_gate.py`, `src/agents/launcher.py`, `src/main.py`, `src/qg/checks.py`. diff --git a/docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md b/docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md new file mode 100644 index 0000000..2b20c91 --- /dev/null +++ b/docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md @@ -0,0 +1,244 @@ +--- +work_item: ORCH-098 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# ADR-001: Машинный журнал уроков `lessons` — аддитивная таблица + observer-leaf + +Work Item: **ORCH-098** — FND: машинный журнал уроков (структурированная база отклонений конвейера) +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0033-lessons-journal.md`** (решение +кросс-каттинговое: новый компонент + новая таблица на общей прод-БД + фундамент эпика +саморазвития). + +## Статус +Proposed + +## Контекст + +ORCH-098 — шаг 1 («Фундамент», F2) эпика саморазвития: формализовать свободнотекстовые «уроки» +из `memory/` в **машинную структурированную таблицу отклонений конвейера**, на которой позже +встанут ретроспективщик (E2), приоритизатор RICE (E3) и Стрим. BRD/TRZ уже зафиксировали состав +полей, набор эндпоинтов и структуру leaf-модуля; нормативное требование Славы 10.06 — колонки +**атрибуции** в схеме **с самого начала** (нуллабельные), чтобы не переделывать схему на живой +общей прод-БД. + +Сверено по коду (recon): +- **Образец observer-leaf**: `src/serial_gate.py`, `src/coverage_gate.py`, `src/metrics.py` — + чистые leaf'ы, импортируют только `config`+`db`, `applies(repo)`-first, never-raise, `snapshot()` + для `GET /queue`. +- **БД-паттерн**: `db.get_db() -> sqlite3.Connection` (`row_factory=sqlite3.Row`, `.close()` в + `finally`); `db.init_db()` — `executescript` с `CREATE TABLE IF NOT EXISTS …`; идемпотентные + миграции `_ensure_column(conn, table, column, decl)` (`src/db.py:341`). Эталон аддитивной таблицы + — `repo_freeze`, `coverage_baseline`; атомарный helper — `ratchet_coverage_baseline` (`db.py:251`). +- **Choke-point'ы автозаписи** (точные сигнатуры): + - `stage_engine._handle_qg_failure_rollbacks(task_id, current_stage, repo, work_item_id, branch, + agent, qg_name, reason, result)` (`src/stage_engine.py:728`) — все нужные поля контекста + локально доступны. + - post-deploy `DEGRADED → set_repo_freeze` (`src/stage_engine.py:~1993`) — доступны `repo`, + `work_item_id`, `branch`, локально собранный `reason`. + - `merge_gate._handle_merge_verify(task_id, repo, work_item_id, branch, result)` + (`src/merge_gate.py:1588`); ветка HOLD ставит `result.note="merge-not-verified-hold"` (~`:1695`). + - `merge_gate._classify_merge_response(repo, branch, index, status_code) -> "transient"|"terminal"` + (`src/merge_gate.py:811`). + - `launcher._watchdog`/`stop_process` (timeout-kill) и `launcher._finalize_transient(job_id, agent, + run_id, exit_code, job, retry_after)` (`src/agents/launcher.py:997`) — транзиент-requeue с + бюджетом `transient_attempts`. +- **Конфиг-паттерн**: pydantic `BaseSettings` с авто-биндингом `ORCH_*`; пары `*_enabled` (bool) + + `*_repos` (CSV); `is_self_hosting_repo(repo)` (`src/qg/checks.py:520`). + +«Как есть» не годится: уроки в `memory/` не машиночитаемы — нельзя считать паттерны, нельзя +приоритизировать. Нужна структурированная таблица, но врезанная в **горячий путь** конвейера, что +на self-hosting прод-инстансе с общей БД (enduro-trails) требует жёсткой изоляции. + +## Решение + +### Сводка + +Ввести **аддитивную идемпотентную таблицу `lessons`** + **чистый observer-leaf `src/lessons.py`** +(never-raise, kill-switch) по образцу `serial_gate`/`coverage_gate`/`metrics`. Leaf несёт +`record()` / `get()` / `update()` / `snapshot()`. Автозапись 4 типов отклонений — тонкими +best-effort врезками в существующие choke-point. Два-три HTTP-эндпоинта в `main.py`. Колонки +атрибуции — в схеме сразу, нуллабельные. **Конвейер (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/ +machine-verdict) и схемы существующих таблиц — байт-в-байт не тронуты; enduro не затронут.** + +### D1 — Таблица `lessons`: аддитивная, идемпотентная, forward-proof (BR-1, BR-2; AC-1, AC-2) + +`CREATE TABLE IF NOT EXISTS lessons (…)` в `db.init_db()` (паттерн `repo_freeze`): + +```sql +CREATE TABLE IF NOT EXISTS lessons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT, + lesson_type TEXT NOT NULL, -- slug-конвенция, НЕ enum-констрейнт + 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, + attribution TEXT, -- platform|project|both|unknown (NULLABLE) + target_repo TEXT, -- orchestrator|enduro-trails|… (NULLABLE) + target_domain TEXT, -- reliability|quality|economy|features|scale (NULLABLE) + 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); +CREATE INDEX IF NOT EXISTS idx_lessons_wi_type ON lessons (work_item_id, lesson_type); +``` + +**Инварианты:** +- Все три колонки **атрибуции создаются сразу и нуллабельны** (NFR-6, требование Славы 10.06): на + живой уже-существующей таблице добавляются через `_ensure_column(conn, "lessons", "", + "TEXT")` — forward-safe, restart-safe, без миграции данных. +- **Нет `enum`/`CHECK`-констрейнта** на `lesson_type`/`attribution`/`target_domain` — значения суть + конвенция строковых слагов (новый тип урока не требует миграции схемы; §6 допущений BRD). +- **Третий индекс `idx_lessons_wi_type`** добавлен сверх двух из TRZ — обслуживает дедуп-запрос + автозаписи (D4) одним indexed-lookup'ом (NFR-5). + +DDL-хелперы в `db.py` (стиль `coverage_baseline`): `record_lesson(...) -> int|None`, +`get_lessons(...) -> list[dict]`, `update_lesson(id, **fields) -> bool`, `lessons_snapshot() -> dict`. +Каждый открывает `get_db()` и закрывает в `finally`. + +### D2 — Observer-leaf `src/lessons.py`: scope **kill-switch only**, НЕ repo-gated (BR-3/4/5/6; NFR-1/2/7) + +Чистый leaf, импортирует только `config`+`db` (lazy `notifications` при необходимости); **никогда +не импортирует `stage_engine`/`merge_gate`/`launcher`** (анти-цикл). Публичный контракт: + +```python +def 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 +def get(*, lesson_type=None, status=None, repo=None, work_item_id=None, limit=None) -> list[dict] +def update(lesson_id, **fields) -> bool +def snapshot() -> dict +``` + +**Ключевое решение D2 — расхождение с шаблоном гейт-leaf'ов: журнал НЕ скоупится по repo.** +В отличие от `serial_gate`/`coverage_gate`/`bug_fast_track` (которые *действуют* на конкретный репо +и потому имеют пару `*_repos`), журнал — **observer-only**: запись строки никогда не влияет на +пайплайн ни одного репо. Поэтому: +- единственный регулятор — глобальный kill-switch `lessons_enabled` (env `ORCH_LESSONS_ENABLED`, + дефолт `True`); **`lessons_repos` НЕ вводится**; +- recorder пишет уроки про **любой** репо (включая enduro-trails) — урок про деградацию деплоя + enduro ценен для петли самообучения; репо-скоуп терял бы этот сигнал; +- `repo`-разрез — на уровне **выборки** (`get(repo=…)`, фильтр `snapshot()`), как зафиксировано в + §6 BRD «репо-скоуп через поле/фильтр выборки». +- **enduro не затронут (NFR-3):** запись observer-строки про enduro не меняет ни одной стадии/гейта + enduro — это чистая память орка. + +**never-raise (NFR-1, AC-6):** при `lessons_enabled is False` каждая функция — немедленный no-op +(`record→None`, `get→[]`, `update→False`, `snapshot→{}`) **без обращения к БД**. При `True` — тело в +`try/except Exception → logger.warning(...) + безопасный дефолт`. Журнал **не** деплоит, **не** +рестартит прод, **не** трогает `main`, **не** порождает процессов/сети (NFR-7). + +### D3 — Точки автозаписи: 4 детектора, тонкая врезка одним вызовом (BR-3; FR-3; AC-3) + +Каждая врезка = локальный импорт + один вызов `lessons.record(...)`, обёрнутый в защитный +`try/except` (паттерн post-deploy-freeze-врезки `stage_engine.py:~1993`), чтобы даже ошибка импорта +не пробилась в горячий путь: + +| Тип (`lesson_type`) | Choke-point | Контекст врезки | +|---|---|---| +| `gate_failure` | `stage_engine._handle_qg_failure_rollbacks` (после решения об откате на `development`) | `work_item_id, task_id, stage=current_stage, agent, repo, root_cause=reason, detail=qg_name` | +| `merge_hold` | `merge_gate._handle_merge_verify` (ветка HOLD, `result.note="merge-not-verified-hold"`) | `work_item_id, task_id, repo, stage="deploy", root_cause="merge-not-verified-hold"` | +| `transient_retry` | **budget-exhaustion**: `merge_gate` (merge-retry исчерпан) и/или `launcher._finalize_transient` (исчерпан `transient_attempts`) | `work_item_id?, repo, agent?, stage?, detail=<код/причина>` | +| `deploy_degraded` | `stage_engine` post-deploy `DEGRADED → set_repo_freeze` | `work_item_id, repo, stage="deploy", root_cause=reason, attribution="unknown", target_repo=repo, target_domain="reliability"` | + +Все врезки — `source="auto"`. Это **4 типа > минимума 2–3** (BR-3). `(г) deploy_degraded` (желаемый +по TRZ) включён как полноценный детектор: это урок слоя-3 «деплой OK / прод сломан» (ET-8), +ради которого Слава и потребовал атрибуцию. + +### D4 — Дедуп автозаписи: один indexed-SELECT в окне (BR-3; FR-3 «решение архитектора»; NFR-5) + +Риск: транзиент-ретраи/повторные откаты плодят дубли. Решение — **дешёвый дедуп только для +`source="auto"`** внутри `record()`: перед `INSERT` — один indexed-lookup +```sql +SELECT 1 FROM lessons +WHERE work_item_id = ? AND lesson_type = ? AND (stage IS ? OR ?) -- stage-match + AND created_at > datetime('now', ?) -- '- seconds' +LIMIT 1; +``` +по индексу `idx_lessons_wi_type` (D1). Найдено → no-op (`return None`, лог DEBUG). Окно — +`lessons_dedup_window_s` (env `ORCH_LESSONS_DEDUP_WINDOW_S`, дефолт `3600`). **`source="manual"` +дедуп НЕ проходит** (оператор/Стрим всегда может записать). Это один лёгкий `SELECT` (NFR-5), без +фоновых сканов. + +**Доп. контроль флуда на самом шумном детекторе:** `transient_retry` пишется **только на исчерпании +бюджета ретраев** (а не на каждом backoff) — это и есть ценный сигнал «транзиенты исчерпаны», а не +шум каждой попытки. Так флуд гасится в источнике до дедупа. + +### D5 — Эндпоинты `main.py`: read-only выборка + ручная запись/обновление (BR-4/5/6; FR-4/5; AC-4/5) + +Стиль `GET /queue` / `POST /coverage/baseline`, все never-raise, при выключенном флаге → +`{"enabled": false}`: +- **`GET /lessons`** — query `type/status/repo/work_item/limit` (дефолт `lessons_query_limit_default`, + напр. 100) → `{"enabled": bool, "lessons": [...]}`, всегда `200`, только чтение. +- **`POST /lessons`** — тело JSON, `lesson_type` обязателен → `lessons.record(..., source="manual")` + → `{"id": }`. +- **`POST /lessons/{id}`** — `lessons.update(id, status=…, attribution=…, target_repo=…, + target_domain=…, related_task=…, root_cause=…, suggestion=…)` → `{"ok": bool}`; стампит + `updated_at=datetime('now')`. Позволяет ретроспективщику/человеку доклассифицировать + автозаписанный `unknown`. +- **`GET /queue`** — добавить read-only ключ `"lessons": lessons.snapshot()` рядом с + `serial_gate`/`coverage`. `snapshot()` — лёгкие `GROUP BY`-счётчики (по типу/статусу) + последние + N. Существующие ключи `/queue` и эндпоинты `/health|/status|/metrics` — **байт-в-байт прежние**. + +### D6 — Изоляция от конвейера и гейтов (NFR-3; AC-8) + +`STAGE_TRANSITIONS`, реестр `QG_CHECKS`, функции `check_*`, machine-verdict-ключи +(`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/`coverage_status:`) и +схемы существующих таблиц — **диффом не затрагиваются**. Журнал — наблюдатель, **не** Quality Gate; +он не участвует в решении о продвижении по стадиям. Никаких новых/изменённых QG-checks (FR-6). + +## Альтернативы + +- **Repo-скоуп `lessons_repos` (как у гейтов)** — отвергнуто: журнал observer-only, не действует на + репо; скоуп терял бы ценные enduro-уроки. Скоуп — на выборке (D2). +- **Без дедупа в v1 (TRZ это допускает)** — отвергнуто как дефолт: транзиент-ретраи реально + флудят таблицу; дешёвый indexed-дедуп (D4) дешевле, чем последующая чистка. Бюджет-exhaustion + + окно дают двойную защиту при одном `SELECT`. +- **Запись `transient_retry` на каждом backoff** — отвергнуто: шум; ценен факт исчерпания бюджета. +- **Отдельная БД/файл для журнала** — отвергнуто: лишняя зависимость; общая SQLite-БД с аддитивной + таблицей соответствует принципу «минимум зависимостей» и паттерну `repo_freeze`/`coverage_baseline`. +- **Фоновый агрегатор/ретенция-крон в v1** — отвергнуто: NFR-5 (без фоновых потоков/сканов); + ретенция — будущая задача (см. `10-tech-risks.md` TR-2). +- **ORM** — отвергнуто: raw SQL достаточно (принцип «без ORM, если хватает raw SQL»). + +## Последствия + +- **+** Уроки становятся машиночитаемыми — фундамент для E2/E3/Стрим; атрибуция forward-proof + (колонки сразу, переделки живой БД не будет). +- **+** Нулевая регрессия: kill-switch + never-raise + чистая аддитивность; enduro не затронут; + конвейер байт-в-байт прежний. +- **+** Следует проверенному additive-observer-leaf шаблону (`serial_gate`/`coverage_gate`/`metrics`/ + `cancel`/`bug_fast_track`) — низкий архитектурный риск, не требует `arch:major-change` (см. + `10-tech-risks.md` сводный вывод). +- **−** Рост таблицы со временем (автозапись на отклонениях). Митигейшн: лёгкие строки + дедуп (D4) + + budget-exhaustion-only для транзиентов; ретенция — TR-2 (будущее). +- **−** Лёгкое усложнение `record()` дедуп-запросом. Митигейшн: один indexed-SELECT, только для + `auto`, под окном; для `manual` пропускается. +- **Откат:** `ORCH_LESSONS_ENABLED=false` → весь функционал инертен мгновенно (no-op, нулевая + регрессия). Полный откат — revert диффа; таблица `lessons` остаётся пустой/неиспользуемой, + существующих таблиц не касается. + +## Ссылки +- BRD: `docs/work-items/ORCH-098/01-brd.md` +- TRZ: `docs/work-items/ORCH-098/02-trz.md` +- Acceptance: `docs/work-items/ORCH-098/03-acceptance-criteria.md` +- Data: `docs/work-items/ORCH-098/08-data-requirements.md` +- Infra: `docs/work-items/ORCH-098/07-infra-requirements.md` +- Risks: `docs/work-items/ORCH-098/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0033-lessons-journal.md` +- Сверено по коду: `src/serial_gate.py`, `src/coverage_gate.py`, `src/metrics.py`, `src/db.py:251,341`, + `src/stage_engine.py:728,~1993`, `src/merge_gate.py:811,1588`, `src/agents/launcher.py:997`, + `src/main.py` (`GET /queue`, `POST /coverage/baseline`), `src/qg/checks.py:520`. diff --git a/docs/work-items/ORCH-098/07-infra-requirements.md b/docs/work-items/ORCH-098/07-infra-requirements.md new file mode 100644 index 0000000..9251083 --- /dev/null +++ b/docs/work-items/ORCH-098/07-infra-requirements.md @@ -0,0 +1,45 @@ +--- +work_item: ORCH-098 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 07 — Инфра-требования: ORCH-098 — машинный журнал уроков `lessons` + +Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable. Топология **не меняется**; файл создан для аудитопригодности (новая env-переменная). + +## I-1. Топология / окружения +**N/A.** Новых контейнеров/портов/сети/томов нет. Таблица `lessons` живёт в существующей общей +SQLite-БД (тот же том `./data`), эндпоинты обслуживаются текущим процессом `orchestrator` (8500) / +`orchestrator-staging` (8501). Принцип «всё в Docker на одном сервере mva154» — соблюдён. + +## I-2. Переменные окружения / секреты +Новые env (pydantic `BaseSettings`, авто-биндинг `ORCH_*`), все с безопасными дефолтами: + +| Env | Дефолт | Назначение | +|---|---|---| +| `ORCH_LESSONS_ENABLED` | `true` | kill-switch журнала (NFR-2); `false` → полная инертность | +| `ORCH_LESSONS_DEDUP_WINDOW_S` | `3600` | окно дедупа автозаписи (ADR-001 D4) | +| `ORCH_LESSONS_QUERY_LIMIT_DEFAULT` | `100` | дефолтный `limit` для `GET /lessons` | + +**`lessons_repos` СОЗНАТЕЛЬНО не вводится** — журнал observer-only и не скоупится по репо +(ADR-001 D2). Секретов нет. `.env.example` дополнить тремя ключами для документируемости (значения — +дефолтные, не секреты). + +## I-3. Деплой / рестарт +- Изменение применяется штатным конвейером: **обязательный staging-гейт (8501) перед прод-деплоем** + орка (self-hosting инвариант). Прод-контейнер **не рестартить вне процедуры деплоя стадии** + `deploy`/`Confirm Deploy` (ORCH-059) — конвейер всех проектов встанет. +- Таблица `lessons` создаётся идемпотентно при старте (`init_db()`) — на первом штатном запуске + нового образа, **без отдельной ручной миграции** (restart-safe, NFR-4). На живой БД enduro не + затронут. +- Откат — `ORCH_LESSONS_ENABLED=false` (мгновенная инертность) либо revert образа. + +## I-4. CI/CD +**Без изменений** в `.gitea/workflows/`. Новые тесты `tests/test_lessons.py` исполняются штатным +шагом `pytest tests/ -q`. Новых системных/pip-зависимостей нет (raw SQL на stdlib `sqlite3`). diff --git a/docs/work-items/ORCH-098/08-data-requirements.md b/docs/work-items/ORCH-098/08-data-requirements.md new file mode 100644 index 0000000..857e3f8 --- /dev/null +++ b/docs/work-items/ORCH-098/08-data-requirements.md @@ -0,0 +1,76 @@ +--- +work_item: ORCH-098 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 08 — Требования к данным: ORCH-098 — машинный журнал уроков `lessons` + +Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: architecture + +> When-applicable: задача **добавляет** одну таблицу на общую прод-БД. Схемы существующих таблиц — +> не затрагиваются. + +## Изменения схемы БД + +**Новая аддитивная таблица `lessons`** + три индекса, создаются идемпотентно в `db.init_db()` +(`CREATE TABLE IF NOT EXISTS` / `CREATE INDEX IF NOT EXISTS`), restart-safe (паттерн `repo_freeze`, +`coverage_baseline`). На уже существующей таблице новые/будущие колонки добавляются через +`_ensure_column(conn, "lessons", "", "")` (`src/db.py:341`) — forward-safe, без миграции +данных. DDL — см. ADR-001 D1. + +Существующие таблицы (`tasks`/`jobs`/`agent_runs`/`events`/`job_deps`/`repo_freeze`/ +`coverage_baseline`/`tracker_messages`) — **байт-в-байт не тронуты** (NFR-3, AC-8). + +## Новые/изменённые сущности + +Сущность **`lesson`** — одна запись структурированного отклонения конвейера. Колонки: + +| Колонка | Тип | Null | Назначение | +|---|---|---|---| +| `id` | INTEGER PK AUTOINCREMENT | — | суррогатный ключ | +| `created_at` | TEXT `DEFAULT datetime('now')` | NOT NULL | момент записи | +| `updated_at` | TEXT | NULL | момент последнего `update` | +| `lesson_type` | TEXT | NOT NULL | slug-тип (`gate_failure`/`merge_hold`/`transient_retry`/`deploy_degraded`/…) | +| `work_item_id` | TEXT | NULL | контекст: задача (`ORCH-NNN`/`ET-NNN`) | +| `task_id` | INTEGER | NULL | контекст: внутренний id задачи | +| `stage` | TEXT | NULL | контекст: стадия конвейера | +| `agent` | TEXT | NULL | контекст: агент-роль | +| `repo` | TEXT | NULL | контекст: репозиторий, **разрез выборки** | +| `root_cause` | TEXT | NULL | анализ: корневая причина (если известна) | +| `suggestion` | TEXT | NULL | анализ: предложенное улучшение (если есть) | +| `status` | TEXT `DEFAULT 'new'` | NOT NULL | `new`/`in_progress`/`closed`/`linked` | +| `related_task` | TEXT | NULL | связанная заведённая задача | +| `attribution` | TEXT | **NULL** | **АТРИБУЦИЯ:** `platform`/`project`/`both`/`unknown` | +| `target_repo` | TEXT | **NULL** | **АТРИБУЦИЯ:** кого касается улучшение | +| `target_domain` | TEXT | **NULL** | **АТРИБУЦИЯ:** `reliability`/`quality`/`economy`/`features`/`scale` | +| `source` | TEXT | NULL | `auto` (детектор) / `manual` (оператор/Стрим) | +| `detail` | TEXT | NULL | свободный JSON/текст — payload детектора | + +**Инварианты данных:** +- Три колонки **атрибуции** (`attribution`/`target_repo`/`target_domain`) присутствуют в исходной + схеме, **нуллабельны** (требование Славы 10.06, NFR-6, AC-2) — при автозаписи допустимо + пусто/`unknown`; проставляются позже через `update` (AC-5). +- **Без `enum`/`CHECK`-констрейнтов** — значения `lesson_type`/`attribution`/`target_domain` суть + конвенция строковых слагов (forward-compatible: новый тип не требует миграции). +- Индексы: `idx_lessons_type_status (lesson_type, status)` — выборка/snapshot; `idx_lessons_repo + (repo)` — репо-разрез; `idx_lessons_wi_type (work_item_id, lesson_type)` — дедуп автозаписи + (ADR-001 D4). + +## Совместимость данных / миграции + +- **Аддитивно / идемпотентно / restart-safe:** только новая таблица + индексы; повторный `init_db()` + не падает и не дублирует (NFR-4). +- **Общая прод-БД (self-hosting):** таблица создаётся на том же файле БД, что обслуживает + orchestrator и enduro-trails. Уроки про любой репо хранятся в одной таблице; **изоляция enduro** — + таблица аддитивна и не участвует в пайплайне enduro (NFR-3); репо-разрез — поле `repo` + фильтр + выборки (ADR-001 D2). +- **Объём строки** — короткие текстовые поля; `detail` — компактный payload. Запись — один `INSERT`, + чтение — простой параметризованный `SELECT … ORDER BY id DESC LIMIT ?` (NFR-5; общий хост впритык: + RAM/диск). +- **Ретенция / архивация** — вне объёма v1; тренд роста и будущая стратегия — `10-tech-risks.md` + (TR-2). +- **Миграция исторических уроков из `memory/`** — вне объёма (BRD §2). diff --git a/docs/work-items/ORCH-098/10-tech-risks.md b/docs/work-items/ORCH-098/10-tech-risks.md new file mode 100644 index 0000000..8694c7c --- /dev/null +++ b/docs/work-items/ORCH-098/10-tech-risks.md @@ -0,0 +1,39 @@ +--- +work_item: ORCH-098 +stage: architecture +author_agent: architect +status: proposed +created_at: 2026-06-10 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-098 — машинный журнал уроков `lessons` + +Work Item: **ORCH-098** · Repo: **orchestrator** · Стадия: architecture + +> Информационный (гейтом не парсится). Риски реализации и их митигейшн. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | Врезка детектора в горячий путь конвейера (`stage_engine`/`merge_gate`/`launcher`) бросает исключение → регрессия пайплайна на self-hosting прод-инстансе (встанет конвейер всех проектов, в т.ч. enduro). | Низ. | Выс. | **NFR-1 never-raise**: `lessons.record` полностью self-contained `try/except → None`; каждая врезка дополнительно обёрнута защитным `try/except` (паттерн post-deploy-freeze, `stage_engine.py:~1993`), ловит даже ошибку импорта. **NFR-2 kill-switch** `ORCH_LESSONS_ENABLED=false` → no-op. Юнит-тест с замоканной падающей БД (AC-6). | +| TR-2 | Неограниченный рост таблицы `lessons` (автозапись на каждом откате/HOLD/деградации) на впритык-хосте (диск 92%). | Сред. | Низ. | Лёгкие строки (короткий текст); **дедуп D4** (один indexed-SELECT в окне) + **`transient_retry` только на budget-exhaustion** гасят флуд в источнике. Ретенция/архивация — отдельная будущая задача (вне объёма v1); тренд наблюдаем через `snapshot()` в `GET /queue`. | +| TR-3 | Недооформленная схема атрибуции → переделка схемы на живой общей прод-БД, когда появится ретроспировщик (E2). | Низ. | Сред. | **BR-2/NFR-6**: три нуллабельные колонки атрибуции (`attribution`/`target_repo`/`target_domain`) в схеме **сразу**; `update`/`POST /lessons/{id}` позволяет доклассифицировать `unknown` позже без миграции. Слаги без `enum`-констрейнта → новые значения не требуют DDL. | +| TR-4 | Дубли автозаписи на ретраях/повторных откатах искажают будущий pattern-анализ. | Сред. | Низ. | **Дедуп D4** для `source="auto"`: indexed `SELECT` по `idx_lessons_wi_type` в окне `ORCH_LESSONS_DEDUP_WINDOW_S` перед `INSERT`. `manual` дедуп не проходит. Если в реальном прогоне дедуп окажется слишком строгим/слабым — окно конфигурируемо без передеплоя логики. | +| TR-5 | Случайное касание инвариантов конвейера (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схемы существующих таблиц) при врезках. | Низ. | Выс. | Врезки — строго аддитивные одиночные вызовы; **AC-8** требует «диффом не затронуты». Reviewer проверяет дифф перечисленных артефактов. Журнал не участвует в решении гейта (FR-6). | +| TR-6 | Эндпоинт `POST /lessons`/`/lessons/{id}` как непреднамеренный мутатор/вектор (запись в прод-БД без аутентификации). | Низ. | Сред. | Пишет **только** в аддитивную таблицу `lessons` (не трогает `tasks`/`jobs`/гейты); never-raise; `enabled:false` при выключенном флаге. Тот же уровень доступа, что у существующего `POST /coverage/baseline`. Дальнейшее ужесточение доступа — общая инфра-тема, вне объёма ORCH-098. | + +## Сводный вывод + +Доминирующий класс рисков — **изоляция наблюдателя от горячего пути конвейера на self-hosting +прод-инстансе** (TR-1, TR-5): высокое влияние при низкой вероятности, полностью покрыто +проверенной связкой *never-raise + kill-switch + чистая аддитивность*, идентичной уже работающим +leaf'ам (`serial_gate`/`coverage_gate`/`metrics`/`bug_fast_track`). Вторичный класс — **рост/шум +данных** (TR-2/TR-4): низкое влияние, смягчён лёгкими строками, дедупом и budget-exhaustion-записью; +ретенция вынесена в будущее. + +**Эскалация не требуется.** Несмотря на формально «новый компонент + новая таблица», изменение +следует устоявшемуся **additive-observer-leaf** шаблону, **не трогает машину стадий, гейты и схемы +существующих таблиц**, полностью обратимо флагом → метка `arch:major-change` **не выставляется**, +возврат в анализ (`back-to:analysis`) не нужен. Остаточный риск для прод-конвейера — **низкий**.