architect(ET): auto-commit from architect run_id=574
All checks were successful
CI / test (push) Successful in 48s
All checks were successful
CI / test (push) Successful in 48s
This commit is contained in:
@@ -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`, `<repo>/.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-0033](adr/adr-0033-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`.
|
||||
|
||||
## Сырьё-эндпоинт `/metrics` для sidecar (ORCH-099 — design)
|
||||
|
||||
|
||||
92
docs/architecture/adr/adr-0033-lessons-journal.md
Normal file
92
docs/architecture/adr/adr-0033-lessons-journal.md
Normal file
@@ -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`.
|
||||
244
docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md
Normal file
244
docs/work-items/ORCH-098/06-adr/ADR-001-lessons-journal.md
Normal file
@@ -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", "<col>",
|
||||
"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', ?) -- '-<window> 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": <int>}`.
|
||||
- **`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`.
|
||||
45
docs/work-items/ORCH-098/07-infra-requirements.md
Normal file
45
docs/work-items/ORCH-098/07-infra-requirements.md
Normal file
@@ -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`).
|
||||
76
docs/work-items/ORCH-098/08-data-requirements.md
Normal file
76
docs/work-items/ORCH-098/08-data-requirements.md
Normal file
@@ -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", "<col>", "<decl>")` (`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).
|
||||
39
docs/work-items/ORCH-098/10-tech-risks.md
Normal file
39
docs/work-items/ORCH-098/10-tech-risks.md
Normal file
@@ -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`) не нужен. Остаточный риск для прод-конвейера — **низкий**.
|
||||
Reference in New Issue
Block a user