diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 647c840..848b8ed 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -416,6 +416,34 @@ ORCH-079 синхронизирует витрину с кодом и закры - ADR: [adr-0023](adr/adr-0023-overview-docs-reviewer-axis-and-epic52-close.md); детально — `docs/work-items/ORCH-079/06-adr/ADR-001-readme-sync-and-reviewer-overview-docs-axis.md`. +#### Карта LLM-консультаций + политика использования LLM (ORCH-118 — design) +Зонтичный follow-up RCA-трека ORCH-114/117: оркестратор не имел нормативного критерия «где LLM нужен, +а где это avoidable control path» и карты мест вызова LLM, прибитой к коду. ORCH-118 — **inventory + +карта + roadmap + политика + структурные тесты** (реализация детерминированных раннеров — follow-up'ы +**по роли**, без выдуманных Plane-ID). Это **docs + tests only**: `STAGE_TRANSITIONS` / реестр и имена +`QG_CHECKS`/`check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; kill-switch не +нужен (нет рантайм-поведения), как ORCH-077/079/101/102/103/011. +- **Три ортогональных оси (ground-truth — код):** (1) consultation ≠ transport/slot (единственный + транспорт LLM-консультации в `src/**` — `launcher._spawn`, `launcher.py:472/610-614`; иного нет; + D1/D2 `deploy-finalizer`/`post-deploy-monitor` занимают слот, но перехватываются в `launch_job` до + `_spawn`, `launcher.py:389/394` — консультации нет); (2) **control-path (C) ≠ artifact-producer (P)** + по коду-потребителю в `src/qg/checks.py` (C: `check_*` ветвится на LLM-вердикте; P: детерминированный + гейт судит артефакт независимо — файлы/CI); (3) деривируемость вердикта из tool-сигналов. +- **Нормативное определение** «avoidable LLM control path» = двухбитный предикат: C-консультация **И** + вердикт деривируем из tool-сигналов. Целевой набор (поимённо, доказательно): **avoidable = + {tester, deployer}**; control-path-но-keep = `{reviewer}`; не-control-path (P, keep) = + `{analyst, architect, developer}`; уже детерминированы = `{deploy-finalizer, post-deploy-monitor}`. +- **Документы (durable, `docs/architecture/`):** `llm-call-sites.md` (карта + control-path-разметка + + классификация, снимок, прибитый тестами), `llm-determinization-roadmap.md` (порядок замен; первый + срез — **deployer staging-status**, чистый маппинг exit-кода `staging_check.py`; прод уже + детерминирован Phase A/B/C ORCH-036), `llm-usage-policy.md` (нормативный принцип «LLM — только где + нужно настоящее суждение»). Анти-дрейф — `tests/test_llm_call_site_inventory.py` (offline; включая + control-path-инвариант сверки с `src/qg/checks.py` и фиксацию avoidable-набора). +- **Норматив сопровождения:** менял места вызова LLM **или** потребителя вердикта в `src/qg/checks.py` + → обнови карту/разметку и политику в том же PR. +- ADR: [adr-0047](adr/adr-0047-llm-usage-policy-and-call-site-map.md); детально — + `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`. + ### Модель и эффорт по ролям (ORCH-41, валидация ORCH-74) Модель и `--effort` каждого агента берутся из config (`src/config.py`), резолвятся `launcher.resolve_agent_model` / `resolve_agent_effort` по приоритету **project-override (`projects_json` `agent_models`/`agent_efforts`) > `ORCH_AGENT_MODEL_`/`ORCH_AGENT_EFFORT_` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_` из `config.py` (его пустой env перебить не может). Floor — строго последний уровень (ниже default) и срабатывает ТОЛЬКО когда все уровни пусты, поэтому пустые прод-`ORCH_AGENT_EFFORT_*=` (которые pydantic трактует как явное `''` и обнуляют дефолт) больше не приводят к запуску без `--effort`: каждая роль получает свой канонический пол (developer=`xhigh`, tester/deployer=`medium`, прочие=`high`). Непустой явный конфиг по-прежнему побеждает floor; опечатка вне `VALID_EFFORTS` дропается валидацией ДО floor (never-break, не маскируется). См. `docs/work-items/ORCH-081/06-adr/ADR-001-effort-resolution-floor.md`. frontmatter `model:` в `.openclaw/agents/*.md` **удалён** (ORCH-74 G1) — он был мёртвой/лживой декларацией (launcher его не читает); config — единственный источник правды о модели. Model-routing (G3) НЕ включён — все 6 агентов на `claude-opus-4-8`. diff --git a/docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md b/docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md new file mode 100644 index 0000000..d6b1fc6 --- /dev/null +++ b/docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md @@ -0,0 +1,114 @@ +--- +work_item: ORCH-118 +stage: architecture +author_agent: architect +status: accepted +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# adr-0047: Нормативная политика использования LLM + карта call-site'ов (control-path-ось «avoidable») + +> **Сквозной (cross-cutting) ADR.** Агрегирует решение ORCH-118, влияющее на **весь** оркестратор: +> нормативная политика использования LLM, три ортогональных оси, определение «avoidable LLM control +> path» и снимок-карта LLM-консультаций, прибитая к коду структурными тестами. Локальная детализация — +> `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`. + +## Статус +Accepted + +## Контекст + +RCA-цепочка ORCH-114/117 (и 110/111/112/113) показала корневой класс: у side-effectful и решающих +control-path'ов не было единого детерминированного владения; местами решение брал LLM-агент «потому +что удобно», хотя по сути это исполнение фиксированных команд + маппинг результата — лишний +недетерминизм, задержка и расход токенов в точке ветвления. + +Оркестратор не имел **нормативного критерия** «где LLM нужен, а где это avoidable control path» и +**карты** мест вызова LLM, прибитой к коду. Без них любая будущая правка control-path'а могла снова +ввести LLM «на удобстве», а «вслепую» убирать LLM нельзя — часть путей несёт настоящее суждение +(анализ, архитектура, написание кода, ревью). + +**Ground-truth кода (ORCH-118, сверено):** единственный транспорт LLM-консультации в `src/**` — +`launcher._spawn` (`launcher.py:472`, CLI `610-614`); иного LLM-транспорта нет (нет SDK-импортов / +прямого HTTP Anthropic / второго сборщика). 6 ролей-агентов консультируют через него; D1/D2 +(`deploy-finalizer`/`post-deploy-monitor`) перехватываются в `launch_job` **до** `_spawn` +(`launcher.py:389/394`) — слот есть, консультации нет. Потребитель вывода каждой роли — конкретный +`check_*`/`_parse_*` в `src/qg/checks.py`. + +## Решение + +### D1 — Три ортогональных оси (нормативно для всего оркестратора) + +1. **consultation ≠ transport/slot** — «потребляет суждение LLM» ≠ «спавнит процесс / занимает слот + агента» (capability ≠ consultation). +2. **control-path (C) ≠ artifact-producer (P)** — определяется кодом-потребителем: C — `check_*` + ветвится на machine-verdict, написанном LLM; P — детерминированный гейт судит артефакт независимо + (файлы/CI). +3. **деривируемость вердикта** — вердикт C-консультации либо детерминированная функция tool-сигналов + (exit-code `pytest`/smoke/`staging_check.py`/деплоя), либо настоящее суждение. + +### D2 — Нормативное определение «avoidable LLM control path» + +> Call-site — **avoidable LLM control path** ⟺ **(i)** C-консультация (LLM-вердикт потребляется +> потоком управления) **И (ii)** вердикт деривируем из tool-сигналов, которые оркестратор уже +> вычисляет → LLM не добавляет информации. + +Целевой набор (доказательно из `src/qg/checks.py`): **avoidable = {tester, deployer}**; +control-path-но-keep = `{reviewer}`; не-control-path (P, keep) = `{analyst, architect, developer}`; +уже детерминированы (вне консультаций) = `{deploy-finalizer, post-deploy-monitor}`. + +### D3 — Нормативная политика использования LLM (`docs/architecture/llm-usage-policy.md`) + +Принцип: **«LLM — только там, где требуется настоящее суждение».** Критерий keep vs replace — +через оси D1 (является ли путь control path; деривируем ли вердикт; обратимость; влияние на +автономность NFR-2). **Требование:** любая новая/изменённая control-path-консультация обязана +обосновать использование LLM против этой политики; reviewer контролирует это как обзорную ось +(в духе ORCH-079) — **как требование, не как новый машинный гейт**. + +### D4 — Карта как снимок, прибитый к коду + +`docs/architecture/llm-call-sites.md` — инвентарь + control-path-разметка + классификация со +схемой полей и машинным блоком (детали — work-item ADR-001 D2/D4). Структурные тесты +`tests/test_llm_call_site_inventory.py` (offline) держат инварианты: транспорт-агностичный +двусторонний инвариант единственной точки, отсутствие консультации в детерминированных путях, +control-path-разметка сверена с `src/qg/checks.py`, avoidable-набор = `{tester, deployer}`. + +### D5 — Roadmap детерминизации (`docs/architecture/llm-determinization-roadmap.md`) + +Рекомендованный первый срез — **deployer (staging-status)** (`replace-deterministic-now`: чистый +маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C ORCH-036; опора на +прецедент D1/D2). Затем — **tester-гибрид** (`needs-hybrid-fallback`). Кандидаты — **по роли**, +без конкретных Plane-ID (NFR-6). + +### D6 — Скоуп и инварианты (нормативно) + +ORCH-118 — **docs + tests only**: `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` / +machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен не реализуются; +follow-up Plane-ID не фиксируются. Self-hosting-безопасно (только чтение кода + запись docs/tests). + +**Норматив сопровождения (durable):** менял места вызова LLM **или** потребителя вердикта в +`src/qg/checks.py` → обнови карту/разметку и политику в **том же PR** (иначе тесты D4 красные). + +## Альтернативы +- **Машинный гейт-enforcement политики (новый QG)** — отвергнуто: политика нормативно-описательная, + как ось трассировки ORCH-078; новый QG увеличил бы поверхность риска без необходимости (FR-6 §QG). +- **Реализация раннеров в этой же задаче** — отвергнуто: inventory-first по требованию заказчика; + «вслепую» убирать LLM рискованно без утверждённой карты. +- **Привязка к конкретным follow-up ID** — отвергнуто (NFR-6, корень отклонённой R2). + +## Последствия +- **+** Единый нормативный критерий и код-привязанная карта закрывают класс «LLM на удобстве» и + делают замены предсказуемыми; автономность защищена политикой. +- **−** Карта — снимок: эволюция `src/qg/checks.py` требует со-обновления карты (держится тестами). + *Митигейшн:* запланированный норматив сопровождения, тест указывает точку дрейфа. +- **Откат:** удаление/правка `docs/architecture/llm-*.md` + тест-файла + секции README; рантайм не + затронут. + +## Ссылки +- Work-item ADR: `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md` +- BRD/TRZ/AC: `docs/work-items/ORCH-118/{01-brd,02-trz,03-acceptance-criteria}.md` +- Сверено по коду: `src/agents/launcher.py`, `src/qg/checks.py`, `.openclaw/agents/*.md` +- Связанные: ORCH-036 (детерминированный self-deploy), ORCH-061 (`staging_verdict`), + ORCH-077/079 (docs/prompts-only прецедент + reviewer-ось обзорных доков), ORCH-114/117 (RCA-трек) + diff --git a/docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md b/docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md new file mode 100644 index 0000000..8cacd28 --- /dev/null +++ b/docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md @@ -0,0 +1,295 @@ +--- +work_item: ORCH-118 +stage: architecture +author_agent: architect +status: accepted +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# ADR-001: Карта LLM-консультаций, control-path-ось «avoidable» и roadmap детерминизации + +Work Item: **ORCH-118** — replace avoidable LLM control paths with deterministic implementations +Стадия: **architecture** +Сквозная регистрация: **`docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`** +(решение кросс-каттинговое — вводит нормативную политику использования LLM для всего оркестратора и +снимок-карту, прибитую к коду тестами). + +## Статус +Accepted + +## Контекст + +ORCH-118 — **зонтичная inventory/architecture-задача** (RCA-трек ORCH-114/117 и предшественники +110/111/112/113): корневым классом инцидентов было отсутствие **единого детерминированного владения** +у side-effectful и решающих control-path'ов — местами решение принималось LLM-агентом «потому что +удобно», хотя по сути это исполнение фиксированных команд и маппинг результата. Задача **не** +реализует детерминированные раннеры (это follow-up'ы); её выход — **доказательная карта** всех мест +вызова LLM + классификация + roadmap + нормативная политика, защищённые структурными тестами. + +ТЗ (02-trz) оставляет **архитектору** решить «как»: структуру/размещение/формат документов карты, +схему классификации, дизайн структурных тестов, рекомендованный первый срез. Этот ADR это фиксирует. + +**Факты, сверенные с кодом на момент задачи (ground-truth):** + +- **Единственный транспорт LLM-консультации в `src/**`** — `src/agents/launcher.py::_spawn` (`def` — + `launcher.py:472`; сборка CLI `f'{self.CLAUDE_BIN} --print … --system-prompt "$(cat {system_prompt})"'` + — `launcher.py:610-614`; парс токенов из CLI-JSON — `_monitor_agent`, `launcher.py:838`). Им + пользуются ровно **6 ролей** (`.openclaw/agents/{analyst,architect,developer,reviewer,tester, + deployer}.md` — подтверждено `ls .openclaw/agents/`). **Иного LLM-транспорта нет:** + `grep -rnE "import (anthropic|openai)|api\.anthropic\.com|/v1/messages" src/ watchdog/` → пусто; + `CLAUDE_BIN` вне `_spawn` встречается только в `src/preflight.py` (проверка `os.path.exists`, **не** + инференс) и `src/config.py:65` (литерал дефолта пути). Это критично для дизайна теста (D5). +- **Потребитель вывода каждой роли** (`src/qg/checks.py`, все `file:line` резолвятся): + `check_analysis_complete:33` (наличие файлов), `check_architecture_done:62` (наличие 06-adr/07), + `check_ci_green:82` + `check_branch_mergeable:657` (CI/merge), `check_reviewer_verdict:336` + (`verdict:`), `check_tests_passed:182` → `_parse_tests_verdict:226` (`result:`), + `check_staging_status:599` → `_parse_staging_status:538` (`staging_status:`), + `check_deploy_status:473` → `_parse_deploy_status:413` (`deploy_status:`). +- **Перехват D1/D2 до `_spawn`:** `launch_job:377` возвращает рано для `agent=="deploy-finalizer"` + (`launcher.py:389`) и `agent=="post-deploy-monitor"` (`launcher.py:394`) — код прямо помечает «Not an + LLM spawn» (`launcher.py:407,428`). Слот агента занят, но консультации LLM нет — **рабочий прецедент + детерминированной замены агента**. +- **Не-агентские control-path'ы уже детерминированы** (LLM-консультации не несут — подтверждено + наличием модулей): `src/{stages,stage_engine,staging_verdict,self_deploy,error_classifier, + frontmatter,serial_gate,merge_gate,coverage_gate,security_gate,post_deploy,transition_lease, + reconciler,job_reaper}.py`. Их subprocess-вызовы (`git`/`pytest`/`docker`/`ssh`/сканеры) — + детерминированные **инструменты**, а не LLM. + +Без архитектурной фиксации «как» развести **три ортогональных факта** (транспорт/слот ≠ консультация +≠ control-path) и без нормативного определения «avoidable LLM control path» карта осталась бы +субъективной, а тесты — тривиальными (корень R4/R5-блокеров BRD). + +## Решение + +### Сводка + +Фиксируем: (D1) набор и размещение durable-документов; (D2) схему записи инвентаря; (D3) три +ортогональных оси и **нормативное определение** «avoidable LLM control path»; (D4) таксономию и +правило **вывода** класса из осей с поимённой канонической таблицей ролей (= «фиксация карты»); +(D5) дизайн структурных анти-дрейф тестов; (D6) рекомендованный первый срез roadmap'а; (D7) +скоуп-гард. ORCH-118 — **docs + tests only**: `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/ +`check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен **не** +реализуются; конкретные follow-up Plane-ID **не** фиксируются (NFR-6). + +### D1 — Набор и размещение документов (BR-1/BR-4/BR-5; FR-1/FR-4/FR-5; AC-1/AC-4/AC-5) + +Три durable-документа размещаются в **`docs/architecture/`** (сквозные, переживают задачу, читаются +будущими follow-up'ами) — НЕ только в `docs/work-items/ORCH-118/`: + +| Файл | Роль | Жизненный цикл | +|------|------|----------------| +| `docs/architecture/llm-call-sites.md` | **Карта** call-site'ов: инвентарь + control-path-разметка + классификация (D2/D3/D4) | **Снимок**, прибит тестами (D5); обновляется при дрейфе кода | +| `docs/architecture/llm-determinization-roadmap.md` | **Roadmap** замен: порядок, экономия, риски, первый срез (D6) | Транзиентный план; обновляется по мере закрытия follow-up'ов | +| `docs/architecture/llm-usage-policy.md` | **Политика**: принцип + критерии keep/replace через ось §0-bis + **определение «avoidable LLM control path»** (D3) | Нормативный durable-документ | + +**Решение о разделении на 3 файла** (а не один): у них разные аудитория и жизненный цикл — карта +машинно-сверяется и есть снимок; roadmap транзиентен; политика нормативна и стабильна. Слияние +размыло бы тестируемость карты и стабильность политики. + +> **Авторство.** Содержательное «как» (структура, поля, оси, классификация, дизайн тестов, первый +> срез) фиксирует **этот ADR** (он и есть «фиксация карты» по TRZ §2). Физическое создание трёх +> `docs/architecture/llm-*.md` + тест-файла + синхронизация golden-source (README/overview/CHANGELOG) +> — деливерабл **стадии development** строго по этому ADR. Канонические таблицы D3/D4 ниже — +> **source of truth**, которую развёрнутые документы и тесты **зеркалят** без расхождений. + +### D2 — Схема записи инвентаря (BR-1/BR-8; FR-1/FR-8; AC-1) + +Каждая строка карты несёт **обязательные поля** (порядок — нормативный, тест D5 проверяет наличие): + +`id` · `location (file:line)` · `trigger` · `stage/owner` · `output artifact` · `machine-verdict key` +(если есть) · **`output consumer`** (`check_*`/`_parse_*` с `file:line` — кто потребляет вывод роли) · +`est. tokens/runtime` (источник — `agent_runs`, помечено «оценка») · **`consults-LLM`** (consultation +vs LLM-capable transport/slot, §0.3 BRD) · **`axis`** (C control-path / P artifact-producer, §0-bis) · +`classification` (D4) · `rationale` · `dependency` · `risk`. + +Каждый `file:line` **обязан резолвиться** в реальный код (тест D5 точечно проверяет ключевые якоря). + +**Машинно-читаемый якорь для тестов.** Карта несёт в `llm-call-sites.md` **канонический +markdown-блок** со стабильным заголовком таблицы (колонки `id | role | location | output_consumer | +consults_llm | axis | avoidable | classification`). Тест D5 парсит этот блок (split по `|`, без новых +зависимостей) и сверяет с кодом. Это держит документ человекочитаемым и одновременно +машинно-проверяемым (вместо хрупкого regex по прозе). + +### D3 — Три ортогональных оси и нормативное определение «avoidable LLM control path» (BR-8/BR-9; FR-8; AC-10; NFR-7) + +Карта/политика **явно** вводят три раздельных факта (их смешение — корень R3→R5-блокеров): + +1. **Ось 1 — consultation ≠ transport/slot.** «LLM-консультация» = точка, где решение/артефакт + конвейера **потребляет суждение LLM**. Транспорт (`_spawn`) — реализация, не определение. Слот + агента (D1/D2 job-роли) делает site LLM-**capable**, но консультация гейтится потоком управления + (перехват до `_spawn`) → capability ≠ consultation. +2. **Ось 2 — control-path (C) ≠ artifact-producer (P).** Определяется **кодом-потребителем**: + - **(C) control-path** — LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт** + (PASS→дальше / FAIL→откат). Суждение LLM входит в control flow. + - **(P) artifact-producer** — LLM производит артефакт, а продвижение решает **детерминированный + гейт**, судящий артефакт **независимо** (наличие файлов / CI). Суждение LLM в control flow не + входит. +3. **Ось 3 — деривируемость вердикта.** Вердикт C-консультации либо есть **детерминированная функция + tool-сигналов** (exit-code `pytest`/smoke/`staging_check.py`/деплоя), которые оркестратор **уже + вычисляет сам**, либо требует **настоящего суждения**, не сводимого к exit-коду. + +**Нормативное определение (фиксируется в `llm-usage-policy.md`):** + +> Call-site — **avoidable LLM control path** ⟺ выполнены **оба** условия: +> **(i)** это **C**-консультация (её LLM-вердикт потребляется потоком управления — `check_*` ветвится +> на нём); **и** **(ii)** вердикт **деривируем** из tool-сигналов, которые оркестратор уже вычисляет +> → суждение LLM не добавляет информации → консультацию можно снять без потери смысла. + +Это **двухбитный проверяемый предикат над `src/qg/checks.py`**, а не «удобство на глаз». + +### D4 — Таксономия и каноническая классификация (= «фиксация карты») (BR-2; FR-2; AC-2) + +Четыре взаимоисключающих класса; класс **выводится** из осей D3 (а не постулируется): + +- `keep-LLM` — нужно настоящее суждение (обязательно **назвать** конкретное суждение). +- `replace-deterministic-now` — безопасная детерминированная замена сейчас. +- `replace-later/risky` — замена возможна позже / с предпосылками. +- `needs-hybrid-fallback` — детерминированное ядро + LLM-фолбэк только на суждение. + +**Правило вывода:** `P → keep-LLM`; `C + не-деривируемый вердикт → keep-LLM`; `C + деривируемый +вердикт → replace-* / needs-hybrid-fallback (= avoidable)`. + +**Каноническая таблица (source of truth; карта и тесты зеркалят её байт-в-смысл):** + +| id | Роль | Транспорт/слот | Потребитель вывода (`src/qg/checks.py`) | Ось | Avoidable LLM control path? | Класс | Названное суждение (для keep) | +|----|------|----------------|------------------------------------------|-----|------------------------------|-------|-------------------------------| +| S0 | `_spawn` | транспорт | — | — | — (транспорт) | — | — | +| A1 | analyst | да (через S0) | `check_analysis_complete:33` (наличие файлов) | **P** | нет | `keep-LLM` | анализ требований, BRD/ТЗ — суждение | +| A2 | architect | да (через S0) | `check_architecture_done:62` (наличие 06-adr/07) | **P** | нет | `keep-LLM` | архитектурное решение/ADR — суждение | +| A3 | developer | да (через S0) | `check_ci_green:82` + `check_branch_mergeable:657` (CI/merge) | **P** | нет | `keep-LLM` | написание кода — суждение | +| A4 | reviewer | да (через S0) | `check_reviewer_verdict:336` (`verdict:`) | **C** | **нет** (вердикт НЕ деривируем) | `keep-LLM` | «приемлемость кода/решения» — не сводится к exit-коду | +| A5 | tester | да (через S0) | `check_tests_passed:182`→`_parse_tests_verdict:226` (`result:`) | **C** | **ДА** | `needs-hybrid-fallback` | (ядро детерминировано; LLM — триаж падений / маппинг TC↔критерии) | +| A6 | deployer | да (через S0) | `check_staging_status:599`→`_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473`→`_parse_deploy_status:413` (`deploy_status:`) | **C** | **ДА** | `replace-deterministic-now` | (вердикт = `staging_check.py`/exit-code; прод уже детерминирован Phase A/B/C ORCH-036) | +| D1 | deploy-finalizer | слот, перехват до `_spawn` (`launcher.py:389`) | — | — | — (уже детерминирован) | `already-deterministic` (эталон) | — | +| D2 | post-deploy-monitor | слот, перехват до `_spawn` (`launcher.py:394`) | — | — | — (уже детерминирован) | `already-deterministic` (эталон) | — | + +**Итог (поимённо, проверяется тестами D5):** `avoidable LLM control paths = {tester, deployer}`; +control-path-но-keep = `{reviewer}`; не-control-path (P) = `{analyst, architect, developer}`; +already-deterministic-эталон = `{deploy-finalizer, post-deploy-monitor}`. + +> **Уточнение по deployer (точность карты).** Роль `deployer` охватывает два ребра. На `deploy-staging` +> (`staging_status:`) её вердикт — чистый маппинг exit-кода `staging_check.py` → `replace-deterministic +> -now`. На `deploy` (`deploy_status:`) для self-hosting `orchestrator` вердикт **уже** производит +> детерминированный finalizer (Phase C, ORCH-036), LLM в критическом self-restart-пути нет; для прочих +> репо deployer-агент делает синхронный ssh-деплой. Поэтому «чисто деривируемый» срез deployer'а — +> прежде всего **staging-status** (см. D6). + +### D5 — Дизайн структурных анти-дрейф тестов (BR-6; FR-6; AC-6) + +Новый offline-файл `tests/test_llm_call_site_inventory.py` (без сети/LLM/subprocess-к-модели; маркер +`# ORCH-118` в шапке — TRACEABILITY). Дискриминатор всех проверок — **«консультирует LLM», а не +«спавнит subprocess»**. + +- **(a) Единственный транспорт.** В `src/**` ровно одна точка сборки/запуска Claude CLI — матчинг по + **конъюнкции** признаков LLM-транспорта (`CLAUDE_BIN` **и** `--system-prompt` **и** `Popen`/`bash -c` + в одном месте), и это `launcher._spawn`. ⚠️ Конъюнкция обязательна: bare-`CLAUDE_BIN` дал бы + false-positive на `preflight.py` (existence-check) и `config.py` (литерал пути) — они **не** + консультируют (см. Контекст). Allowlist единственного транспорта = `_spawn`. +- **(f) Отсутствие иного LLM-транспорта.** В `src/**`+`watchdog/**` нет импорта `anthropic`/`openai`/ + LLM-SDK, нет прямого HTTP-эндпоинта Anthropic/Claude (`api.anthropic.com`, `/v1/messages`), нет + второго model-invoking subprocess-сборщика. *(a)+(f) вместе = транспорт-агностичный двусторонний + инвариант.* +- **(b) Нет консультации в детерминированных путях.** Перечисленные модули D-списка и обработчики + D1/D2 не содержат LLM-транспорта (ни `_spawn`, ни (f)). +- **(c) Промпты ↔ файлы.** Карта перечисляет **ровно** те 6 промптов, что физически лежат в + `.openclaw/agents/` (двусторонняя сверка с `glob`). +- **(d) Тотальность.** Каждый перечисленный в карте call-site классифицирован **ровно один раз**. +- **(e) Capability ≠ consultation.** `launch_job` перехватывает `deploy-finalizer`/`post-deploy-monitor` + **до** `_spawn` (assert по `launcher.py` — наличие ранних return-веток до точки spawn). +- **(g) Control-path-разметка верна (TC-13).** Из машинного блока карты (D2) извлекается `role→axis`; + тест сверяет: P-роли потребляются `check_analysis_complete`/`check_architecture_done`/`check_ci_green`, + C-роли — `check_reviewer_verdict`/`_parse_tests_verdict`/`_parse_staging_status`/`_parse_deploy_status` + (наличие этих `def` в `src/qg/checks.py` как ground-truth). Дрейф разметки → красный. +- **(h) Avoidable-набор зафиксирован (TC-14).** Множество avoidable из карты = ровно `{tester, + deployer}`; `reviewer` = control-path-keep; `analyst`/`architect`/`developer` = не control path. + +> ❌ **Не вводить** тест, прибивающий карту к конкретным follow-up Plane-ID → ✅ только инварианты, +> резолвящиеся в код/файлы репозитория (R3/NFR-6). Тесты — обычный `pytest`, **не** Quality Gate и +> **не** стадия (FR-6 §QG). + +### D6 — Рекомендованный первый срез roadmap'а (BR-4; FR-4; AC-4) + +**Первый срез = deployer (staging-status).** Обоснование (самый низкорисковый «чисто деривируемый» +control path): + +1. Вердикт — **чистый маппинг** exit-кода `scripts/staging_check.py` → `staging_status:` (уже есть + leaf `src/staging_verdict.py` с `compute_staging_verdict`, ORCH-061) — деривируемость максимальна. +2. **Прод уже детерминирован** (Phase A/B/C, ORCH-036) → срез не трогает критический self-restart-путь + → минимальная поверхность риска. +3. Опирается на **существующий прецедент** D1/D2 (`launch_job`-перехват до `_spawn`) — архитектурный + риск замены снижен (BRD §6). +4. `replace-deterministic-now`, без потребности в hybrid-fallback (в отличие от tester). + +**Порядок roadmap'а:** (1) **deployer-замена** (staging-маппинг; prod уже детерминирован) → +(2) **tester-гибрид** (детерминированное ядро `pytest`+smoke + LLM-фолбэк на триаж падений / маппинг +TC↔критерии — `needs-hybrid-fallback`). Для каждого кандидата roadmap несёт: зависимости, **оценку** +экономии токенов/времени из `agent_runs` (помечено «оценка до фактического замера», NFR-5), риск +безопасности, потребность в hybrid-fallback, ожидание kill-switch/обратимости. Кандидаты названы +**по роли**; конкретный Plane-ID **не** фиксируется (NFR-6) — заводится при создании задачи. + +### D7 — Скоуп-гард и инварианты (BR-7; FR-7; NFR-1/NFR-3; AC-7/AC-9) + +- **Docs + tests only.** Диф не меняет `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` / + machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/ + `coverage_status:`) / схему БД. В `src/**` нет нового детерминированного раннера tester/deployer. +- **Анти-фабрикация.** Ни один артефакт не фиксирует конкретный follow-up Plane-ID; все ссылки + резолвятся в репозиторий. Тест не пинит карту к follow-up ID. +- **Self-hosting.** Только чтение кода + запись docs/tests — не деплоит, не рестартит прод, не трогает + `main`/force-push, без процессов/сети. kill-switch не нужен (нет рантайм-поведения), как + ORCH-077/079/101/102/103/011. +- **Наблюдаемость в `GET /queue`/`GET /metrics`** — **вне скоупа** (TRZ §4); карта/политика — + документы, не рантайм. + +### Применимость 07/08 +- **07-infra-requirements — N/A:** топология не меняется (нет нового сервиса/контейнера/порта/маунта). +- **08-data-requirements — N/A:** схема БД не меняется; `agent_runs` читается только для оценок (NFR-5). + +## Альтернативы + +- **Один объединённый документ (карта+roadmap+политика)** — отвергнуто: разные жизненные циклы и + тестируемость (D1); снижает стабильность нормативной политики и проверяемость снимка-карты. +- **Размещение карты только в `docs/work-items/ORCH-118/`** — отвергнуто: документ сквозной и durable, + читается будущими follow-up'ами; work-item-папка — неверная альтитуда (теряется при навигации по + архитектуре). +- **Тест по сырому regex прозы карты** — отвергнуто как хрупкое: дрейф формулировок ломал бы тест без + смыслового дрейфа. Выбран машинный markdown-блок (D2/D5g). +- **Тест-матчинг по bare-`CLAUDE_BIN`** — отвергнуто: false-positive на `preflight.py`/`config.py` + (capability/литерал, не консультация). Выбрана конъюнкция признаков транспорта (D5a). +- **Фиксация follow-up Plane-ID (`ORCH-115`/`ORCH-116`)** — отвергнуто нормативно (NFR-6, корень + отклонённой ревизии R2): этих work item нет; кандидаты — по роли. +- **Первый срез = tester** — отвергнуто: tester требует hybrid-fallback (триаж падений), поверхность + риска и объём больше; deployer-staging — чище деривируем и лучше обеспечен прецедентом (D6). +- **Включить наблюдаемость карты в `GET /queue`** — отвергнуто как scope-creep (TRZ §4): карта — + документ, а не рантайм-состояние. + +## Последствия + +- **+** Доказательная, код-привязанная карта разводит транспорт/слот ≠ консультация ≠ control-path и + даёт **проверяемый** предикат «avoidable» → закрывает блокеры R3→R5; follow-up'ы выполняются + предсказуемо. +- **+** Нормативная политика делает «LLM только там, где нужно суждение» инвариантом любой будущей + правки control-path'а; защищает автономность (NFR-2). +- **+** Структурные тесты держат карту синхронной с кодом (включая control-path-ось) — анти-дрейф. +- **−** Карта — **снимок**: при эволюции `src/qg/checks.py` (смена потребителя / новая роль) тесты + D5g/h станут красными — требуется обновлять карту/политику в том же PR. *Митигейшн:* это + **запланированное** свойство (норматив сопровождения), а не дефект; тест указывает точку дрейфа. +- **−** Машинный блок карты вводит лёгкую форматную дисциплину (стабильный заголовок таблицы). + *Митигейшн:* формат человекочитаемый, документирован в D2; парсер — stdlib split. +- **Откат:** удаление/правка трёх `docs/architecture/llm-*.md` + тест-файла + секции README. Рантайм + не затронут (риска нет). + +## Ссылки +- BRD: `docs/work-items/ORCH-118/01-brd.md` (§0 / §0-bis / BR-1…BR-9 / NFR-1…NFR-7) +- TRZ: `docs/work-items/ORCH-118/02-trz.md` (FR-1…FR-8 / §2 таблица модулей) +- Acceptance: `docs/work-items/ORCH-118/03-acceptance-criteria.md` (AC-1…AC-10) +- Tech-risks: `docs/work-items/ORCH-118/10-tech-risks.md` +- Сквозной ADR: `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md` +- Сверено по коду: `src/agents/launcher.py` (`_spawn:472`, CLI `610-614`, `launch_job:377`, + перехват `389/394`, «Not an LLM spawn» `407/428`), `src/qg/checks.py` + (`33/62/82/336/182/226/599/538/473/413/657`), `.openclaw/agents/*.md` (6 промптов), + `src/{staging_verdict,self_deploy,frontmatter,...}.py` (детерминированные leaf'ы) +- Прецедент детерминированной замены агента: ORCH-036 (self-deploy Phase A/B/C), D1/D2 `launch_job` +- Прецедент docs+tests-only задач: ORCH-077/079/101/102/103/011 + + diff --git a/docs/work-items/ORCH-118/10-tech-risks.md b/docs/work-items/ORCH-118/10-tech-risks.md new file mode 100644 index 0000000..56a9618 --- /dev/null +++ b/docs/work-items/ORCH-118/10-tech-risks.md @@ -0,0 +1,43 @@ +--- +work_item: ORCH-118 +stage: architecture +author_agent: architect +status: accepted +created_at: 2026-06-15 +model_used: claude-opus-4-8 +--- + +# 10 — Технические риски: ORCH-118 — replace avoidable LLM control paths (inventory + map + policy) + +Work Item: **ORCH-118** · Repo: **orchestrator** · Стадия: architecture + +> Информационный документ (гейтом не парсится). Перечисляет риски реализации **этой** задачи +> (docs + tests only — inventory/карта/политика/тесты). Риски будущих раннеров замен — в roadmap'е и +> в ADR соответствующих follow-up'ов, **не здесь**. + +## Реестр рисков + +| ID | Риск | Вер. | Влия. | Митигейшн | +|----|------|------|-------|-----------| +| TR-1 | **Тривиальный тест** — структурные тесты «зелёные, но ничего не проверяют» (рецидив корня R4: проверяют «один `Popen`» без control-path-оси) | Сред. | Выс. | D5: обязательные инварианты (g) control-path-разметка сверена с `src/qg/checks.py` и (h) avoidable-набор `{tester, deployer}`; (a)+(f) двусторонний транспорт-инвариант; ревью AC-6 буквально требует (g)/(h) | +| TR-2 | **False-positive матчинга транспорта** — тест ловит `preflight.py`/`config.py` (bare `CLAUDE_BIN` — capability/литерал, не консультация) → ложный «второй транспорт» | Сред. | Сред. | D5a: матчинг по **конъюнкции** признаков (`CLAUDE_BIN` ∧ `--system-prompt` ∧ `Popen`/`bash -c`); allowlist = `_spawn`; явный негативный кейс на `preflight`/`config` | +| TR-3 | **Дрейф карты-снимка** — `src/qg/checks.py` эволюционирует (смена потребителя / новая роль), карта не обновлена → ложно-зелёная витрина | Сред. | Сред. | Запланированное свойство: тесты D5g/h краснеют в точке дрейфа; норматив сопровождения «менял потребителя вердикта → обнови карту в том же PR» (ADR-001 D7 / adr-0047 D6) | +| TR-4 | **Хрупкий парс машинного блока** — regex по прозе карты ломается на переформулировке без смыслового дрейфа | Низ. | Сред. | D2/D5: стабильный markdown-блок с фиксированным заголовком таблицы, парс stdlib-split; формат документирован | +| TR-5 | **Непроверяемые ссылки / фабрикация follow-up ID** (рецидив дефекта R2) | Низ. | Выс. | NFR-6/AC-9: только резолвящиеся `file:line`/doc-ссылки; кандидаты — по роли; тест **не** пинит карту к follow-up ID; ревью AC-9 | +| TR-6 | **Scope-creep в рантайм** — соблазн «заодно» тронуть `QG_CHECKS`/`check_*`/раннер | Низ. | Выс. | AC-7/D7: docs+tests only; диф не меняет `STAGE_TRANSITIONS`/реестр-имена `QG_CHECKS`/machine-verdict/схему БД; нет нового раннера tester/deployer; ревью буквально | +| TR-7 | **Пере-/недо-классификация** (LLM убран где нужно суждение / сохранён где не нужен) | Низ. | Сред. | Класс **выводится** из осей D3 (двухбитный предикат), не «на глаз»; `keep-LLM` обязан назвать конкретное суждение; ревью карты против `src/qg/checks.py` | +| TR-8 | **Рассинхрон golden-source** — карта/политика введены, README/overview/CHANGELOG не обновлены | Сред. | Сред. | AC-8 (ось ORCH-079/011 → finding ≥P1); README-секция добавлена на стадии architecture; development досинхронизирует overview/CHANGELOG в том же PR | +| TR-9 | **Line-привязки `file:line` устаревают** между анализом и реализацией | Низ. | Низ. | Тест проверяет якоря по **имени** `def` (наличие в `src/qg/checks.py`), а не по номеру строки; номера в карте — справочные, обновляются разработчиком при материализации | + +## Сводный вывод + +Доминирующий класс — **риски качества тестов и анти-дрейфа** (TR-1/TR-2/TR-4), не рантайм-риски: +задача физически не меняет поведение конвейера (`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/ +machine-verdict/схема БД — байт-в-байт), не деплоит и не трогает прод (self-hosting безопасно, NFR-3), +enduro-trails не затронут. Остаточный риск для прод-конвейера — **пренебрежимо мал**. + +Эскалация `arch:major-change` **не требуется** (нет новой стадии/компонента/смены БД — это +docs+tests-only задача по прецеденту ORCH-077/079/101/102/103/011). Возврат в анализ **не требуется**: +ТЗ удовлетворяется без нарушения принципов архитектуры. Ключевой управляемый риск — не дать тестам +выродиться в тривиальные (TR-1) и не словить false-positive транспорта (TR-2); оба сняты дизайном D5. +