ORCH-118: LLM call-site map, control-path axis, roadmap & usage policy (docs+tests only) #140
@@ -3,6 +3,11 @@
|
||||
Формат: [Keep a Changelog](https://keepachangelog.com/). Записи — на смысловой PR/задачу.
|
||||
|
||||
## [Unreleased]
|
||||
- **Карта LLM-консультаций + control-path-ось «avoidable» + roadmap + нормативная политика** (ORCH-118, `docs`+`test`, inventory-first, docs+tests only): зонтичный follow-up RCA-трека ORCH-114/117 — у оркестратора не было ни нормативного критерия «где LLM нужен, а где это avoidable control path», ни карты мест вызова LLM, прибитой к коду. Выпущена **доказательная карта** каждого места, где control-path потребляет (или способен потребить) суждение LLM, с control-path-разметкой и классификацией; **упорядоченный roadmap** детерминированных замен; **нормативная политика** использования LLM; набор **структурных анти-дрейф тестов**. Это **docs + tests only**: `src/**`-рантайм не меняется → `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` / machine-verdict-ключи / схема БД — **байт-в-байт не тронуты**; раннеры замен **не** реализуются (FR-7); конкретные follow-up Plane-ID **не** фиксируются (R3/NFR-6 — кандидаты по роли). kill-switch не нужен (нет рантайм-поведения), как ORCH-077/079/101/102/103/011. ADR: `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`, сквозной `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`.
|
||||
- **Единица инвентаря — LLM-консультация** (control-path потребляет суждение LLM), а **не** «спавн процесса / существование Claude CLI» (R4, capability ≠ consultation). Карта разводит **три ортогональных факта**: (1) consultation ≠ transport/slot (единственный транспорт LLM-консультации в `src/**` — `launcher._spawn`, `launcher.py:472`/CLI-сборка `610-614`; иного транспорта нет; job-роли `deploy-finalizer`/`post-deploy-monitor` занимают слот, но перехватываются в `launch_job` **до** `_spawn`, `launcher.py:389/394` — консультации нет); (2) **control-path (C) ≠ artifact-producer (P)** по коду-потребителю в `src/qg/checks.py` (C: гейт ветвится на LLM-вердикте; P: детерминированный гейт судит артефакт независимо — файлы/CI); (3) деривируемость вердикта из tool-сигналов.
|
||||
- **Нормативное определение** «avoidable LLM control path» = двухбитный предикат (C-консультация **И** вердикт деривируем из exit-кодов `pytest`/smoke/`staging_check.py`/деплоя). Поимённый целевой набор (доказательно, прибит тестами): **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` (нормативный принцип + критерии keep/replace через ось + определение «avoidable»). Витрина `docs/overview/tech-quality-security.md` и `docs/architecture/README.md` ссылаются на карту и политику.
|
||||
- **Анти-дрейф тесты (offline, без сети/LLM/subprocess-к-модели):** `tests/test_llm_call_site_inventory.py` (TC-01 единственный транспорт = `_spawn`; TC-12 отсутствие иного LLM-транспорта; TC-02 детерминированные модули без консультации; TC-03 промпты↔файлы; TC-04 тотальность+согласованность класса с осью; TC-05 keep-LLM именует суждение; TC-06 capability≠consultation; TC-09 снимок рантайм-контрактов; **TC-13** control-path-разметка сверена с потребителем в `src/qg/checks.py`; **TC-14** avoidable-набор = `{tester, deployer}`) и `tests/test_llm_determinization_docs.py` (TC-07 полнота roadmap+первый срез; TC-08 политика нормативна+определяет термин; TC-11 анти-фабрикация follow-up ID). Дискриминатор всех проверок — **«консультирует LLM», а не «спавнит subprocess»**. Норматив сопровождения: менял место вызова LLM или потребителя вердикта в `src/qg/checks.py` → обнови карту/разметку и политику в том же PR.
|
||||
- **Sandbox-only fail-closed изоляция записи в Plane из тест-процесса** (ORCH-117, `fix`, bug→escalate full-cycle): закрыт корневой класс инцидента **ORCH-114** — тест/worktree-процесс выполнил РЕАЛЬНУЮ запись (`PATCH …/issues/… state=<Done>` + комментарий «Stage: deploy → done») против **боевого** Plane-проекта, т.к. тест/staging-процессы наследуют живой боевой Plane-токен (`PLANE_HEADERS`/`PROJECT_ID` захвачены литералами **на импорте** — подмена env/токена постфактум бесполезна, NFR-4), и **ничто** не принуждало их писать только в sandbox. Симметрия прецеденту `tests/conftest.py::_no_telegram` (autouse-глушилка Telegram «pytest на проде слал реальные сообщения») — для Plane-**записи** такой защиты не было. Аддитивно, never-raise в боевом пути; `STAGE_TRANSITIONS`/реестр `QG_CHECKS`/семантика и имена `check_*`/machine-verdict-ключи/схема БД — **байт-в-байт не тронуты** (это изоляция клиента Plane, **не** Quality Gate и **не** стадия). Новый чистый leaf `src/plane_write_guard.py` (`decide(project_id, op, work_item) -> (ALLOW|BLOCK, reason)`, по образцу `deploy_status_guard`/`serial_gate`) врезан в **3 примитива записи** `plane_sync` (`update_issue_state`/`add_comment`/`_set_issue_state_direct`) **на момент вызова** — сразу после локального `_resolve_project_id` и **до** любого сетевого шага (ни GET, ни PATCH/POST). Гард активен **только в тест-процессе** (детект `"pytest" in sys.modules` / `PYTEST_CURRENT_TEST`); боевой и staging рантаймы (`uvicorn src.main:app`, без pytest в процессе) — строгий **no-op** (NFR-2/NFR-3). В тест-процессе запись разрешена **только** при одновременном (а) opt-in `plane_test_write_enabled=True` **и** (б) целевом проекте ∈ sandbox-allowlist `plane_test_sandbox_projects` (дефолт = единственный SANDBOX `8c5a3025-…`); иначе — default-deny; нерезолвимый проект → блок (fail-closed, NFR-1); боевой проект запрещён **даже при opt-in** (allowlist sandbox-only). Второй независимый sandbox-bound слой — autouse-floor `tests/conftest.py::_plane_sandbox_only` (opt-in OFF для всего сьюта, по образцу `_no_telegram`/`_disable_*`); sandbox-e2e ре-энейблит opt-in в своей фикстуре поверх floor. **Умышленно БЕЗ kill-switch прод-блока** (NFR-6/FR-7/anti-drift): выключателя, переоткрывающего прод-запись из pytest, нет — единственный реверс — sandbox-bound opt-in. Аудит: блок → громкий структурный ERROR (`project_id`/`work_item`/`op`/`reason` — делает инцидент класса ORCH-114 очевидным), разрешённая sandbox-запись → INFO. Новые ключи `ORCH_PLANE_TEST_WRITE_ENABLED` (дефолт `false`) / `ORCH_PLANE_TEST_SANDBOX_PROJECTS` (дефолт = SANDBOX id) с безопасными дефолтами; `scripts/staging_check.py` Block C (E2E в SANDBOX) — отдельный процесс с собственными httpx-вызовами, гардом не затронут. Покрытие — `tests/test_orch117_plane_write_isolation.py` (TC-01 — обязательный регресс ORCH-114: красный до врезки, зелёный после; TC-02…TC-14). ADR: `docs/work-items/ORCH-117/06-adr/ADR-001-sandbox-only-plane-write-guard.md`, сквозной `docs/architecture/adr/adr-0046-sandbox-only-plane-write-guard.md`.
|
||||
- **Ownership-lease для side-effectful переходов стадий + умное восстановление при старте** (ORCH-114, `fix`, bug→escalate full-cycle): закрыт **корневой класс** инцидент-цепочки ORCH-110/111/112/113 — у side-effectful переходов стадий не было единого владения. `advance_stage` ре-ентерабельна и пишет стадию «голым» `UPDATE … WHERE id=?` (без compare-and-swap), а ≥5 акторов (монитор / Plane-webhook / reconciler F-1 / job-reaper / deploy-finalizer) входят в один переход независимо → конкурентный или после-рестартовый повторный вход **дважды** применял необратимые эффекты (merge_pr / coverage-ratchet / image-rebuild / инициация прод-деплоя) и давал **противоречие rollback↔done** (инцидент ORCH-111, job 1914 / PR #130). Два комплементарных слоя, оба аддитивные, под единым kill-switch, never-raise: **(1) durable transition-lease** (новая таблица `transition_lease`) — владение на ВХОДЕ в side-effectful регион (второй актор, увидев живого владельца, не стартует тяжёлые под-гейты вовсе — предотвращение, не починка постфактум); **(2) expected-stage CAS** (`update_task_stage_cas`) — на ЗАПИСИ стадии (проигравший гонку — аборт без побочных эффектов), что закрывает и **6 путей записи стадии в обход `advance_stage`** (gitea×5 + plane rollback). Liveness владельца = `owner_pid` + `owner_boot_id` (НЕ heartbeat: блокирующий 900s merge re-test не может бить heartbeat — довод самого ORCH-113), что делает рестарт-recovery бесплатным (новый процесс → новый boot-id → все прежние lease мгновенно устаревшие → реклеймятся). Lease без собственного TTL: его потолок возраста = Tier-3 backstop `reaper_max_running_s` (5400) → сквозной бюджет ORCH-065/109/110/113 не тронут. `STAGE_TRANSITIONS` / реестр `QG_CHECKS` / семантика и имена `check_*` / machine-verdict-ключи / **схемы существующих таблиц** — байт-в-байт (одна аддитивная таблица, без epoch-колонки на `tasks`). Скоуп self-hosting (`transition_lease_repos=""` → только `orchestrator`; enduro не затронут); kill-switch `ORCH_TRANSITION_LEASE_ENABLED=false` → CAS вырождается в прежний безусловный `update_task_stage`, lease инертен → поведение байт-в-байт до ORCH-114. ADR: `docs/work-items/ORCH-114/06-adr/ADR-001-transition-ownership-lease-and-stage-cas.md`, сквозной `docs/architecture/adr/adr-0045-transition-ownership-lease-and-stage-cas.md`.
|
||||
- **Leaf `src/transition_lease.py` (новый, чистый never-raise):** по образцу `serial_gate`/`coverage_gate`/`finalizer_liveness` (импортирует только `db`+`config`, лениво `merge_gate.pid_alive`/`qg.checks`/`notifications`; НЕ импортирует `stage_engine`/`launcher`) — `applies(repo)` / `acquire(task_id, owner, run_id, stage)` (атомарный rowcount-guard `INSERT … ON CONFLICT DO NOTHING` после очистки stale-строки) / `is_held_by_live_owner(task_id)` (fail-closed → defer на сомнении) / `release(task_id, force=False)` (holder-aware по boot) / `reclaim_if_stale` / `recover_on_startup` / `commit_stage_cas(task_id, expected, new, repo)` (flag-off → unconditional `update_task_stage`; flag-on → CAS) / `snapshot()`.
|
||||
|
||||
@@ -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_<AGENT>`/`ORCH_AGENT_EFFORT_<AGENT>` > `*_default` > CLI-дефолт (без флага)**. **Эффорт (ORCH-081):** ниже `*_default` добавлен непустой **per-role floor** — class-default поля `agent_effort_<role>` из `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`.
|
||||
|
||||
|
||||
@@ -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-трек)
|
||||
</content>
|
||||
156
docs/architecture/llm-call-sites.md
Normal file
156
docs/architecture/llm-call-sites.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# LLM call-site map — inventory, control-path axis & classification (ORCH-118)
|
||||
|
||||
> **Что это.** Доказательная карта **каждого места**, где control-path оркестратора
|
||||
> потребляет (или способен потребить) суждение LLM. Единица инвентаря — **LLM-консультация**
|
||||
> (control-path потребляет суждение LLM), **не** «спавн процесса / существование Claude CLI»
|
||||
> (capability ≠ consultation, BRD §0 / R4).
|
||||
>
|
||||
> **Снимок, прибитый к коду.** Карта — *снимок*; её инварианты держат структурные тесты
|
||||
> `tests/test_llm_call_site_inventory.py` (анти-дрейф). Меняешь место вызова LLM или потребителя
|
||||
> вердикта в `src/qg/checks.py` → **обнови эту карту и политику в том же PR** (норматив сопровождения).
|
||||
>
|
||||
> **Источник истины** содержательной классификации — ADR
|
||||
> `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md`
|
||||
> (D2/D3/D4) и сквозной `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md`.
|
||||
> Нормативное определение «avoidable LLM control path» и критерии keep/replace — в
|
||||
> [`llm-usage-policy.md`](llm-usage-policy.md); порядок замен — в
|
||||
> [`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).
|
||||
|
||||
---
|
||||
|
||||
## 0. Три ортогональных факта (как читать карту)
|
||||
|
||||
Карта **явно** разводит три раздельных факта — их смешение было корнем блокеров R3→R5:
|
||||
|
||||
1. **Ось 1 — consultation ≠ transport/slot.** «LLM-консультация» = точка, где решение/артефакт
|
||||
конвейера **потребляет суждение LLM**. Транспорт (`_spawn`) — реализация, не определение. Слот
|
||||
агента (job-роли `D1`/`D2`) делает site LLM-**capable**, но консультация гейтится потоком
|
||||
управления (перехват **до** `_spawn`) → **capability ≠ consultation**.
|
||||
2. **Ось 2 — control-path (C) ≠ artifact-producer (P).** Определяется **кодом-потребителем** вывода
|
||||
роли в `src/qg/checks.py`:
|
||||
- **(C) control-path** — LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт**
|
||||
(PASS → дальше / FAIL → откат). Суждение LLM **входит** в поток управления.
|
||||
- **(P) artifact-producer** — LLM производит артефакт, а продвижение решает **детерминированный
|
||||
гейт**, судящий артефакт **независимо** (наличие файлов / CI-статус). Суждение LLM в control
|
||||
flow **не входит**.
|
||||
3. **Ось 3 — деривируемость вердикта.** Вердикт C-консультации либо есть **детерминированная функция
|
||||
tool-сигналов** (exit-code `pytest`/smoke/`staging_check.py`/деплоя), которые оркестратор **уже
|
||||
вычисляет сам**, либо требует **настоящего суждения**, не сводимого к exit-коду.
|
||||
|
||||
> **Avoidable LLM control path** (нормативное определение — [`llm-usage-policy.md`](llm-usage-policy.md)):
|
||||
> call-site, для которого выполнены **оба** условия — **(i)** это C-консультация (её LLM-вердикт
|
||||
> потребляется потоком управления) **и** **(ii)** вердикт **деривируем** из tool-сигналов. Тогда
|
||||
> суждение LLM не добавляет информации → консультацию можно снять без потери смысла.
|
||||
|
||||
---
|
||||
|
||||
## 1. Инвентарь LLM-консультаций (полный, привязан к коду)
|
||||
|
||||
Каждая запись несёт: `id` · `location (file:line)` · `trigger` · `stage/owner` · `output artifact` ·
|
||||
`machine-verdict key` · `output consumer` (кто потребляет вывод роли) · `est. tokens/runtime`
|
||||
(оценка из `agent_runs`) · `consults-LLM` · `axis` (C/P) · `classification` · `rationale`.
|
||||
|
||||
| id | location (file:line) | trigger | stage / owner | output artifact | machine-verdict key | output consumer (`src/qg/checks.py`) | est. tokens/runtime (оценка) | consults-LLM | axis | classification | rationale |
|
||||
|----|----------------------|---------|---------------|-----------------|---------------------|--------------------------------------|------------------------------|--------------|------|----------------|-----------|
|
||||
| **S0** | `src/agents/launcher.py:472` (`_spawn`; CLI-сборка `610-614`; парс токенов `_monitor_agent:838`) | `launch_job` → `_spawn` для любой из 6 ролей | — (транспорт) | — | — | — | — | **транспорт** (capability) | — | — | Единственный транспорт LLM-консультации в `src/**`; не call-site решения |
|
||||
| **A1** | `.openclaw/agents/analyst.md` | стадия `analysis` | analyst | `01-brd` … `04-test-plan` | — | `check_analysis_complete:33` (наличие файлов) | ~80–200k / 5–20 мин | да (через S0) | **P** | `keep-LLM` | анализ требований / BRD/ТЗ — настоящее суждение; гейт судит лишь наличие артефактов |
|
||||
| **A2** | `.openclaw/agents/architect.md` | стадия `architecture` | architect | `06-adr/`, `07` | — | `check_architecture_done:62` (наличие 06-adr/07) | ~80–200k / 5–20 мин | да (через S0) | **P** | `keep-LLM` | архитектурное решение / ADR — настоящее суждение |
|
||||
| **A3** | `.openclaw/agents/developer.md` | стадия `development` | developer | код + PR | — | `check_ci_green:82` (+ `check_branch_mergeable:657`) — CI/merge | ~150–400k / 10–40 мин | да (через S0) | **P** | `keep-LLM` | написание кода — настоящее суждение; гейт судит CI/merge, не самоотчёт |
|
||||
| **A4** | `.openclaw/agents/reviewer.md` | стадия `review` | reviewer | `12-review.md` | `verdict:` | `check_reviewer_verdict:336` (`verdict:`) | ~100–300k / 5–25 мин | да (через S0) | **C** | `keep-LLM` | control path, но вердикт «приемлемость кода/решения» **НЕ деривируем** из exit-кода — настоящее суждение |
|
||||
| **A5** | `.openclaw/agents/tester.md` | стадия `testing` | tester | `13-test-report.md` | `result:` | `check_tests_passed:182` → `_parse_tests_verdict:226` (`result:`) | ~60–150k / 5–20 мин | да (через S0) | **C** | `needs-hybrid-fallback` | **avoidable**: PASS/FAIL = exit-code `pytest`+smoke (деривируем); LLM нужен лишь на триаж падений / маппинг TC↔критерии |
|
||||
| **A6** | `.openclaw/agents/deployer.md` | стадии `deploy-staging` / `deploy` | deployer | `15-staging-log.md` / `14-deploy-log.md` | `staging_status:` / `deploy_status:` | `check_staging_status:599` → `_parse_staging_status:538` (`staging_status:`); `check_deploy_status:473` → `_parse_deploy_status:413` (`deploy_status:`) | ~40–120k / 3–15 мин | да (через S0) | **C** | `replace-deterministic-now` | **avoidable**: staging-вердикт = маппинг exit-кода `staging_check.py`; прод уже детерминирован Phase A/B/C (ORCH-036) |
|
||||
| **D1** | `src/agents/launcher.py:389` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `407`) | post-deploy edge | deploy-finalizer | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Занимает слот агента, но LLM не консультируется — рабочий прецедент замены |
|
||||
| **D2** | `src/agents/launcher.py:394` (перехват в `launch_job` **до** `_spawn`; «Not an LLM spawn» `428`) | post-deploy observation | post-deploy-monitor | jobs-row | — | — | — (детерминированный) | **нет** (слот, перехват до `_spawn`) | — | `already-deterministic` (эталон) | Тик наблюдения; LLM не консультируется |
|
||||
|
||||
> **Итог (поимённо).** `avoidable LLM control paths = {tester, deployer}`; control-path-но-keep =
|
||||
> `{reviewer}`; не-control-path (P) = `{analyst, architect, developer}`; already-deterministic-эталон =
|
||||
> `{deploy-finalizer, post-deploy-monitor}`.
|
||||
|
||||
### 1.1 Машинно-читаемый блок инвентаря
|
||||
|
||||
> Стабильный заголовок таблицы (`id | role | location | output_consumer | consults_llm | axis |
|
||||
> avoidable | classification`) парсится `tests/test_llm_call_site_inventory.py` (split по `|`, без
|
||||
> новых зависимостей) и сверяется с кодом (TC-03/04/05/13/14). **Не менять заголовок и значения без
|
||||
> синхронной правки кода/тестов.**
|
||||
|
||||
<!-- ORCH-118-INVENTORY-BLOCK:START -->
|
||||
| id | role | location | output_consumer | consults_llm | axis | avoidable | classification |
|
||||
|----|------|----------|-----------------|--------------|------|-----------|----------------|
|
||||
| S0 | _spawn | src/agents/launcher.py:472 | - | transport | - | - | - |
|
||||
| A1 | analyst | .openclaw/agents/analyst.md | check_analysis_complete | yes | P | no | keep-LLM |
|
||||
| A2 | architect | .openclaw/agents/architect.md | check_architecture_done | yes | P | no | keep-LLM |
|
||||
| A3 | developer | .openclaw/agents/developer.md | check_ci_green | yes | P | no | keep-LLM |
|
||||
| A4 | reviewer | .openclaw/agents/reviewer.md | check_reviewer_verdict | yes | C | no | keep-LLM |
|
||||
| A5 | tester | .openclaw/agents/tester.md | _parse_tests_verdict | yes | C | yes | needs-hybrid-fallback |
|
||||
| A6 | deployer | .openclaw/agents/deployer.md | _parse_staging_status | yes | C | yes | replace-deterministic-now |
|
||||
| D1 | deploy-finalizer | src/agents/launcher.py:389 | - | no | - | - | already-deterministic |
|
||||
| D2 | post-deploy-monitor | src/agents/launcher.py:394 | - | no | - | - | already-deterministic |
|
||||
<!-- ORCH-118-INVENTORY-BLOCK:END -->
|
||||
|
||||
### 1.2 keep-LLM — названное суждение (обоснование)
|
||||
|
||||
> Для каждой `keep-LLM`-записи назван **конкретный** вид суждения, ради которого LLM сохраняется.
|
||||
> Для C-keep (`reviewer`) обоснование явно фиксирует **НЕ-деривируемость** вердикта (почему не сводится
|
||||
> к exit-коду). Парсится TC-05 (`- role: текст`).
|
||||
|
||||
<!-- ORCH-118-KEEP-JUSTIFICATION-BLOCK:START -->
|
||||
- analyst: анализ требований и написание BRD/ТЗ — настоящее суждение; детерминированный гейт `check_analysis_complete` судит лишь наличие файлов, не их содержательное качество.
|
||||
- architect: архитектурное решение и ADR — настоящее суждение о компромиссах/инвариантах; `check_architecture_done` судит лишь наличие 06-adr/07.
|
||||
- developer: написание кода — настоящее суждение; гейт `check_ci_green` судит CI/merge, а не самоотчёт агента.
|
||||
- reviewer: «приемлемость кода/решения» — настоящее суждение, которое НЕ деривируемо (not derivable) из exit-кода `pytest`/smoke/деплоя; в отличие от tester/deployer вердикт reviewer'а не сводится к tool-сигналу, поэтому это control-path-но-keep, а не avoidable.
|
||||
<!-- ORCH-118-KEEP-JUSTIFICATION-BLOCK:END -->
|
||||
|
||||
---
|
||||
|
||||
## 2. Таксономия классификации (4 класса, выведена из осей)
|
||||
|
||||
Четыре взаимоисключающих класса; класс **выводится** из осей §0 (а не постулируется):
|
||||
|
||||
- **`keep-LLM`** — нужно настоящее суждение (обязательно **назвать** конкретное суждение, §1.2).
|
||||
- **`replace-deterministic-now`** — безопасная детерминированная замена сейчас.
|
||||
- **`replace-later/risky`** — замена возможна позже / с предпосылками.
|
||||
- **`needs-hybrid-fallback`** — детерминированное ядро + LLM-фолбэк только на суждение.
|
||||
|
||||
**Правило вывода:**
|
||||
`P → keep-LLM`; `C + не-деривируемый вердикт → keep-LLM`;
|
||||
`C + деривируемый вердикт → replace-* / needs-hybrid-fallback` (**= avoidable**).
|
||||
|
||||
`deploy-finalizer`/`post-deploy-monitor` помечены `already-deterministic` — вне таксономии замен
|
||||
(эталон: LLM не консультируется, перехват до `_spawn`).
|
||||
|
||||
---
|
||||
|
||||
## 3. Детерминизм не-агентских control-path'ов (доказательство, FR-3 / AC-3)
|
||||
|
||||
Эти пути **не консультируют LLM** (ни через `_spawn`-транспорт, ни альтернативным транспортом). ⚠️ Они
|
||||
**спавнят subprocess'ы** (`git`/`pytest`/`docker`/`ssh`/сканеры/`staging_check.py`) — это
|
||||
детерминированные **инструменты**, не LLM: доказательство детерминизма — **отсутствие *LLM*-транспорта**,
|
||||
а не отсутствие *subprocess* (дискриминатор §0). Проверяется TC-02.
|
||||
|
||||
| Путь / модуль | `file:line` (якорь) | Природа |
|
||||
|---------------|---------------------|---------|
|
||||
| Маршрутизация стадий | `src/stages.py::STAGE_TRANSITIONS:12`, `advance_stage` в `src/stage_engine.py` | статический словарь + детерминированная функция |
|
||||
| Реестр Quality Gate | `src/qg/checks.py::QG_CHECKS:812` (14 имён) | словарь имя→функция |
|
||||
| Все `check_*` | `src/qg/checks.py` (`33/62/82/182/336/473/599/657`, …) | файловые/HTTP/exit-code проверки |
|
||||
| Парсеры вердиктов `_parse_*` | `src/qg/checks.py:226/413/538` через `src/frontmatter.py::parse_frontmatter` | YAML-frontmatter парс (читают, **не производят** вердикт) |
|
||||
| Классификатор ошибок | `src/error_classifier.py` | regex по строкам |
|
||||
| Под-гейты | `src/{security_gate,merge_gate,coverage_gate,staging_verdict}.py` | сканеры/`pytest --cov`/git/маппинг exit-кодов |
|
||||
| Self-deploy Phase A/B/C | `src/self_deploy.py` | детерминированный detached-деплой (ORCH-036) |
|
||||
| Сериализация / владение | `src/{serial_gate,transition_lease,reconciler,job_reaper}.py` | FIFO-гейт / durable-lease / CAS / reaper |
|
||||
|
||||
Любая найденная **неожиданная** LLM-консультация в этих путях добавляется в инвентарь §1 и
|
||||
классифицируется §2 (тогда TC-02 станет красным — точка дрейфа).
|
||||
|
||||
---
|
||||
|
||||
## 4. Скоуп и анти-дрейф
|
||||
|
||||
- **Docs + tests only (ORCH-118).** `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
|
||||
machine-verdict-ключи (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
|
||||
`coverage_status:`) / схема БД — **байт-в-байт не тронуты**. Раннеры замен **не** реализованы
|
||||
(см. roadmap — follow-up'ы по роли, без Plane-ID).
|
||||
- **Анти-дрейф тесты:** `tests/test_llm_call_site_inventory.py` (TC-01…TC-06, TC-09, TC-12, TC-13,
|
||||
TC-14) и `tests/test_llm_determinization_docs.py` (TC-07/08/11). Дискриминатор всех проверок —
|
||||
**«консультирует LLM», а не «спавнит subprocess»**.
|
||||
- **Связанные документы:** [`llm-usage-policy.md`](llm-usage-policy.md),
|
||||
[`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).
|
||||
70
docs/architecture/llm-determinization-roadmap.md
Normal file
70
docs/architecture/llm-determinization-roadmap.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# LLM determinization roadmap (ORCH-118)
|
||||
|
||||
> **Что это.** Упорядоченный план детерминированных замен **avoidable LLM control paths**
|
||||
> (`{tester, deployer}` — см. [`llm-call-sites.md`](llm-call-sites.md)). Это **транзиентный план**:
|
||||
> он обновляется по мере закрытия follow-up'ов. ORCH-118 раннеры **не реализует** (FR-7); старт каждого
|
||||
> кандидата гейтится утверждением карты.
|
||||
>
|
||||
> **Кандидаты названы ПО РОЛИ.** Конкретные follow-up Plane-ID **не фиксируются** ни в одном артефакте
|
||||
> (R3 / NFR-6): этих work item нет в подтверждённом backlog; ID присваивается при заведении задачи.
|
||||
> Анти-фабрикация прибита тестом `TC-11` (`tests/test_llm_determinization_docs.py`).
|
||||
>
|
||||
> **Оценки экономии — «оценка до фактического замера» (NFR-5).** Источник — существующие колонки
|
||||
> `agent_runs` (`model`/`effort`/токены/стоимость/время); точные числа снимаются при заведении
|
||||
> follow-up'а через `GET /metrics` / запрос к `agent_runs`. Здесь — порядок величины, а не контракт.
|
||||
|
||||
---
|
||||
|
||||
## 1. Рекомендованный первый срез — **deployer (staging-status)**
|
||||
|
||||
Обоснование (самый низкорисковый «чисто деривируемый» control path):
|
||||
|
||||
1. **Деривируемость максимальна.** Вердикт `staging_status:` — **чистый маппинг** exit-кода
|
||||
`scripts/staging_check.py`; готовый leaf `src/staging_verdict.py::compute_staging_verdict` (ORCH-061)
|
||||
уже считает этот вердикт детерминированно.
|
||||
2. **Прод уже детерминирован.** Ребро `deploy` (`deploy_status:`) для self-hosting `orchestrator`
|
||||
производит детерминированный finalizer (Phase A/B/C, ORCH-036) — LLM в критическом
|
||||
self-restart-пути нет. Срез не трогает критический путь → минимальная поверхность риска.
|
||||
3. **Опирается на прецедент** `D1`/`D2` (`launch_job`-перехват **до** `_spawn`) — архитектурный риск
|
||||
замены агента уже снят рабочим паттерном.
|
||||
4. **`replace-deterministic-now`, без hybrid-fallback** (в отличие от tester).
|
||||
|
||||
## 2. Второй кандидат — **tester (гибрид)**
|
||||
|
||||
Детерминированное ядро (`pytest` + smoke даёт PASS/FAIL по exit-коду) покрывает основной вердикт;
|
||||
LLM-фолбэк сохраняется **только** на суждение, не сводимое к exit-коду: **триаж падений** и **маппинг
|
||||
TC ↔ критерии приёмки**. Поэтому `needs-hybrid-fallback`, а не `replace-deterministic-now`: поверхность
|
||||
суждения шире и объём работы больше.
|
||||
|
||||
## 3. Общие требования к каждому follow-up'у
|
||||
|
||||
Каждый кандидат при заведении задачи несёт: kill-switch + обратимость (паттерн
|
||||
ORCH-022/027/043/089/090 — флаг `*_enabled`, пустой CSV `*_repos` → self-hosting only), скоуп-гард
|
||||
(не трогать `STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict/схему БД), а замена-агента —
|
||||
перехват в `launch_job` **до** `_spawn` (как `D1`/`D2`). Свежесть прецедента — инцидент-трек
|
||||
ORCH-110/111/112/113/114/117 (единое детерминированное владение side-effectful путями).
|
||||
|
||||
---
|
||||
|
||||
## 4. Машинно-читаемый блок roadmap
|
||||
|
||||
> Заголовок (`rank | role | dependencies | savings_estimate_source | security_risk | hybrid_needed |
|
||||
> followup_type | first_slice`) парсится `tests/test_llm_determinization_docs.py::test_tc07_*`.
|
||||
> `followup_type` — **по роли**, без Plane-ID. Ровно один `first_slice = yes`.
|
||||
|
||||
<!-- ORCH-118-ROADMAP-BLOCK:START -->
|
||||
| rank | role | dependencies | savings_estimate_source | security_risk | hybrid_needed | followup_type | first_slice |
|
||||
|------|------|--------------|-------------------------|---------------|---------------|---------------|-------------|
|
||||
| 1 | deployer | staging_verdict.compute_staging_verdict (ORCH-061) + launch_job pre-spawn precedent (D1/D2) | agent_runs (deployer rows; estimate pending GET /metrics) | low (prod already deterministic via Phase A/B/C ORCH-036) | no | deployer-replacement (staging-status mapping) | yes |
|
||||
| 2 | tester | deterministic pytest+smoke core; LLM fallback for failure triage / TC-to-criteria mapping | agent_runs (tester rows; estimate pending GET /metrics) | medium (failure-triage judgment must stay correct) | yes | tester-hybrid (deterministic core + LLM fallback) | no |
|
||||
<!-- ORCH-118-ROADMAP-BLOCK:END -->
|
||||
|
||||
---
|
||||
|
||||
## 5. Вне scope
|
||||
|
||||
- `reviewer` — C, но **keep** (вердикт «приемлемость кода/решения» не деривируем): **не** в roadmap'е.
|
||||
- `analyst`/`architect`/`developer` — P (artifact-producer, не control path): **не** в roadmap'е.
|
||||
- Реализация раннеров — отдельные follow-up задачи (по роли), стартуют после утверждения карты.
|
||||
|
||||
Связанные документы: [`llm-call-sites.md`](llm-call-sites.md), [`llm-usage-policy.md`](llm-usage-policy.md).
|
||||
96
docs/architecture/llm-usage-policy.md
Normal file
96
docs/architecture/llm-usage-policy.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# LLM usage policy (ORCH-118)
|
||||
|
||||
> **Нормативный durable-документ.** Формулирует принцип использования LLM в оркестраторе, критерии
|
||||
> «keep vs replace» через **control-path-ось**, и нормативное **определение «avoidable LLM control
|
||||
> path»**. Применяется ко **всем** будущим правкам control-path'ов. Сопутствующие артефакты —
|
||||
> карта [`llm-call-sites.md`](llm-call-sites.md) и roadmap [`llm-determinization-roadmap.md`](llm-determinization-roadmap.md).
|
||||
|
||||
---
|
||||
|
||||
## 1. Принцип
|
||||
|
||||
**LLM — только там, где нужно настоящее суждение.** Если решение/вердикт control-path'а есть
|
||||
**детерминированная функция tool-сигналов**, которые оркестратор уже вычисляет (exit-code `pytest`,
|
||||
smoke, `staging_check.py`, статус деплоя, наличие файлов, CI-статус), — оно должно приниматься
|
||||
**детерминированно**, а не консультацией LLM. LLM сохраняется там, где требуется суждение, **не
|
||||
сводимое** к tool-сигналу (анализ требований, архитектурное решение, написание кода, приемлемость
|
||||
ревью).
|
||||
|
||||
Это защищает **автономность** (NFR-2): меньше точек, где недетерминизм/стоимость/латентность LLM
|
||||
встроены в поток управления, и меньше класса инцидентов «LLM-агент принял решение, которое на деле
|
||||
есть исполнение фиксированных команд и маппинг результата» (RCA-трек ORCH-110/111/112/113/114/117).
|
||||
|
||||
---
|
||||
|
||||
## 2. Три оси решения (ground-truth — код)
|
||||
|
||||
1. **consultation ≠ transport/slot.** «LLM консультируется» ⇔ решение/артефакт конвейера **потребляет
|
||||
суждение LLM**. Существование транспорта (`_spawn`) или слота агента (job-роли с перехватом до
|
||||
`_spawn`) — это **capability**, не консультация.
|
||||
2. **control-path (C) ≠ artifact-producer (P)** — определяется **кодом-потребителем** вывода роли:
|
||||
- **(C)** LLM эмитит machine-verdict, на котором **ветвится `check_*`-гейт** → суждение входит в
|
||||
поток управления.
|
||||
- **(P)** LLM производит артефакт, а продвижение решает **детерминированный гейт** независимо
|
||||
(наличие файлов / CI) → суждение в control flow не входит.
|
||||
3. **деривируемость вердикта** — вердикт C-консультации либо детерминированная функция tool-сигналов,
|
||||
либо настоящее суждение, не сводимое к exit-коду.
|
||||
|
||||
---
|
||||
|
||||
## 3. Нормативное определение «avoidable LLM control path»
|
||||
|
||||
Это **двухбитный проверяемый предикат над `src/qg/checks.py`**, а не «удобство на глаз».
|
||||
|
||||
<!-- ORCH-118-AVOIDABLE-DEFINITION-BLOCK:START -->
|
||||
Call-site является **avoidable LLM control path** тогда и только тогда, когда выполнены **оба** условия:
|
||||
- **(i)** это **C (control-path)** консультация — её LLM-вердикт потребляется потоком управления
|
||||
(`check_*`-гейт ветвится на нём: PASS → дальше / FAIL → откат);
|
||||
- **(ii)** вердикт **деривируем** (derivable) из tool-сигналов, которые оркестратор уже вычисляет сам —
|
||||
exit-code `pytest` / smoke / `staging_check.py` / статус деплоя.
|
||||
|
||||
Если оба условия выполнены, суждение LLM не добавляет информации → консультацию можно снять без потери
|
||||
смысла (заменить детерминированным раннером или гибридом с LLM-фолбэком только на не-деривируемую часть).
|
||||
<!-- ORCH-118-AVOIDABLE-DEFINITION-BLOCK:END -->
|
||||
|
||||
**Поимённый целевой набор** (сверен с кодом, прибит тестами TC-13/TC-14):
|
||||
|
||||
- **avoidable LLM control paths = `{tester, deployer}`** — C **и** вердикт деривируем
|
||||
(`result:` = exit-code `pytest`+smoke; `staging_status:` = маппинг exit-кода `staging_check.py`).
|
||||
- **`reviewer`** — C, но **keep**: вердикт «приемлемость кода/решения» **НЕ деривируем** из exit-кода
|
||||
(настоящее суждение). Это control-path-но-keep, **не** avoidable.
|
||||
- **`analyst` / `architect` / `developer`** — **не** control path (**P**, artifact-producer):
|
||||
детерминированный гейт судит артефакт независимо.
|
||||
|
||||
---
|
||||
|
||||
## 4. Критерии решения: keep vs replace
|
||||
|
||||
| Ситуация (по осям §2) | Решение | Класс |
|
||||
|-----------------------|---------|-------|
|
||||
| **P** — artifact-producer (детерминированный гейт судит артефакт) | **keep** LLM | `keep-LLM` |
|
||||
| **C**, вердикт **НЕ деривируем** (настоящее суждение) | **keep** LLM (назвать суждение) | `keep-LLM` |
|
||||
| **C**, вердикт **деривируем**, замена безопасна сейчас | **replace** | `replace-deterministic-now` |
|
||||
| **C**, вердикт деривируем, но замена позже / с предпосылками | **replace later** | `replace-later/risky` |
|
||||
| **C**, ядро деривируемо, но часть требует суждения | **hybrid** (детерм. ядро + LLM-фолбэк) | `needs-hybrid-fallback` |
|
||||
|
||||
> **keep-LLM требует обоснования:** любая `keep-LLM`-запись обязана **назвать конкретное суждение**;
|
||||
> для C-keep — явно зафиксировать **не-деривируемость** вердикта (почему не сводится к exit-коду).
|
||||
|
||||
---
|
||||
|
||||
## 5. Требование к новым/изменённым control-path'ам (норматив)
|
||||
|
||||
- **Обоснование против политики.** Любой **новый** или изменённый control-path, который консультирует
|
||||
LLM, обязан в своём ADR обосновать это против настоящей политики: показать, что он **P** (artifact
|
||||
judged independently) **или** **C с не-деривируемым** вердиктом. C-консультация с деривируемым
|
||||
вердиктом — это `avoidable`; её ввод без обоснования reviewer ловит как finding ≥P1.
|
||||
- **Reviewer-ось (как ORCH-079) — требование, не реализация гейта.** Политика **рекомендует**
|
||||
reviewer'у проверять соответствие новых control-path'ов настоящей политике; ORCH-118 **не** вводит
|
||||
новый Quality Gate (`QG_CHECKS`/`check_*` не меняются) — это нормативное требование процесса.
|
||||
- **Норматив сопровождения.** Меняешь место вызова LLM или потребителя вердикта в `src/qg/checks.py` →
|
||||
обнови карту [`llm-call-sites.md`](llm-call-sites.md) и эту политику **в том же PR** (анти-дрейф
|
||||
держат TC-13/TC-14).
|
||||
- **Единственный транспорт.** Единственный разрешённый транспорт LLM-консультации в `src/**` — это
|
||||
`launcher._spawn` (S0). Ввод второго транспорта (новый `_spawn`, импорт `anthropic`/`openai`/иного
|
||||
LLM-SDK, прямой HTTP Anthropic/Claude, второй model-invoking subprocess) запрещён без явного ADR;
|
||||
прибито тестами TC-01/TC-12.
|
||||
@@ -40,6 +40,22 @@
|
||||
- Анти-регресс машинный: структурные тесты сканируют исполняемый код на боевые хост-литералы,
|
||||
а документацию — на секретоподобные значения; находка рвёт CI.
|
||||
|
||||
## Где уместен LLM: карта вызовов и нормативная политика
|
||||
|
||||
Платформа держит **доказательную карту** всех мест, где поток управления потребляет суждение
|
||||
LLM, и **нормативную политику** «LLM — только там, где нужно настоящее суждение». Карта разводит
|
||||
три факта: консультация ≠ транспорт/слот; **control-path** (вердикт LLM ветвит поток управления)
|
||||
≠ **artifact-producer** (детерминированный гейт судит артефакт независимо); и деривируемость
|
||||
вердикта из tool-сигналов. Путь называется **avoidable LLM control path**, когда он одновременно
|
||||
control-path и его вердикт деривируем из exit-кодов (тогда консультацию можно заменить
|
||||
детерминированным раннером). Поимённо: avoidable = `{tester, deployer}`; настоящее суждение
|
||||
сохраняется у `{analyst, architect, developer, reviewer}`. Карта — снимок, прибитый структурными
|
||||
анти-дрейф тестами; реализация замен — отдельные follow-up'ы по роли.
|
||||
|
||||
- Карта вызовов LLM: [`../architecture/llm-call-sites.md`](../architecture/llm-call-sites.md)
|
||||
- Нормативная политика: [`../architecture/llm-usage-policy.md`](../architecture/llm-usage-policy.md)
|
||||
- Порядок замен: [`../architecture/llm-determinization-roadmap.md`](../architecture/llm-determinization-roadmap.md)
|
||||
|
||||
## Self-hosting-страховки
|
||||
|
||||
Платформа дорабатывает сама себя тем же конвейером — прод-инстанс при этом обслуживает и
|
||||
|
||||
7
docs/work-items/ORCH-118/00-business-request.md
Normal file
7
docs/work-items/ORCH-118/00-business-request.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Business Request: ORCH: replace avoidable LLM control paths with deterministic implementations
|
||||
|
||||
Work Item ID: ORCH-118
|
||||
|
||||
## Description
|
||||
|
||||
TBD
|
||||
360
docs/work-items/ORCH-118/01-brd.md
Normal file
360
docs/work-items/ORCH-118/01-brd.md
Normal file
@@ -0,0 +1,360 @@
|
||||
---
|
||||
work_item: ORCH-118
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 01 — BRD (бизнес-требования): ORCH-118 — replace avoidable LLM control paths with deterministic implementations
|
||||
|
||||
Work Item: **ORCH-118** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ⚠️ **Inventory-first.** Это **зонтичная inventory/architecture-задача**, а НЕ реализация
|
||||
> детерминированных раннеров. Её результат — **карта** всех мест вызова LLM + классификация +
|
||||
> упорядоченный roadmap + нормативная политика использования LLM, защищённая структурными тестами.
|
||||
> Реализация конкретных замен — **последующие задачи**, запускаемые ПОСЛЕ утверждения карты. Их код
|
||||
> в ORCH-118 **не вносится** (см. §2 «Вне объёма»).
|
||||
|
||||
> 📌 **Follow-up'ы именуются по РОЛИ, без выдуманных Plane-ID.** Roadmap рекомендует отдельные
|
||||
> follow-up задачи по ролям-кандидатам (**deployer-замена**, **tester-гибрид** и т.д.). Конкретные
|
||||
> Plane-ID этих задач в артефактах ORCH-118 **НЕ фиксируются** — они присваиваются при фактическом
|
||||
> заведении задач в backlog. Аналитик не имеет доказательного источника на конкретные ID и не должен
|
||||
> их выдумывать (см. NFR-6).
|
||||
|
||||
> 🔁 **Revision R3 (2026-06-15).** Из пакета **удалена** нормативная привязка follow-up'ов к
|
||||
> конкретным ID `ORCH-115`/`ORCH-116` (этих work item нет ни в репозитории, ни в подтверждённом
|
||||
> backlog — claim «источник истины: live Plane backlog» был **непроверяемым**). Вместе с ней удалены
|
||||
> навязывавший её структурный тест (бывш. TC-11) и отдельный критерий приёмки (бывш. AC-9). Follow-up'ы
|
||||
> теперь описаны **по роли**, ID — TBD. Содержательная классификация ролей не менялась
|
||||
> (deployer = кандидат на детерминированную замену, tester = кандидат на hybrid-fallback).
|
||||
> *(R1→R2 ранее «чинил» порядок этих ID; R3 убирает корень проблемы — фиксацию несуществующих ID.)*
|
||||
|
||||
> 🔁 **Revision R4 (2026-06-15).** Закрыт блокер R3-ревью: инвариант «места вызова LLM» **смешивал**
|
||||
> факт *«существует процесс Claude CLI / спавнится subprocess»* (транспорт/механизм) с фактом
|
||||
> *«потребляется суждение LLM»* (LLM-консультация). R4 развёл **транспорт** (`_spawn`) и **слот/
|
||||
> capability** (D1/D2) от факта **консультации** во всех артефактах (см. §0). Содержательная
|
||||
> классификация ролей и весь R3-материал (анти-фабрикация ID) — без изменений.
|
||||
|
||||
> 🔁 **Revision R5 (2026-06-15) — единственный оставшийся блокер R4-ревью.** Рецензент подтвердил R4
|
||||
> (консультация ≠ транспорт/слот; no-alternative-LLM-transport; follow-up'ы по роли/TBD; нет
|
||||
> runtime-дифа) — **менять их не нужно**. Закрывается **один** блокер: артефакты разводили
|
||||
> «консультация ≠ транспорт/слот», но **не делали явной третью ось — самую важную для названия
|
||||
> задачи** *«replace avoidable LLM **control paths**»*: среди фактических консультаций **не
|
||||
> различались** (i) **control-path-консультации** — где LLM-вердикт **потребляется потоком управления**
|
||||
> (на нём ветвится `check_*`-гейт), и (ii) **artifact-producer-консультации** — где LLM лишь
|
||||
> **производит артефакт**, а ветвление делает **детерминированный гейт** (наличие файлов / CI), и
|
||||
> суждение LLM в control flow **не входит**. И главное — нигде **явно не определена «avoidable LLM
|
||||
> control path»**. R5 добавляет эту ось и определение во **все** артефакты (новый §0-bis, уточнённые
|
||||
> BR-1/BR-2 + новые BR-8/BR-9, FR-1/FR-2 + новый FR-8, AC-1/AC-2 + новый AC-10, TC-13/TC-14).
|
||||
> Содержательная классификация (analyst/architect/developer/reviewer → keep-LLM; deployer →
|
||||
> replace-deterministic; tester → hybrid) **не меняется** — R5 даёт ей **доказательный control-path
|
||||
> вывод** из кода (`check_*`/`_parse_*` в `src/qg/checks.py`).
|
||||
|
||||
---
|
||||
|
||||
## 0. Единица анализа: «LLM-консультация» ≠ «процесс Claude CLI» (R4)
|
||||
|
||||
Задача — про **LLM control paths**, поэтому единица инвентаря/классификации/инвариантов — это
|
||||
**LLM-консультация (call-site)**, а НЕ «спавн процесса / существование Claude CLI». Три ортогональных
|
||||
факта фиксируются **раздельно** (их смешение и было блокером R3-ревью):
|
||||
|
||||
1. **LLM-консультация (семантическая единица).** Точка потока управления, где решение/артефакт
|
||||
конвейера **потребляет суждение LLM** (инференс модели). Именно её перечисляет и классифицирует
|
||||
карта. «Заменить avoidable LLM control path» = убрать *консультацию* там, где суждение не нужно, —
|
||||
независимо от того, каким транспортом она реализована.
|
||||
2. **Транспорт (механизм) ≠ консультация.** Claude CLI-subprocess через `launcher._spawn`
|
||||
(`src/agents/launcher.py`, сборка `CLAUDE_BIN --print … --system-prompt "$(cat …)"` + `Popen`) —
|
||||
**текущий единственный транспорт**, которым реализуется LLM-консультация. Транспорт — это одна
|
||||
*реализация* понятия, а НЕ его *определение*. «Существует процесс Claude CLI» само по себе **не**
|
||||
равно «принято решение на основе суждения LLM».
|
||||
3. **Capability ≠ consultation (способность ≠ факт консультации).** Спавн процесса делает site
|
||||
*LLM-capable*; происходит ли консультация фактически — **гейтится потоком управления**:
|
||||
- `D1/D2` (`deploy-finalizer`/`post-deploy-monitor`) — job-роли, **занимающие слот агента**, но
|
||||
перехватываемые в `launch_job` **до** `_spawn` (`src/agents/launcher.py:389,394`, код прямо
|
||||
помечает «Not an LLM spawn») → **консультации LLM нет**, хотя «агент» как job существует;
|
||||
- timeout-kill (`exit_code=-9`) / salvage-guard (`if exit_code==0`) → спавненный процесс может **не
|
||||
произвести** потреблённого суждения.
|
||||
Поэтому «процесс спавнится» **переоценивает** «суждение потреблено».
|
||||
|
||||
Следствие для инварианта единственной точки (детализируется в BR-1/BR-6): он должен быть
|
||||
**транспорт-агностичным и двусторонним** — (i) единственный транспорт LLM-консультации в `src/**` —
|
||||
`_spawn`, И (ii) **отсутствует любой иной транспорт** (импорт `anthropic`/`openai`/LLM-SDK, прямой
|
||||
HTTP-эндпоинт Anthropic/Claude, второй model-invoking subprocess-сборщик `--system-prompt`/`--model`).
|
||||
Дискриминатор — **«консультирует LLM», а не «спавнит subprocess»**: десятки subprocess-вызовов в
|
||||
`src/**` (`git`/`pytest`/`docker`/`ssh`/сканеры/`staging_check.py`) **не** являются LLM-консультациями
|
||||
и не должны попадать под инвариант (см. FR-6 матчинг).
|
||||
|
||||
---
|
||||
|
||||
## 0-bis. Третья ось: control-path-консультация ≠ artifact-producer-консультация; что такое «avoidable» (R5)
|
||||
|
||||
§0 (R4) отделил **факт консультации** от **транспорта** и **слота**. Но название задачи —
|
||||
«replace **avoidable** LLM **control paths**» — требует ещё одного, **решающего** различия **внутри
|
||||
множества фактических консультаций** (A1…A6). Без него «control path» и «avoidable» остаются
|
||||
неопределёнными, и карта не отвечает на главный вопрос задачи: *какие именно консультации — это
|
||||
avoidable LLM control paths*. Это и есть оставшийся блокер R4-ревью.
|
||||
|
||||
### Ось 3 — как именно LLM-вывод соотносится с потоком управления
|
||||
|
||||
Каждая фактическая консультация (роль-агент) относится **ровно к одному** из двух типов:
|
||||
|
||||
- **(C) Control-path-консультация.** LLM эмитит **machine-verdict**, который **потребляется потоком
|
||||
управления конвейера**: соответствующий `check_*`-гейт **ветвится на этом вердикте**
|
||||
(PASS→дальше / FAIL→откат). Суждение LLM **входит в control flow** — это и есть «**LLM control
|
||||
path**» в точном смысле названия задачи.
|
||||
- **(P) Artifact-producer-консультация.** LLM **производит артефакт** (документы/код/PR), а решение
|
||||
о продвижении принимает **детерминированный гейт**, судящий артефакт **независимо** от
|
||||
самоотчёта LLM (наличие файлов, статус CI). Суждение LLM **в control flow не входит** → это **не**
|
||||
«LLM control path» (хотя консультация реальна и может требовать настоящего суждения).
|
||||
|
||||
Различие доказывается кодом — **кто потребляет вывод роли** (`src/qg/checks.py`, ground-truth на
|
||||
момент задачи):
|
||||
|
||||
| Роль | Потребитель вывода (control-flow consumer) | Тип (C/P) | Control path? |
|
||||
|------|--------------------------------------------|-----------|---------------|
|
||||
| analyst | `check_analysis_complete` (checks.py:33) — **наличие файлов** 01–04 | **P** | нет |
|
||||
| architect | `check_architecture_done` (checks.py:62) — **наличие** 06-adr/07 | **P** | нет |
|
||||
| developer | `check_ci_green` (checks.py:82) + `check_branch_mergeable` (657) — **CI/merge** | **P** | нет |
|
||||
| reviewer | `check_reviewer_verdict` (checks.py:336) читает **`verdict:`** → REQUEST_CHANGES-откат | **C** | **да** |
|
||||
| tester | `check_tests_passed` (checks.py:182) → `_parse_tests_verdict` (226) читает **`result:`** | **C** | **да** |
|
||||
| deployer | `check_staging_status` (599)→`_parse_staging_status` (538) **`staging_status:`**; `check_deploy_status` (473)→`_parse_deploy_status` (413) **`deploy_status:`** | **C** | **да** |
|
||||
|
||||
> Для **P**-ролей гейт читает **не самоотчёт LLM**, а независимый детерминированный сигнал
|
||||
> (файлы/CI) — поэтому подделать ветвление «самооценкой» нельзя; это структурно НЕ control path.
|
||||
> Для **C**-ролей гейт читает **именно machine-verdict, который написал LLM** — суждение LLM и есть
|
||||
> точка ветвления.
|
||||
|
||||
### Определение «avoidable LLM control path» (нормативное, R5)
|
||||
|
||||
Call-site — **avoidable LLM control path** ⟺ выполнены **оба** условия:
|
||||
|
||||
1. **(control path)** это **C**-консультация — её LLM-вердикт потребляется потоком управления
|
||||
(`check_*` ветвится на нём); **и**
|
||||
2. **(deterministically derivable)** этот вердикт по сути есть **детерминированная функция от
|
||||
tool-сигналов** (exit-code `pytest`/smoke, `staging_check.py`, exit-code деплоя), которые
|
||||
оркестратор **уже вычисляет сам** → суждение LLM **не добавляет информации** → консультацию можно
|
||||
снять без потери смысла.
|
||||
|
||||
Отсюда — **точный целевой набор задачи** (доказательно, не «на глаз»):
|
||||
|
||||
- **avoidable LLM control paths = {tester, deployer}** — C **и** вердикт деривируем из exit-кодов
|
||||
(`_parse_tests_verdict` судит то, что есть исход `pytest`; `_parse_staging_status`/
|
||||
`_parse_deploy_status` судят то, что есть исход `staging_check.py`/деплоя; прод-деплой
|
||||
self-hosting **уже** идёт детерминированным путём Phase A/B/C, ORCH-036).
|
||||
- **control path, но НЕ avoidable = {reviewer}** — C, но вердикт **не** деривируем: «приемлем ли
|
||||
код?» — настоящее суждение (keep-LLM). Это показывает, что «control path» **сам по себе** не равен
|
||||
«avoidable» — отсекает условие 2.
|
||||
- **НЕ control path (avoidable-вопрос неприменим) = {analyst, architect, developer}** — P:
|
||||
детерминированный гейт судит артефакт, суждение LLM в control flow не входит; авторская работа
|
||||
требует настоящего суждения (keep-LLM). Это отсекает условие 1.
|
||||
- **уже детерминированы (вне консультаций) = {deploy-finalizer, post-deploy-monitor}** — §0.3.
|
||||
|
||||
Эта ось **выводит** R3/R4-классификацию из кода, а не постулирует её: класс call-site'а есть
|
||||
**функция** его (C/P)-типа и деривируемости вердикта (см. BR-2). «Avoidable» больше не «удобство на
|
||||
глаз», а проверяемый двухбитный предикат над `src/qg/checks.py`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Бизнес-контекст и проблема
|
||||
|
||||
Зонтичный follow-up по итогам RCA-цепочки **ORCH-114/117** (и предшествующих ORCH-110/111/112/113):
|
||||
корневым классом инцидентов было то, что **side-effectful и решающие control-path'ы не имели единого
|
||||
детерминированного владения** — где-то решение принималось «потому что так удобно» через LLM-агента,
|
||||
хотя по сути это исполнение фиксированных команд и маппинг результата.
|
||||
|
||||
Установленный факт по текущему коду (инвентаризация — §1 TRZ, артефакт-карта):
|
||||
|
||||
- В оркестраторе **единственный транспорт LLM-консультации** — `src/agents/launcher.py::_spawn` (одна
|
||||
`subprocess.Popen` сборка Claude CLI: `CLAUDE_BIN --print … --system-prompt "$(cat …)"`), которым
|
||||
пользуются **6 ролей-агентов** (analyst, architect, developer, reviewer, tester, deployer).
|
||||
Прямых вызовов Anthropic API / LLM-SDK / иных model-invoking транспортов в `src/**` **нет**
|
||||
(подтверждено инвентаризацией; впредь — держится тестом FR-6). ⚠️ Это утверждение про **транспорт**,
|
||||
а не про «единственный subprocess»: в `src/**` десятки прочих `subprocess`-вызовов (`git`/`pytest`/
|
||||
`docker`/`ssh`/сканеры) — они **не** консультируют LLM (см. §0).
|
||||
- **Из 6 консультаций только 3 — это LLM control paths** (вердикт потребляется потоком управления,
|
||||
§0-bis): **reviewer / tester / deployer**. Остальные 3 (**analyst / architect / developer**) —
|
||||
artifact-producer'ы: их выход судит **детерминированный** гейт (наличие файлов / CI), суждение LLM
|
||||
в control flow не входит. Среди 3 control path'ов **avoidable** — те, чей вердикт деривируем из
|
||||
exit-кодов: **tester** и **deployer**; **reviewer** — control path с **настоящим** суждением
|
||||
(keep).
|
||||
- **Все остальные control-path'ы уже детерминированы (без LLM):** маршрутизация стадий
|
||||
(`STAGE_TRANSITIONS`/`advance_stage`), все Quality Gate'ы и под-гейты (`check_*`, security/merge/
|
||||
coverage/image-freshness), парсеры вердиктов (`_parse_*` через `frontmatter.py`), классификатор
|
||||
ретраев (`error_classifier.py`), serial-gate/transition-lease/reconciler/reaper, а также **две
|
||||
зарезервированные job-роли** `deploy-finalizer` и `post-deploy-monitor` (перехватываются в
|
||||
`launch_job` **до** `_spawn` — это рабочий прецедент детерминированной замены агента).
|
||||
|
||||
Боль/риск, который закрывает задача: LLM на **механических control path'ах** — это (а) лишний
|
||||
источник недетерминизма и инцидентов (ложный вердикт/действие в точке ветвления), (б) задержка
|
||||
(запуск opus-агента вместо прямого вызова), (в) расход токенов/денег. При этом «вслепую» убирать LLM
|
||||
нельзя — часть путей несёт **настоящее суждение** (анализ, архитектура, написание кода, **ревью**), и
|
||||
автономность/гибкость должны сохраниться. Точный дискриминатор «убирать/оставить» — определение
|
||||
«avoidable LLM control path» (§0-bis).
|
||||
|
||||
ORCH-118 даёт **доказательную карту** «где LLM действительно нужен, а где это avoidable control path»
|
||||
и **упорядоченный план** безопасных замен — фундамент, на котором последующие срезы (по
|
||||
ролям-кандидатам) выполняются предсказуемо и без регресса.
|
||||
|
||||
## 2. Объём (scope)
|
||||
|
||||
### В объёме
|
||||
- **BR-1** Полная инвентаризация всех мест вызова LLM и всех ролей-агентов (карта call-site'ов).
|
||||
- **BR-2** Классификация каждого call-site в один из 4 классов (keep / replace-now / replace-later /
|
||||
hybrid-fallback) с явным обоснованием, **выведенным** из control-path-оси (§0-bis): класс есть
|
||||
функция (C/P)-типа и деривируемости вердикта.
|
||||
- **BR-3** Доказательное подтверждение (с привязкой `file:line`), что не-агентские control-path'ы
|
||||
(маршрутизация / ретраи / QG / парсеры / finalizer'ы) уже детерминированы.
|
||||
- **BR-4** Упорядоченный roadmap замен: зависимости, оценка экономии токенов/времени, риски
|
||||
безопасности, потребность в hybrid-fallback, рекомендованный **первый срез** — кандидаты названы
|
||||
**по роли** (follow-up ID — TBD).
|
||||
- **BR-5** Нормативная **политика использования LLM** («LLM — только там, где нужно настоящее
|
||||
суждение») как durable-документ.
|
||||
- **BR-6** Структурные regression-тесты, **прибивающие инварианты карты к коду** (транспорт-агностичный
|
||||
двусторонний инвариант + control-path-ось) — анти-дрейф.
|
||||
- **BR-7** Явно позиционировать **роль deployer** и **роль tester** как **кандидаты-follow-up** для
|
||||
детерминированной замены — **по роли, без привязки к конкретным Plane-ID** (см. NFR-6).
|
||||
- **BR-8 (R5)** Карта **явно** размечает каждую консультацию по оси (C) control-path /
|
||||
(P) artifact-producer с доказательством — **кто потребляет вывод роли** (`check_*`/`_parse_*` с
|
||||
`file:line`).
|
||||
- **BR-9 (R5)** Карта/политика **явно** определяют термин **«avoidable LLM control path»** (двухбитный
|
||||
предикат §0-bis) и **поимённо** называют целевой набор `{tester, deployer}`, явно отделяя его от
|
||||
control-path-но-keep (`reviewer`) и от не-control-path (`analyst`/`architect`/`developer`).
|
||||
|
||||
### Вне объёма
|
||||
- ❌ **Реализация** детерминированных раннеров deployer / tester и любых других замен — это отдельные
|
||||
follow-up задачи ПОСЛЕ утверждения карты (явное требование заказчика в business request).
|
||||
- ❌ **Выдумывание/фиксация конкретных follow-up Plane-ID** (напр. `ORCH-115`/`ORCH-116`) в любых
|
||||
артефактах ORCH-118 — ID присваиваются при заведении задач (NFR-6).
|
||||
- ❌ Изменение `STAGE_TRANSITIONS` / реестра `QG_CHECKS` / семантики и имён `check_*` /
|
||||
machine-verdict-ключей (`verdict:`/`result:`/`staging_status:`/`deploy_status:`/`security_status:`/
|
||||
`coverage_status:`) / схемы БД.
|
||||
- ❌ Включение model-routing (G3 ORCH-41) или смена модели/эффорта ролей.
|
||||
- ❌ Любая правка поведения `src/**` в рантайме (ORCH-118 — **docs + tests only**, по образцу
|
||||
ORCH-077/079/101/102/103/011).
|
||||
- ❌ Снижение автономности или гибкости конвейера.
|
||||
|
||||
## 3. Заинтересованные стороны
|
||||
- **Заказчик / Owner** — инициатор RCA-трека ORCH-114/117; принимает карту и roadmap (gate утверждения).
|
||||
- **Сопровождающие платформы (self-hosting)** — выигрывают в стабильности, скорости, экономии токенов.
|
||||
- **Downstream-проекты (enduro-trails)** — делят общий прод/очередь; для них требуется **нулевая
|
||||
регрессия** (NFR-1).
|
||||
- **Будущие исполнители follow-up'ов** — потребители карты, roadmap и политики.
|
||||
|
||||
## 4. Бизнес-требования (BR)
|
||||
|
||||
- **BR-1 — Инвентарь LLM-консультаций (call-site'ов).** Выпустить durable-документ, перечисляющий
|
||||
**каждое** место, где control-path **потребляет суждение LLM** или может его потребить (единица —
|
||||
*LLM-консультация*, §0, а не «спавн процесса»): единственный транспорт `_spawn`, все 6 ролей-агентов
|
||||
и обе зарезервированные job-роли `D1/D2` (включаются как доказательство «слот агента есть, но
|
||||
консультации LLM нет» — перехват до `_spawn`). Для каждого — `file:line`, триггер, стадия/владелец,
|
||||
выходной артефакт, machine-verdict-ключ (если есть), **потребитель вывода (`check_*`/`_parse_*` с
|
||||
`file:line`)**, оценка токенов/времени, **признак capability-vs-consultation** (§0.3) и **признак
|
||||
оси (C) control-path / (P) artifact-producer** (§0-bis, BR-8). Проверяемо: каждый `file:line`
|
||||
резолвится в реальный код.
|
||||
- **BR-2 — Классификация.** Каждому call-site присвоить **ровно один** класс из таксономии:
|
||||
`keep-LLM` (нужно настоящее суждение), `replace-deterministic-now` (безопасная замена сейчас),
|
||||
`replace-later/risky` (замена позже / рискованно), `needs-hybrid-fallback` (детерминированное ядро +
|
||||
LLM-фолбэк на суждение). Класс **выводится** из control-path-оси (§0-bis): **P** → `keep-LLM`
|
||||
(артефактная авторская работа); **C + не-деривируемый вердикт** → `keep-LLM` (reviewer); **C +
|
||||
деривируемый вердикт** → `replace-*` / `needs-hybrid-fallback` (tester/deployer = avoidable). Для
|
||||
`keep-LLM` — **назвать конкретное суждение**, ради которого LLM сохраняется (для **C**-keep —
|
||||
почему вердикт **не** деривируем).
|
||||
- **BR-3 — Подтверждение детерминизма не-агентских путей.** Документально, с `file:line`-доказательством,
|
||||
зафиксировать, что маршрутизация стадий, ретраи, QG-проверки, парсеры вердиктов и finalizer'ы **не
|
||||
консультируют LLM** (не зависят от суждения LLM — ни через `_spawn`, ни через иной транспорт; их
|
||||
subprocess-вызовы git/pytest/docker/ssh/сканеров — детерминированные инструменты, не LLM, §0). Если
|
||||
инвентаризация найдёт неожиданную LLM-консультацию — она попадает в карту (BR-1) и классификацию
|
||||
(BR-2).
|
||||
- **BR-4 — Упорядоченный roadmap.** Ранжированный план замен: для каждого кандидата (названного **по
|
||||
роли**) — зависимости, **оценка** экономии токенов/времени (из телеметрии `agent_runs`), риск
|
||||
безопасности, нужен ли hybrid-fallback, ожидание kill-switch/обратимости. Явно указать
|
||||
**рекомендованный первый срез** и обоснование выбора (опора на control-path-вывод: первым берётся
|
||||
самый «чисто деривируемый» control path). Привязка к follow-up задаче — **по роли**; конкретный
|
||||
Plane-ID НЕ фиксируется (заводится отдельно, NFR-6).
|
||||
- **BR-5 — Политика использования LLM.** Нормативный durable-документ: «LLM — только там, где требуется
|
||||
настоящее суждение»; критерии решения keep vs replace, **сформулированные через ось §0-bis**
|
||||
(является ли путь control path; деривируем ли вердикт); требование к новым/изменённым control-path'ам
|
||||
обосновывать любое использование LLM против этой политики.
|
||||
- **BR-6 — Анти-дрейф структурными тестами.** Тесты, привязывающие инварианты карты к коду:
|
||||
транспорт-агностичный двусторонний инвариант единственной точки (§0); перечисленные
|
||||
детерминированные модули/job-роли не несут LLM-консультации; карта перечисляет ровно те 6 промптов,
|
||||
что лежат в `.openclaw/agents/`; тотальность классификации; **плюс control-path-ось (R5):** карта
|
||||
размечает каждую роль (C/P) согласованно с фактическим потребителем в `src/qg/checks.py`, а
|
||||
avoidable-набор = `{tester, deployer}`. Тесты — offline. **Тест на привязку к конкретным follow-up
|
||||
ID не вводится** (анти-паттерн R3).
|
||||
- **BR-7 — Позиционирование follow-up'ов по роли.** Карта/roadmap явно отмечают **роль deployer** и
|
||||
**роль tester** как кандидаты-замены, **не** реализуемые в ORCH-118; их старт гейтится утверждением
|
||||
карты. Привязка — **по роли**, без конкретных Plane-ID (NFR-6).
|
||||
- **BR-8 (R5) — Явная control-path-разметка.** Карта несёт для каждой консультации поле оси
|
||||
**(C) control-path / (P) artifact-producer** + доказательство (потребитель вывода с `file:line`).
|
||||
Разметка **проверяема** структурным тестом против `src/qg/checks.py` (BR-6, TC-13).
|
||||
- **BR-9 (R5) — Явное определение «avoidable LLM control path» и поимённый целевой набор.** Карта/
|
||||
политика дают нормативное определение термина (двухбитный предикат §0-bis) и **поимённо** называют
|
||||
`{tester, deployer}` как avoidable, явно отделяя их от `reviewer` (control path, keep) и от
|
||||
`analyst/architect/developer` (не control path). Набор **проверяем** тестом (BR-6, TC-14).
|
||||
|
||||
## 5. Нефункциональные требования (NFR)
|
||||
|
||||
- **NFR-1 — Сохранение поведения (нулевая регрессия).** ORCH-118 — docs+tests only:
|
||||
`STAGE_TRANSITIONS`/`QG_CHECKS`/`check_*`/machine-verdict-ключи/схема БД — **байт-в-байт**; поведение
|
||||
конвейера 1:1; enduro-trails не затронут.
|
||||
- **NFR-2 — Сохранение автономности и гибкости.** Ни инвентаризация, ни политика не должны
|
||||
предлагать решений, снижающих автономный/пакетный режим (ORCH-088/089) или гибкость; политика
|
||||
**защищает** автономность как инвариант любой будущей замены.
|
||||
- **NFR-3 — Self-hosting безопасность.** Задача только **читает** код и пишет docs+tests — не
|
||||
деплоит, не рестартит прод-контейнер, не трогает `main`/force-push, не запускает процессов/сети.
|
||||
- **NFR-4 — Трассируемость и сопровождаемость.** Карта привязана к коду маркерами/тестом и остаётся
|
||||
честной при эволюции кода; формат — по `docs/_standards/PIPELINE_DOCS.md` и `TRACEABILITY.md`.
|
||||
- **NFR-5 — Доказательность экономии.** Цифры экономии берутся из реальной телеметрии `agent_runs`
|
||||
(модель/эффорт/токены/стоимость/время по ролям) и помечаются как **оценки** до фактического замера
|
||||
после реализации.
|
||||
- **NFR-6 — Только проверяемые ссылки (анти-фабрикация, R3).** В артефактах фиксируются только
|
||||
ссылки, резолвящиеся в код/документы репозитория. Конкретные **follow-up Plane-ID не выдумываются**:
|
||||
кандидаты-замены именуются по роли; ID присваивается при заведении задачи. (Это закрывает корень
|
||||
отклонённой ревизии R2.)
|
||||
- **NFR-7 (R5) — Контроль-ориентированность определений.** «LLM control path» и «avoidable»
|
||||
определяются **через потребление вывода потоком управления** (кто из `check_*` ветвится на выводе),
|
||||
а не через «есть ли вообще LLM-вызов на стадии». Любое утверждение «это avoidable LLM control path»
|
||||
обязано резолвиться в конкретный `check_*`/`_parse_*` + tool-сигнал, из которого вердикт деривируем.
|
||||
|
||||
## 6. Допущения и ограничения
|
||||
- Единственный транспорт LLM сейчас — Claude CLI через `launcher._spawn`; прямых вызовов Anthropic API
|
||||
в `src/**` нет (подтверждается инвентаризацией).
|
||||
- Model-routing не включён — все 6 ролей на `claude-opus-4-8` (ORCH-41), что упрощает оценку экономии.
|
||||
- Карта — **снимок на момент задачи**, защищённый структурными тестами от тихого расхождения с кодом.
|
||||
- Прецедент детерминированной замены агента уже существует и работает (`deploy-finalizer`/
|
||||
`post-deploy-monitor` в `launch_job` до `_spawn`) — это снижает архитектурный риск follow-up'ов.
|
||||
- На момент анализа конкретные follow-up work item для замены ролей в backlog **не подтверждены** —
|
||||
поэтому ID не фиксируются (NFR-6).
|
||||
- **(R5)** Control-path-разметка опирается на ground-truth `src/qg/checks.py` на момент задачи; при
|
||||
эволюции кода её честность держит структурный тест (TC-13/TC-14).
|
||||
|
||||
## 7. Критерии успеха
|
||||
Карта LLM-консультаций полна и привязана к коду, и **разводит три ортогональных факта**: транспорт/слот
|
||||
(«процесс Claude CLI существует», R4 §0), факт консультации, **и — control-path-ось (R5 §0-bis):
|
||||
потребляется ли LLM-вывод потоком управления (`check_*` ветвится на нём) или его независимо судит
|
||||
детерминированный гейт**. Термин **«avoidable LLM control path» явно определён** (двухбитный предикат)
|
||||
и целевой набор **поимённо** назван `{tester, deployer}` с доказательным отделением от `reviewer`
|
||||
(control path, keep) и от `analyst/architect/developer` (не control path). Каждый site классифицирован
|
||||
с обоснованием, **выведенным** из этой оси; детерминизм не-агентских путей доказан (отсутствием
|
||||
LLM-консультации, не отсутствием subprocess); есть упорядоченный roadmap с зависимостями/экономией/
|
||||
рисками и рекомендованным первым срезом (кандидаты — по роли); есть нормативная политика; структурные
|
||||
тесты зелёные и осмысленные (инвариант единственной точки транспорт-агностичен и двусторонен; control-path
|
||||
-разметка сверена с `src/qg/checks.py`; avoidable-набор зафиксирован); ни один рантайм-инвариант не
|
||||
тронут; раннеры замен НЕ реализованы; ни один артефакт не фиксирует непроверяемых follow-up ID.
|
||||
Детальные PASS/FAIL — в `03-acceptance-criteria.md`.
|
||||
|
||||
## 8. Риски
|
||||
- **Недо-/пере-классификация** (LLM убран там, где нужно суждение, или сохранён там, где не нужен) →
|
||||
митигирует **control-path-вывод** (§0-bis: класс выводится из (C/P)-типа и деривируемости вердикта,
|
||||
а не «на глаз») + требование «назвать конкретное суждение» для `keep-LLM` + ревью карты.
|
||||
- **Конфляция «есть LLM на стадии» с «это control path»** (рецидив корня R4-блокера) → митигирует
|
||||
явная разметка оси и тест TC-13 (сверка с фактическим потребителем `check_*`).
|
||||
- **Дрейф карты** относительно кода со временем → митигируют структурные тесты (BR-6), включая
|
||||
control-path-инварианты (TC-13/TC-14).
|
||||
- **Преждевременная замена** в погоне за экономией ценой автономности/гибкости → инвентаризация
|
||||
отделена от реализации; первый срез — самый низкорисковый «чисто деривируемый» control path.
|
||||
- **Фабрикация ссылок/ID** (рецидив дефекта R2) → митигирует NFR-6 (только проверяемые ссылки;
|
||||
follow-up'ы — по роли) и ревью. Детали техн.рисков — `10-tech-risks.md` (архитектор).
|
||||
234
docs/work-items/ORCH-118/02-trz.md
Normal file
234
docs/work-items/ORCH-118/02-trz.md
Normal file
@@ -0,0 +1,234 @@
|
||||
---
|
||||
work_item: ORCH-118
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 02 — ТЗ (TRZ): ORCH-118 — replace avoidable LLM control paths with deterministic implementations
|
||||
|
||||
Work Item: **ORCH-118** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
> ТЗ описывает **что** должно измениться и **где** (артефакты/контракты/тесты), выведено из BRD и
|
||||
> фактического кода. **Как** (точная структура/размещение документов карты, формат классификации,
|
||||
> схема структурных тестов) — решает архитектор в `06-adr/`. ТЗ фиксирует требования и границы.
|
||||
>
|
||||
> ⚠️ **Скоуп — inventory + классификация + roadmap + политика + структурные тесты.** Реализация
|
||||
> детерминированных раннеров — **вне скоупа** (FR-7). Это **docs + tests only**: `src/**`-рантайм не
|
||||
> меняется.
|
||||
>
|
||||
> 📌 **Follow-up'ы — по роли, без выдуманных Plane-ID** (R3, NFR-6). Конкретные ID (напр.
|
||||
> `ORCH-115`/`ORCH-116`) в артефактах **не фиксируются** — этих work item нет в репо/подтверждённом
|
||||
> backlog; ID присваивается при заведении задачи.
|
||||
>
|
||||
> 🔁 **R4 — блокер R3-ревью (закрыт).** Единица инвентаря/инварианта — **LLM-консультация** (control-path
|
||||
> потребляет суждение LLM), **не** «спавн процесса / существование Claude CLI». Claude CLI-subprocess
|
||||
> через `_spawn` — лишь **текущий транспорт**; «процесс существует» ≠ «LLM консультирован»
|
||||
> (capability ≠ consultation). Развёрнуто — BRD §0; затрагивает FR-1/FR-3/FR-6.
|
||||
>
|
||||
> 🔁 **R5 — единственный оставшийся блокер R4-ревью.** Артефакты разводили «консультация ≠ транспорт/
|
||||
> слот», но **не делали явной control-path-ось** — самую важную для названия задачи: среди реальных
|
||||
> консультаций **не различались** (C) control-path (LLM-вердикт потребляется потоком управления —
|
||||
> `check_*` ветвится на нём) и (P) artifact-producer (детерминированный гейт судит артефакт; суждение
|
||||
> LLM в control flow не входит); и нигде **не был определён** термин **«avoidable LLM control path»**.
|
||||
> R5 добавляет ось + определение (BRD §0-bis) и тянет их в FR-1/FR-2 + новый **FR-8**, в инвентарь
|
||||
> §1 (новая колонка), в тесты FR-6 (**TC-13/TC-14**). Содержательная классификация не меняется —
|
||||
> R5 **выводит** её из `src/qg/checks.py`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Сводка изменения
|
||||
|
||||
Выпустить **доказательную карту** всех мест вызова LLM в оркестраторе с классификацией и
|
||||
упорядоченным roadmap'ом детерминированных замен, а также нормативную **политику использования LLM**;
|
||||
прибить инварианты карты к коду набором **структурных тестов**. Реализация замен не входит. Все
|
||||
рантайм-контракты (`STAGE_TRANSITIONS` / `QG_CHECKS` / `check_*` / machine-verdict-ключи / схема БД) —
|
||||
**не меняются**.
|
||||
|
||||
Опорный факт инвентаризации (ground-truth кода на момент задачи; line-привязки уточняет карта):
|
||||
|
||||
> Единица — **LLM-консультация** (потребление суждения LLM), а не «спавн процесса» (R4, BRD §0).
|
||||
> R5: дополнительно размечается **ось (C) control-path / (P) artifact-producer** и **потребитель
|
||||
> вывода** (`check_*`/`_parse_*`), доказывающий ось. Колонка `Консультирует LLM?` помечает транспорт/
|
||||
> слот vs факт консультации; колонка `Control path?` помечает, входит ли LLM-вывод в поток управления.
|
||||
|
||||
| # | Call-site | Где | Что делает | Консультирует LLM? | Потребитель вывода (control-flow consumer) | Control path? (C/P) | Avoidable LLM control path? |
|
||||
|---|-----------|-----|------------|--------------------|--------------------------------------------|---------------------|-----------------------------|
|
||||
| S0 | **Единственный транспорт LLM-консультации** | `src/agents/launcher.py::_spawn` (`CLAUDE_BIN --print … --system-prompt "$(cat …)"` + `Popen`) | реализует консультацию для любой из 6 ролей | транспорт (capability) | — | — | — (транспорт, не call-site решения) |
|
||||
| A1 | analyst | `.openclaw/agents/analyst.md`, стадия `analysis` | анализ → 01–04 | да (через S0) | `check_analysis_complete` (`src/qg/checks.py:33`) — **наличие файлов** | **P** (artifact-producer) | **нет** (не control path) → keep-LLM |
|
||||
| A2 | architect | `.openclaw/agents/architect.md`, стадия `architecture` | архитектура → 06-adr | да (через S0) | `check_architecture_done` (`checks.py:62`) — **наличие** 06-adr/07 | **P** | **нет** → keep-LLM |
|
||||
| A3 | developer | `.openclaw/agents/developer.md`, стадия `development` | реализация + PR | да (через S0) | `check_ci_green` (`checks.py:82`) + `check_branch_mergeable` (`checks.py:657`) — **CI/merge** | **P** | **нет** → keep-LLM |
|
||||
| A4 | reviewer | `.openclaw/agents/reviewer.md`, стадия `review` | ревью → `12-review.md` (`verdict:`) | да (через S0) | `check_reviewer_verdict` (`checks.py:336`) читает **`verdict:`** → REQUEST_CHANGES-откат | **C** (control path) | **нет** (вердикт НЕ деривируем — настоящее суждение) → keep-LLM |
|
||||
| A5 | tester | `.openclaw/agents/tester.md`, стадия `testing` | `pytest`+smoke → `13-test-report.md` (`result:`) | да (через S0) | `check_tests_passed` (`checks.py:182`) → `_parse_tests_verdict` (`checks.py:226`) читает **`result:`** | **C** | **ДА** (вердикт = exit-code `pytest`/smoke) → needs-hybrid-fallback |
|
||||
| A6 | deployer | `.openclaw/agents/deployer.md`, стадии `deploy-staging`/`deploy` | `staging_check.py`/exit-code → `15`/`14` логи | да (через S0) | `check_staging_status` (`checks.py:599`)→`_parse_staging_status` (`checks.py:538`) **`staging_status:`**; `check_deploy_status` (`checks.py:473`)→`_parse_deploy_status` (`checks.py:413`) **`deploy_status:`** | **C** | **ДА** (вердикт = `staging_check.py`/exit-code; прод уже детерминирован Phase A/B/C) → replace-deterministic |
|
||||
| D1 | deploy-finalizer | `launch_job` перехват **до** `_spawn` (`launcher.py:389`) | детерминированный (LLM не консультируется) | **нет** (слот агента, перехват до `_spawn`) | — | — (нет консультации) | — (уже детерминирован) |
|
||||
| D2 | post-deploy-monitor | `launch_job` перехват **до** `_spawn` (`launcher.py:394`) | детерминированный (LLM не консультируется) | **нет** (слот агента, перехват до `_spawn`) | — | — (нет консультации) | — (уже детерминирован) |
|
||||
|
||||
> **Чтение таблицы (R5).** Три ортогональных факта: (1) `Консультирует LLM?` — транспорт/слот vs факт
|
||||
> консультации (R4 §0); (2) `Control path?` — входит ли вывод роли в **поток управления** (C: `check_*`
|
||||
> ветвится на LLM-вердикте; P: детерминированный гейт судит артефакт независимо); (3) `Avoidable …?` —
|
||||
> двухбитный предикат BRD §0-bis (C **И** вердикт деривируем из tool-сигналов). Итог: **avoidable LLM
|
||||
> control paths = {tester, deployer}**; control-path-но-keep = `{reviewer}`; не-control-path =
|
||||
> `{analyst, architect, developer}`.
|
||||
>
|
||||
> Не-агентские control-path'ы (маршрутизация `advance_stage`, `QG_CHECKS`/`check_*`/`_parse_*`,
|
||||
> `error_classifier`, `serial_gate`/`merge_gate`/`coverage_gate`/`security_gate`/`staging_verdict`/
|
||||
> `review_parse`/`frontmatter`, `self_deploy` Phase A/B/C) — **уже детерминированы** (FR-3).
|
||||
|
||||
## 2. Задействованные модули / пути
|
||||
|
||||
| Путь | Действие |
|
||||
|------|----------|
|
||||
| `src/agents/launcher.py` | **читать** (инвентарь S0/D1/D2; `_spawn`, `launch_job:389/394`, `AGENT_CONFIGS`, `resolve_agent_model/effort`) — **не менять** |
|
||||
| `.openclaw/agents/{analyst,architect,developer,reviewer,tester,deployer}.md` | **читать** (инвентарь 6 ролей) — **не менять** |
|
||||
| `src/qg/checks.py` | **читать** (доказательство control-path-оси: кто потребляет вывод каждой роли — `check_analysis_complete:33`/`check_architecture_done:62`/`check_ci_green:82`/`check_reviewer_verdict:336`/`check_tests_passed:182`+`_parse_tests_verdict:226`/`check_staging_status:599`+`_parse_staging_status:538`/`check_deploy_status:473`+`_parse_deploy_status:413`; `QG_CHECKS`-реестр) — **не менять** |
|
||||
| `src/stages.py`, `src/stage_engine.py` | **читать** (доказать детерминизм маршрутизации) — **не менять** |
|
||||
| `src/{serial_gate,merge_gate,coverage_gate,security_gate,staging_verdict,review_parse,error_classifier,frontmatter,self_deploy,post_deploy,transition_lease,reconciler,job_reaper}.py` | **читать** (детерминированные leaf'ы — доказательная база) — **не менять** |
|
||||
| `src/usage.py`, `src/db.py` (`agent_runs`) | **читать** (источник оценок экономии токенов/времени) — **не менять** |
|
||||
| `docs/architecture/llm-call-sites.md` *(имя — пример; финально решает архитектор)* | **создать**: карта call-site'ов + классификация + **control-path-разметка** (FR-1/FR-2/FR-3/FR-8) |
|
||||
| `docs/architecture/llm-determinization-roadmap.md` *(имя — пример)* | **создать**: упорядоченный roadmap (FR-4) |
|
||||
| `docs/architecture/llm-usage-policy.md` *(имя — пример)* | **создать**: нормативная политика + определение «avoidable LLM control path» (FR-5/FR-8) |
|
||||
| `docs/work-items/ORCH-118/06-adr/ADR-001-*.md` | **создать** (архитектор): фиксация карты/таксономии/control-path-оси/первого среза как ADR |
|
||||
| `tests/test_llm_call_site_inventory.py` *(имя — пример)* | **создать**: структурные анти-дрейф тесты (FR-6), включая control-path-инварианты |
|
||||
| `docs/architecture/README.md`, `docs/overview/*`, `CHANGELOG.md` | **обновить** (ссылка на карту/политику; норматив golden-source) |
|
||||
|
||||
> Документы карты/политики целесообразно разместить в `docs/architecture/` (durable, сквозное), а не
|
||||
> только в `docs/work-items/ORCH-118/` — окончательное размещение/формат решает архитектор.
|
||||
|
||||
## 3. Функциональные требования
|
||||
|
||||
### FR-1 — Полнота и привязка инвентаря LLM-консультаций (BR-1)
|
||||
Единица — **LLM-консультация** (control-path потребляет суждение LLM), а не «спавн процесса» (R4, BRD
|
||||
§0). Карта перечисляет **каждый** call-site: `S0` (единственный транспорт `_spawn`), `A1…A6` (6 ролей,
|
||||
консультируют через S0), `D1/D2` (job-роли, **занимают слот агента, но НЕ консультируют LLM** —
|
||||
перехват в `launch_job` до `_spawn`; включены как доказательство паттерна). Поля записи: `id`,
|
||||
`location` (`file:line`), `trigger`, `stage/owner`, `output artifact`, `machine-verdict key` (если
|
||||
есть), **`output consumer` (`check_*`/`_parse_*` с `file:line` — кто потребляет вывод роли)**,
|
||||
`est. tokens/runtime`, **`consults-LLM`** (consultation vs LLM-capable transport/slot, §0.3),
|
||||
**`axis` (C control-path / P artifact-producer, §0-bis)**, `classification`, `rationale`, `dependency`,
|
||||
`risk`. Каждый `file:line` обязан резолвиться в реальный код. Инвариант (транспорт-агностичный,
|
||||
двусторонний): единственный транспорт LLM-консультации в `src/**` — `S0`; **иного LLM-транспорта нет** —
|
||||
тесты FR-6(a)+(f).
|
||||
|
||||
### FR-2 — Таксономия классификации (BR-2)
|
||||
Ровно 4 взаимоисключающих класса с определениями:
|
||||
- `keep-LLM` — нужно настоящее суждение; **обязательно назвать** конкретное суждение.
|
||||
- `replace-deterministic-now` — безопасная детерминированная замена сейчас.
|
||||
- `replace-later/risky` — замена возможна, но позже / с риском (нужны предпосылки).
|
||||
- `needs-hybrid-fallback` — детерминированное ядро + LLM-фолбэк только на суждение.
|
||||
|
||||
Каждому call-site присвоен **ровно один** класс, и класс **выводится из control-path-оси** (§0-bis,
|
||||
FR-8), а не постулируется:
|
||||
- **P (artifact-producer)** → `keep-LLM` — детерминированный гейт судит артефакт; авторская работа
|
||||
требует суждения: `analyst`, `architect`, `developer`.
|
||||
- **C + НЕ-деривируемый вердикт** → `keep-LLM` — control path, но суждение настоящее: `reviewer`
|
||||
(назвать суждение: «приемлемость кода/решения», не сводится к exit-коду).
|
||||
- **C + деривируемый вердикт** → avoidable → `replace-*` / `needs-hybrid-fallback`:
|
||||
`deployer → replace-deterministic-now`/`replace-later/risky` (staging = exit-code-маппинг; прод
|
||||
self-hosting уже детерминирован Phase A/B/C), `tester → needs-hybrid-fallback` (детерминированный
|
||||
прогон `pytest`+smoke даёт PASS/FAIL; LLM-суждение только на триаж падений / маппинг TC↔критерии).
|
||||
- `deploy-finalizer/post-deploy-monitor → already-deterministic` (вне таксономии замен, эталон).
|
||||
|
||||
> 📌 **Follow-up'ы — по роли, без Plane-ID (R3, NFR-6).** Кандидаты обозначаются ролью
|
||||
> (deployer-замена, tester-гибрид), а не конкретными ID.
|
||||
|
||||
### FR-3 — Подтверждение детерминизма не-агентских путей (BR-3)
|
||||
Карта отдельным разделом фиксирует, с `file:line`-доказательством, что НЕ консультируют LLM (не зависят
|
||||
от суждения LLM ни через `_spawn`, ни иным транспортом): маршрутизация (`advance_stage`/
|
||||
`STAGE_TRANSITIONS`), все `QG_CHECKS`/`check_*`, парсеры вердиктов (`_parse_*` через
|
||||
`frontmatter.parse_frontmatter`), `error_classifier` (regex), под-гейты (security/merge/coverage/
|
||||
image-freshness), `self_deploy` Phase A/B/C, reconciler/reaper/serial-gate/transition-lease. ⚠️ Эти
|
||||
модули **спавнят subprocess'ы** (`git`/`pytest`/`docker`/`ssh`/сканеры) — это детерминированные
|
||||
инструменты, **не** LLM-консультации (§0): доказательство детерминизма — отсутствие *LLM*-транспорта,
|
||||
а не отсутствие *subprocess*. Любая найденная неожиданная LLM-консультация добавляется в инвентарь
|
||||
(FR-1) и классифицируется (FR-2).
|
||||
|
||||
### FR-4 — Упорядоченный roadmap замен (BR-4)
|
||||
Ранжированный список кандидатов (названных **по роли**); для каждого: зависимости (предпосылки/блокеры),
|
||||
**оценка** экономии токенов/времени (из `agent_runs`), риск безопасности, нужен ли hybrid-fallback,
|
||||
ожидание kill-switch/обратимости, и **тип будущей follow-up задачи по роли** (без конкретного
|
||||
Plane-ID — заводится отдельно, NFR-6). Явно: **рекомендованный первый срез** + обоснование (самый
|
||||
низкорисковый, «чисто деривируемый» control path, опирающийся на существующий прецедент D1/D2).
|
||||
|
||||
### FR-5 — Политика использования LLM (BR-5)
|
||||
Нормативный durable-документ: принцип «LLM — только где нужно настоящее суждение»; критерии решения
|
||||
keep vs replace, **сформулированные через ось §0-bis** (является ли путь control path — ветвится ли
|
||||
`check_*` на LLM-выводе; деривируем ли вердикт из tool-сигналов; обратимость; влияние на автономность);
|
||||
требование к новым/изменённым control-path'ам обосновывать любое использование LLM против политики.
|
||||
Может включать рекомендацию reviewer-оси (как ORCH-079) — **как требование, не как реализацию гейта**
|
||||
(новый QG не вводится, FR-6 §QG).
|
||||
|
||||
### FR-6 — Структурные анти-дрейф тесты (BR-6)
|
||||
Новый offline-тест-файл (без сети/LLM/subprocess-к-модели), проверяющий инварианты карты. ⚠️ **R4 —
|
||||
инвариант формулируется вокруг LLM-консультации/транспорта, а не «существования процесса Claude CLI».**
|
||||
Дискриминатор тестов — **«консультирует LLM», а не «спавнит subprocess»**; десятки прочих subprocess
|
||||
(`git`/`pytest`/`docker`/`ssh`/сканеры/`staging_check.py`) явно исключаются из матчинга.
|
||||
- **(a) Единственный транспорт.** В `src/**` ровно **одна** точка сборки/запуска Claude CLI (матчинг
|
||||
по совокупности признаков LLM-транспорта: `CLAUDE_BIN` + `--system-prompt` + `Popen`/`bash -c`), и
|
||||
это `launcher._spawn`. *(Необходимое, но не достаточное — дополняется (f).)*
|
||||
- **(f) Отсутствие иного LLM-транспорта (R4).** В `src/**` **нет** альтернативного транспорта
|
||||
LLM-консультации: ни импорта `anthropic`/`openai`/иного LLM-SDK, ни прямого HTTP-эндпоинта
|
||||
Anthropic/Claude (`api.anthropic.com`, `/v1/messages` и т.п.), ни второго model-invoking
|
||||
subprocess-сборщика. Allowlist единственного разрешённого транспорта = `S0`.
|
||||
- **(b) Нет консультации в детерминированных путях.** Перечисленные детерминированные модули и
|
||||
обработчики `D1/D2` **не** консультируют LLM (ни `_spawn`-транспорта, ни альтернативного по (f)).
|
||||
- **(c)** Карта перечисляет ровно те промпт-файлы, что физически лежат в `.openclaw/agents/`.
|
||||
- **(d)** Классификация покрывает каждый перечисленный call-site **ровно один раз** (тотальность).
|
||||
- **(e) Capability ≠ consultation.** `D1/D2` перехватываются в `launch_job` **до** `_spawn`
|
||||
(`launcher.py:389/394`) → занимают слот, но консультации нет.
|
||||
- **(g) Control-path-разметка верна (R5, TC-13).** Для каждой роли поле `axis` (C/P) карты **согласовано
|
||||
с фактическим потребителем** в `src/qg/checks.py`: P-роли (`analyst`/`architect`/`developer`)
|
||||
потребляются детерминированными гейтами (`check_analysis_complete`/`check_architecture_done`/
|
||||
`check_ci_green`), C-роли (`reviewer`/`tester`/`deployer`) — verdict-парсерами
|
||||
(`check_reviewer_verdict`/`_parse_tests_verdict`/`_parse_staging_status`/`_parse_deploy_status`).
|
||||
Дрейф (роль переразмечена или потребитель в коде сменил природу) → красный.
|
||||
- **(h) Avoidable-набор зафиксирован (R5, TC-14).** Множество «avoidable LLM control path» в карте =
|
||||
ровно `{tester, deployer}`; `reviewer` помечен control-path-но-keep; `analyst`/`architect`/
|
||||
`developer` — не control path. Любое добавление/удаление без обновления карты → красный.
|
||||
|
||||
> ❌ **Не вводить** тест, прибивающий карту к конкретным follow-up Plane-ID → ✅ тесты проверяют
|
||||
> только инварианты, резолвящиеся в код/файлы репозитория (R3, NFR-6).
|
||||
|
||||
### FR-7 — Скоуп-гард (BR-7)
|
||||
Раннеры замен **не реализуются** в ORCH-118. Карта/roadmap явно помечают кандидатов **по роли**
|
||||
(deployer-замена, tester-гибрид) как follow-up, старт которых гейтится утверждением карты. Тест/диф не
|
||||
должны содержать новых детерминированных раннеров tester/deployer. **Конкретные follow-up Plane-ID не
|
||||
фиксируются** ни в одном артефакте (NFR-6).
|
||||
|
||||
### FR-8 (R5) — Явная control-path-ось и определение «avoidable» (BR-8/BR-9)
|
||||
1. **Разметка оси.** Карта несёт для каждой консультации поле `axis` ∈ {C, P} (§0-bis) + доказательство
|
||||
(поле `output consumer` — `check_*`/`_parse_*` с `file:line`). Разметка проверяема (FR-6g/TC-13).
|
||||
2. **Определение термина.** Карта/политика дают **нормативное определение** «avoidable LLM control
|
||||
path» — двухбитный предикат: (i) C-консультация (LLM-вердикт потребляется потоком управления) **И**
|
||||
(ii) вердикт деривируем из tool-сигналов (exit-code `pytest`/smoke/`staging_check.py`/деплоя).
|
||||
3. **Поимённый целевой набор.** Карта/roadmap **явно** называют `{tester, deployer}` как avoidable LLM
|
||||
control paths, явно отделяя их от `reviewer` (C, но keep — суждение не деривируемо) и от
|
||||
`analyst/architect/developer` (P — не control path). Набор проверяем (FR-6h/TC-14).
|
||||
4. **Связь с классификацией.** Класс из FR-2 **выводится** из (C/P)-типа и деривируемости (а не наоборот).
|
||||
|
||||
## 4. Изменения API
|
||||
Нет. (Опциональная read-only наблюдаемость в `GET /queue`/`GET /metrics` — **вне скоупа** ORCH-118;
|
||||
если архитектор сочтёт полезным — отдельная аддитивная врезка, но не требуется этой задачей.)
|
||||
|
||||
## 5. Изменения схемы БД
|
||||
Нет. (Источник оценок экономии — существующие колонки `agent_runs`: `model`/`effort`/токены/стоимость/
|
||||
время; только чтение.)
|
||||
|
||||
## 6. Требования к новым/изменённым QG checks
|
||||
Нет. `QG_CHECKS` / `check_*` / `_parse_*` / machine-verdict-ключи — **байт-в-байт**. Структурные тесты
|
||||
FR-6 (включая control-path TC-13/TC-14) — обычные `pytest`-тесты, **не** Quality Gate и **не** стадия.
|
||||
Политика LLM (FR-5) — нормативный документ, а не машинный гейт. ⚠️ Control-path-ось — **аналитическая
|
||||
разметка карты**, читающая `check_*` как ground-truth; она ничего в `src/qg/checks.py` не меняет.
|
||||
|
||||
## 7. Совместимость / регресс
|
||||
- **Docs + tests only:** рантайм `src/**` не меняется → нулевая регрессия; enduro-trails не затронут;
|
||||
kill-switch не нужен (нет рантайм-поведения), как в ORCH-077/079/101/102/103/011.
|
||||
- **Обратимость:** артефакты — документы и тесты; откат = удаление/правка docs (рантайм-риска нет).
|
||||
- **Анти-дрейф:** структурные тесты держат карту синхронной с кодом, включая control-path-ось
|
||||
(TC-13/TC-14); норматив сопровождения — «менял места вызова LLM или потребителя вердикта в
|
||||
`src/qg/checks.py` → обнови карту/разметку и политику в том же PR».
|
||||
- **Анти-фабрикация (R3):** артефакты фиксируют только проверяемые ссылки; follow-up'ы — по роли,
|
||||
без выдуманных Plane-ID (NFR-6).
|
||||
- **Self-hosting:** не деплоит/не рестартит прод/не трогает `main` — безопасно для общего инстанса.
|
||||
209
docs/work-items/ORCH-118/03-acceptance-criteria.md
Normal file
209
docs/work-items/ORCH-118/03-acceptance-criteria.md
Normal file
@@ -0,0 +1,209 @@
|
||||
---
|
||||
work_item: ORCH-118
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
---
|
||||
|
||||
# 03 — Критерии приёмки (Acceptance Criteria): ORCH-118 — replace avoidable LLM control paths with deterministic implementations
|
||||
|
||||
Work Item: **ORCH-118** · Repo: **orchestrator** · Стадия: analysis
|
||||
|
||||
Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что считается
|
||||
провалом). Reviewer/тестер проверяет их буквально по файлам репозитория. Напоминание: ORCH-118 —
|
||||
**inventory-first**, docs+tests only; реализация раннеров приёмкой **запрещена** в этой задаче (AC-7);
|
||||
фиксация конкретных follow-up Plane-ID **запрещена** (AC-9, R3).
|
||||
|
||||
> 🔁 **R5.** Добавлены/уточнены критерии под **control-path-ось** (BRD §0-bis): среди реальных
|
||||
> консультаций различаются (C) control-path (LLM-вердикт потребляется потоком управления) и
|
||||
> (P) artifact-producer (детерминированный гейт судит артефакт); термин **«avoidable LLM control path»**
|
||||
> явно определён, целевой набор `{tester, deployer}` поимённо назван. Затронуты **AC-1**, **AC-2** и
|
||||
> новый **AC-10**; добавлены тесты **TC-13/TC-14**.
|
||||
|
||||
---
|
||||
|
||||
## AC-1 — Полнота и привязка инвентаря LLM-консультаций (+ control-path-разметка)
|
||||
|
||||
**Условие:** Документ-карта перечисляет каждый call-site, где control-path потребляет (или способен
|
||||
потребить) суждение LLM — **единица = LLM-консультация, не «спавн процесса»** (R4) — с обязательными
|
||||
полями, привязанными к коду, **включая ось (C/P) и потребителя вывода** (R5).
|
||||
- **PASS:** Карта содержит `S0` (`launcher._spawn` — **единственный транспорт LLM-консультации**), все
|
||||
6 ролей (analyst/architect/developer/reviewer/tester/deployer, консультируют через S0) и обе job-роли
|
||||
(deploy-finalizer/post-deploy-monitor, помеченные **«занимают слот агента, но LLM не консультируют»** —
|
||||
перехват до `_spawn`); у каждой записи заполнены `location (file:line)` / `trigger` / `stage/owner` /
|
||||
`output` / `machine-verdict key (если есть)` / **`output consumer` (`check_*`/`_parse_*` с `file:line`)** /
|
||||
`est. tokens-runtime` / **`consults-LLM`** / **`axis` (C control-path / P artifact-producer)** /
|
||||
`classification` / `rationale`; каждый `file:line` резолвится в реальный код. Карта явно разводит
|
||||
«транспорт/слот существует» и «LLM фактически консультируется» (§0) **и** «consultation входит в поток
|
||||
управления (C)» vs «детерминированный гейт судит артефакт (P)» (§0-bis).
|
||||
- **FAIL:** Пропущен любой call-site; отсутствует любое обязательное поле (включая `output consumer`
|
||||
или `axis`); `file:line` не резолвится; карта смешивает «процесс Claude CLI существует» с
|
||||
«LLM-консультация происходит» (напр. помечает `D1/D2` консультирующими LLM); **или не размечает
|
||||
ось C/P** (напр. называет analyst «control path», или не доказывает потребителем `check_*`); заявлен
|
||||
второй транспорт LLM, не подтверждённый кодом.
|
||||
|
||||
---
|
||||
|
||||
## AC-2 — Классификация по таксономии (4 класса, тотально и однозначно, выведена из control-path-оси)
|
||||
|
||||
**Условие:** Каждый перечисленный call-site отнесён ровно к одному классу с обоснованием,
|
||||
**выведенным** из оси §0-bis.
|
||||
- **PASS:** Таксономия определена явно (`keep-LLM` / `replace-deterministic-now` /
|
||||
`replace-later/risky` / `needs-hybrid-fallback`); каждому site присвоен **ровно один** класс; класс
|
||||
согласован с осью: **P → keep-LLM** (analyst/architect/developer), **C + не-деривируемый вердикт →
|
||||
keep-LLM** (reviewer, с названным конкретным суждением, не сводимым к exit-коду), **C + деривируемый
|
||||
вердикт → replace-\*/hybrid** (tester/deployer); у `keep-LLM`-записей назван **конкретный** вид
|
||||
суждения, ради которого LLM сохраняется.
|
||||
- **FAIL:** Site не классифицирован / классифицирован дважды; класс вне таксономии; `keep-LLM` без
|
||||
названного суждения; **класс противоречит оси** (напр. control-path-deployer помечен `keep-LLM` без
|
||||
доказательства не-деривируемости вердикта, или artifact-producer-analyst помечен `replace-*`).
|
||||
|
||||
---
|
||||
|
||||
## AC-3 — Доказанный детерминизм не-агентских путей
|
||||
|
||||
**Условие:** Карта отдельно фиксирует, что control-path'ы вне 6 агентов не консультируют LLM, с доказательством.
|
||||
- **PASS:** Перечислены маршрутизация (`advance_stage`/`STAGE_TRANSITIONS`), все `QG_CHECKS`/`check_*`,
|
||||
парсеры `_parse_*`, `error_classifier`, под-гейты (security/merge/coverage/image-freshness),
|
||||
`self_deploy` Phase A/B/C, reconciler/reaper/serial-gate/transition-lease — каждый с `file:line`,
|
||||
подтверждающим отсутствие **LLM-консультации** (ни `_spawn`-транспорта, ни альтернативного). Их
|
||||
subprocess-вызовы инструментов (`git`/`pytest`/`docker`/`ssh`/сканеры) явно квалифицированы как
|
||||
**не-LLM** (детерминизм доказывается отсутствием LLM-транспорта, а не отсутствием subprocess).
|
||||
- **FAIL:** Утверждение о детерминизме без `file:line`-доказательства; путь, заявленный
|
||||
детерминированным, фактически консультирует LLM (и это не отражено в инвентаре/классификации); либо
|
||||
детерминизм «доказан» простым отсутствием subprocess (подмена дискриминатора, §0).
|
||||
|
||||
---
|
||||
|
||||
## AC-4 — Упорядоченный roadmap с обязательными атрибутами и первым срезом
|
||||
|
||||
**Условие:** Есть ранжированный roadmap детерминированных замен.
|
||||
- **PASS:** Roadmap упорядочен; для каждого кандидата (названного **по роли**) указаны зависимости,
|
||||
**оценка** экономии токенов/времени (со ссылкой на источник — `agent_runs`/`usage`), риск
|
||||
безопасности, потребность в hybrid-fallback, ожидание kill-switch/обратимости и **тип follow-up
|
||||
задачи по роли** (без конкретного Plane-ID); явно назван **рекомендованный первый срез** с
|
||||
обоснованием (самый низкорисковый «чисто деривируемый» control path).
|
||||
- **FAIL:** Roadmap не упорядочен; у кандидата отсутствует любой обязательный атрибут; оценка экономии
|
||||
не привязана к источнику; нет рекомендованного первого среза; кандидат привязан к выдуманному
|
||||
Plane-ID (→ см. AC-9).
|
||||
|
||||
---
|
||||
|
||||
## AC-5 — Нормативная политика использования LLM
|
||||
|
||||
**Условие:** Существует durable-документ политики.
|
||||
- **PASS:** Политика формулирует принцип «LLM — только где нужно настоящее суждение», даёт критерии
|
||||
решения keep vs replace **через ось §0-bis** (control path ли это; деривируем ли вердикт) и требование
|
||||
обосновывать любое новое использование LLM против политики; документ нормативный (durable, в `docs/`),
|
||||
а не разовая заметка.
|
||||
- **FAIL:** Политика отсутствует; не нормативна; не опирается на control-path-критерий; противоречит
|
||||
сохранению автономности (NFR-2).
|
||||
|
||||
---
|
||||
|
||||
## AC-6 — Структурные анти-дрейф тесты: зелёные и осмысленные
|
||||
|
||||
**Условие:** Новый offline-тест-файл прибивает инварианты карты к коду; инвариант сформулирован вокруг
|
||||
**LLM-консультации/транспорта** (R4) **и control-path-оси** (R5), а не «существования процесса Claude CLI».
|
||||
- **PASS:** Тесты проверяют: (a) единственный транспорт LLM-консультации в `src/**` (= `launcher._spawn`);
|
||||
**(f) отсутствие любого иного LLM-транспорта** (нет импорта `anthropic`/`openai`/LLM-SDK, нет прямого
|
||||
HTTP-эндпоинта Anthropic/Claude, нет второго model-invoking subprocess-сборщика); (b) отсутствие
|
||||
LLM-консультации в перечисленных детерминированных модулях и в обработчиках deploy-finalizer/
|
||||
post-deploy-monitor; (c) двустороннюю сверку списка промптов карты с `.openclaw/agents/`;
|
||||
(d) тотальность классификации; (e) перехват `D1/D2` в `launch_job` до `_spawn`; **(g) корректность
|
||||
control-path-разметки (TC-13)** — `axis` каждой роли согласован с фактическим потребителем в
|
||||
`src/qg/checks.py` (P-роли → `check_analysis_complete`/`check_architecture_done`/`check_ci_green`;
|
||||
C-роли → `check_reviewer_verdict`/`_parse_tests_verdict`/`_parse_staging_status`/`_parse_deploy_status`);
|
||||
**(h) фиксацию avoidable-набора (TC-14)** — множество avoidable LLM control paths = `{tester, deployer}`,
|
||||
reviewer = control-path-keep, analyst/architect/developer = не control path. Дискриминатор тестов —
|
||||
**«консультирует LLM», а не «спавнит subprocess»**. Тесты не используют сеть/LLM/subprocess-к-модели.
|
||||
Полный `pytest tests/ -q` — зелёный.
|
||||
- **FAIL:** Тестов нет; тест тривиально проходит (не привязан к коду); инвариант проверяет лишь «один
|
||||
`Popen` Claude CLI» **без** (f); **отсутствует control-path-инвариант (g/h)** — карта могла бы помечать
|
||||
analyst «control path» или забыть deployer в avoidable-наборе, и тест бы это пропустил (корень
|
||||
R4-блокера); тест выродился в «подсчёт всех subprocess»; любой тест красный; полный прогон `tests/`
|
||||
падает; введён тест, прибивающий карту к конкретным follow-up Plane-ID (анти-паттерн R3).
|
||||
|
||||
---
|
||||
|
||||
## AC-7 — Скоуп и сохранение поведения
|
||||
|
||||
**Условие:** ORCH-118 не меняет рантайм и не реализует раннеры.
|
||||
- **PASS:** Диф не меняет `STAGE_TRANSITIONS` / реестр и имена `QG_CHECKS`/`check_*` /
|
||||
machine-verdict-ключи / схему БД; в `src/**` нет нового детерминированного раннера tester/deployer
|
||||
(раннеры замен не реализованы); изменения ограничены docs + новый(е) тест-файл(ы).
|
||||
- **FAIL:** Изменён любой из перечисленных рантайм-контрактов; реализован детерминированный раннер
|
||||
замены; правки `src/**`-поведения вне инвентаря/тестов.
|
||||
|
||||
---
|
||||
|
||||
## AC-8 — Синхронизация golden-source документации
|
||||
|
||||
**Условие:** Обзорные/архитектурные доки и CHANGELOG отражают новые артефакты.
|
||||
- **PASS:** `docs/architecture/README.md` и витрина `docs/overview/*` ссылаются на карту call-site'ов
|
||||
и политику использования LLM; `CHANGELOG.md` обновлён в том же PR.
|
||||
- **FAIL:** Карта/политика введены, но golden-source доки/витрина/CHANGELOG не обновлены (ось ORCH-079/
|
||||
ORCH-011 → finding ≥P1).
|
||||
|
||||
---
|
||||
|
||||
## AC-9 — Только проверяемые ссылки; follow-up'ы — по роли, без выдуманных ID (анти-фабрикация, R3)
|
||||
|
||||
**Условие:** Ни один артефакт не фиксирует непроверяемых follow-up Plane-ID; кандидаты-замены
|
||||
именуются по роли.
|
||||
- **PASS:** Везде, где карта/BRD/TRZ/roadmap/ADR упоминают кандидата-замену, он назван **по роли**
|
||||
(deployer-замена, tester-гибрид); конкретные follow-up Plane-ID **не указаны**; все `file:line`/
|
||||
ссылки на документы резолвятся в репозиторий. Тип follow-up'а описан по роли, ID — «TBD / при
|
||||
заведении задачи».
|
||||
- **FAIL:** Любой артефакт фиксирует конкретный follow-up Plane-ID (напр. `ORCH-115`/`ORCH-116`) как
|
||||
нормативную привязку роли; присутствует ссылка/маркер, не резолвящийся в код/документ репозитория;
|
||||
введён структурный тест, прибивающий карту к конкретному follow-up ID.
|
||||
|
||||
> Контекст: AC-9 заменяет ранее ошибочный «канонический-маппинг» критерий ревизии R2. Корень
|
||||
> отклонения R2 — фиксация **несуществующих** ID (`ORCH-115`/`ORCH-116`) как нормативного инварианта
|
||||
> с непроверяемым источником («live Plane backlog»). R3 запрещает фабрикацию ID и переводит follow-up'ы
|
||||
> на именование по роли.
|
||||
|
||||
---
|
||||
|
||||
## AC-10 (R5) — Явная control-path-ось и определение «avoidable LLM control path»
|
||||
|
||||
**Условие:** Артефакты **явно** различают control-path-консультации и artifact-producer-консультации и
|
||||
**явно** определяют целевой термин — это закрывает единственный оставшийся блокер R4-ревью (название
|
||||
задачи — «replace avoidable LLM **control paths**»).
|
||||
- **PASS:**
|
||||
1. **Ось определена и применена.** Карта/политика явно вводят ось (C) control-path (LLM-вердикт
|
||||
потребляется потоком управления — `check_*` ветвится на нём) vs (P) artifact-producer
|
||||
(детерминированный гейт судит артефакт; суждение LLM в control flow не входит), и присваивают
|
||||
**ровно один** тип каждой из 6 ролей с доказательством-потребителем (`check_*`/`_parse_*`,
|
||||
`file:line`): P = `analyst`/`architect`/`developer`; C = `reviewer`/`tester`/`deployer`.
|
||||
2. **Термин определён.** Дано нормативное определение «avoidable LLM control path» = двухбитный
|
||||
предикат: (i) C-консультация **И** (ii) вердикт деривируем из tool-сигналов (exit-code `pytest`/
|
||||
smoke/`staging_check.py`/деплоя).
|
||||
3. **Целевой набор поимённо назван.** Артефакты явно называют **avoidable LLM control paths =
|
||||
{tester, deployer}**, и явно отделяют: `reviewer` — control path, но **keep** (вердикт не
|
||||
деривируем, настоящее суждение); `analyst`/`architect`/`developer` — **не** control path
|
||||
(artifact-producer). Это согласовано с классификацией (AC-2) и закреплено тестами TC-13/TC-14.
|
||||
- **FAIL:** Артефакты упоминают «LLM control paths» без явного определения; не размечают ось C/P
|
||||
по ролям или размечают её без доказательства-потребителя; не определяют «avoidable» как
|
||||
проверяемый предикат; не называют поимённо целевой набор `{tester, deployer}` **или** не отделяют его
|
||||
от reviewer (control-path-keep) и от analyst/architect/developer (не control path); разметка не
|
||||
согласована с `src/qg/checks.py` / не закреплена TC-13/TC-14.
|
||||
|
||||
---
|
||||
|
||||
## Сводная матрица AC ↔ FR/BR
|
||||
| AC | Покрывает |
|
||||
|----|-----------|
|
||||
| AC-1 | BR-1 / FR-1 / BR-8 / FR-8 |
|
||||
| AC-2 | BR-2 / FR-2 (выведена из §0-bis) |
|
||||
| AC-3 | BR-3 / FR-3 |
|
||||
| AC-4 | BR-4 / FR-4 |
|
||||
| AC-5 | BR-5 / FR-5 |
|
||||
| AC-6 | BR-6 / FR-6 (вкл. TC-13/TC-14) |
|
||||
| AC-7 | BR-7 / FR-7 / NFR-1 / NFR-3 |
|
||||
| AC-8 | NFR-4 / правила агентов §2,§6 (golden-source) |
|
||||
| AC-9 | NFR-6 / BR-7 / FR-7 (только проверяемые ссылки; follow-up'ы по роли, без выдуманных ID) |
|
||||
| AC-10 | **BR-8 / BR-9 / FR-8 / NFR-7 (R5 — control-path-ось + определение «avoidable»)** |
|
||||
132
docs/work-items/ORCH-118/04-test-plan.yaml
Normal file
132
docs/work-items/ORCH-118/04-test-plan.yaml
Normal file
@@ -0,0 +1,132 @@
|
||||
work_item: ORCH-118
|
||||
stage: analysis
|
||||
author_agent: analyst
|
||||
status: ready-for-review
|
||||
created_at: 2026-06-15
|
||||
model_used: claude-opus-4-8
|
||||
title: "LLM call-site inventory + control-path axis + classification + roadmap + usage policy (inventory-first, docs+tests only)"
|
||||
framework: pytest
|
||||
scope: >
|
||||
Покрываются СТРУКТУРНЫЕ инварианты карты LLM-консультаций и анти-дрейф (FR-6), плюс скоуп-гард
|
||||
(рантайм-контракты не тронуты, раннеры не реализованы) и анти-фабрикация ссылок/ID (TC-11).
|
||||
Единица — LLM-КОНСУЛЬТАЦИЯ (control-path потребляет суждение LLM), а не «спавн процесса / Claude
|
||||
CLI существует» (R4, BRD §0). Инвариант единственной точки — транспорт-агностичный и двусторонний:
|
||||
TC-01 (единственный транспорт = _spawn) + TC-12 (отсутствует иной LLM-транспорт).
|
||||
R5: добавлена CONTROL-PATH-ОСЬ (BRD §0-bis) — среди реальных консультаций различаются
|
||||
(C) control-path (LLM-вердикт потребляется потоком управления, check_* ветвится на нём) и
|
||||
(P) artifact-producer (детерминированный гейт судит артефакт); термин «avoidable LLM control path»
|
||||
определён как двухбитный предикат (C И вердикт деривируем из tool-сигналов), целевой набор поимённо
|
||||
= {tester, deployer}. Эту ось проверяют TC-13 (разметка C/P согласована с потребителем в
|
||||
src/qg/checks.py) и TC-14 (avoidable-набор зафиксирован). ВНЕ покрытия: реализация детерминированных
|
||||
раннеров deployer / tester — отдельные follow-up задачи (именуются по роли; конкретные Plane-ID в
|
||||
ORCH-118 не фиксируются, R3/NFR-6).
|
||||
notes: >
|
||||
Все тесты детерминированы и offline: без сети, без запуска LLM, без subprocess-к-модели.
|
||||
Имена файла теста и документов карты — примерные (финально решает архитектор); тест-кейсы
|
||||
привязываются к фактическим путям артефактов, выбранным в 06-adr. Полный регресс tests/
|
||||
должен оставаться зелёным (TC-10). Дискриминатор всех structural-тестов — "консультирует LLM",
|
||||
а НЕ "спавнит subprocess": прочие subprocess (git/pytest/docker/ssh/сканеры/staging_check.py) явно
|
||||
исключаются из матчинга, иначе тест выродился бы в подсчёт всех Popen. Регрессом считается:
|
||||
появление второго ТРАНСПОРТА LLM-консультации (новый _spawn ИЛИ импорт anthropic/openai/LLM-SDK ИЛИ
|
||||
прямой HTTP Anthropic/Claude ИЛИ второй model-invoking subprocess), LLM-консультация в
|
||||
детерминированном модуле, дрейф карты относительно .openclaw/agents/, изменение рантайм-контрактов
|
||||
(STAGE_TRANSITIONS / QG_CHECKS / check_* / machine-verdict / схема БД), рассогласование
|
||||
control-path-разметки с фактическим потребителем в src/qg/checks.py (TC-13), либо изменение
|
||||
avoidable-набора без обновления карты (TC-14).
|
||||
R5 (единственный блокер R4-ревью): артефакты разводили "консультация ≠ транспорт/слот", но не делали
|
||||
явной CONTROL-PATH-ОСЬ — самую важную для названия задачи "replace avoidable LLM CONTROL PATHS".
|
||||
Добавлены TC-13 (control-path-разметка C/P доказывается фактическим потребителем check_*/_parse_*) и
|
||||
TC-14 (avoidable LLM control paths = {tester, deployer}; reviewer = control-path-keep;
|
||||
analyst/architect/developer = не control path). TC-04 (тотальность) теперь сверяет согласованность
|
||||
класса с осью.
|
||||
R4 (предыдущий блокер): инвариант "места вызова LLM" разведён на ТРАНСПОРТ и КОНСУЛЬТАЦИЮ;
|
||||
TC-01 уточнён (необходимое, но не достаточное), добавлен TC-12 (no-alternative-transport),
|
||||
TC-02 уточнён по дискриминатору, TC-06 закрепляет capability ≠ consultation (D1/D2 — слот без
|
||||
консультации).
|
||||
R3: тест на привязку follow-up'ов к конкретным Plane-ID УДАЛЁН (бывш. TC-11) как анти-паттерн;
|
||||
TC-11 теперь проверяет анти-фабрикацию (ID не выдуманы).
|
||||
|
||||
tests:
|
||||
- id: TC-01
|
||||
type: unit
|
||||
description: "Единственный ТРАНСПОРТ LLM-консультации: ровно одно место в src/** собирает/запускает Claude CLI (матчинг по совокупности признаков LLM-транспорта CLAUDE_BIN + --system-prompt + Popen/bash -c), и это launcher._spawn. Необходимое, но НЕ достаточное условие — дополняется TC-12 (отсутствие иного транспорта) (R4 / FR-6a / AC-1)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-02
|
||||
type: unit
|
||||
description: "Детерминированные модули без LLM-консультации: перечисленные leaf'ы (serial_gate, merge_gate, coverage_gate, security_gate, staging_verdict, review_parse, error_classifier, frontmatter, self_deploy, post_deploy, transition_lease, reconciler, job_reaper) не консультируют LLM (нет ни _spawn-транспорта, ни альтернативного по TC-12); их subprocess-вызовы git/pytest/docker/ssh/сканеров LLM-консультацией НЕ считаются — дискриминатор 'консультирует LLM', а не 'спавнит subprocess' (R4 / FR-6b / AC-3)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-03
|
||||
type: unit
|
||||
description: "Анти-дрейф промптов: карта перечисляет ровно те 6 промпт-файлов, что физически лежат в .openclaw/agents/ (двусторонняя сверка, нет лишних/пропущенных) (FR-6c / AC-1)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-04
|
||||
type: unit
|
||||
description: "Тотальность классификации: каждый перечисленный в карте call-site отнесён ровно к одному классу из таксономии {keep-LLM, replace-deterministic-now, replace-later/risky, needs-hybrid-fallback}; без дублей и пропусков; класс СОГЛАСОВАН с осью §0-bis (P → keep-LLM; C+не-деривируем → keep-LLM; C+деривируем → replace-*/hybrid) (FR-6d / FR-2 / AC-2)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-05
|
||||
type: unit
|
||||
description: "keep-LLM требует обоснования: каждая запись класса keep-LLM несёт непустое поле названного конкретного суждения; для C-keep (reviewer) обоснование явно фиксирует НЕ-деривируемость вердикта (почему не сводится к exit-коду) (FR-2 / AC-2)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-06
|
||||
type: unit
|
||||
description: "Capability ≠ consultation: launch_job перехватывает deploy-finalizer и post-deploy-monitor ДО _spawn (launcher.py:389/394) — job занимает слот агента, но LLM НЕ консультируется (процесс/слот существует, суждение не потребляется) — эталон паттерна замены и прямая иллюстрация R4-различия (FR-6e / AC-3)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-07
|
||||
type: unit
|
||||
description: "Полнота roadmap: документ roadmap для каждого кандидата (названного ПО РОЛИ) содержит обязательные атрибуты (зависимости / оценка экономии со ссылкой на agent_runs / риск / hybrid-need / тип follow-up задачи по роли) и явно называет рекомендованный первый срез (FR-4 / AC-4)"
|
||||
module: tests/test_llm_determinization_docs.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-08
|
||||
type: unit
|
||||
description: "Политика LLM существует и нормативна: документ политики содержит принцип 'LLM только где нужно суждение', критерии keep vs replace СФОРМУЛИРОВАННЫЕ через ось §0-bis (control path ли это; деривируем ли вердикт), и нормативное определение термина 'avoidable LLM control path' (FR-5 / FR-8 / AC-5, AC-10)"
|
||||
module: tests/test_llm_determinization_docs.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-09
|
||||
type: integration
|
||||
description: "Скоуп-гард рантайм-контрактов: снимок set ролей-агентов из STAGE_TRANSITIONS и набора имён QG_CHECKS не изменился относительно эталона — ORCH-118 не тронул машину стадий/гейты (FR-7 / AC-7)"
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-10
|
||||
type: integration
|
||||
description: "Полный регресс tests/ остаётся зелёным (pytest tests/ -q) — инвентаризация и тесты не ломают существующий конвейер (NFR-1 / AC-6, AC-7)"
|
||||
module: tests/
|
||||
expected: PASS
|
||||
|
||||
- id: TC-11
|
||||
type: unit
|
||||
description: "Анти-фабрикация follow-up ID (R3 / NFR-6 / AC-9): документы карты/roadmap НЕ содержат привязки кандидатов-замен к конкретным follow-up Plane-ID несуществующих work item (паттерн ORCH-1\\d\\d, не равный самому ORCH-118 и не присутствующий в docs/work-items/); кандидаты именуются по роли. Заменяет ошибочный mapping-тест R2, прибивавший карту к выдуманным ID."
|
||||
module: tests/test_llm_determinization_docs.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-12
|
||||
type: unit
|
||||
description: "Отсутствие иного LLM-транспорта (R4 / FR-6f / AC-1, AC-6): в src/** НЕТ альтернативного транспорта LLM-консультации помимо _spawn — ни импорта anthropic/openai/иного LLM-SDK, ни прямого HTTP-эндпоинта Anthropic/Claude (api.anthropic.com, /v1/messages), ни второго model-invoking subprocess-сборщика (другой бинарь с --system-prompt/--model). Закрывает дыру 'один _spawn зелёный, а рядом проросла новая консультация другим транспортом', которую TC-01 в одиночку не ловит. Allowlist единственного разрешённого транспорта = S0/launcher._spawn."
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-13
|
||||
type: unit
|
||||
description: "Control-path-ось верна (R5 / FR-6g / FR-8 / AC-10): поле axis (C/P) каждой из 6 ролей в карте СОГЛАСОВАНО с фактическим потребителем вывода в src/qg/checks.py — P-роли (analyst/architect/developer) потребляются детерминированными гейтами (check_analysis_complete:33 / check_architecture_done:62 / check_ci_green:82, судящими наличие файлов / CI, НЕ самоотчёт LLM); C-роли (reviewer/tester/deployer) потребляются verdict-парсерами, читающими machine-verdict, который написал LLM (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:'). Дискриминатор: 'LLM-вердикт ветвит поток управления', а не 'на стадии есть LLM'. Рассогласование (роль переразмечена ИЛИ потребитель в коде сменил природу) → красный."
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
|
||||
- id: TC-14
|
||||
type: unit
|
||||
description: "Avoidable-набор зафиксирован (R5 / FR-6h / FR-8 / AC-10): множество 'avoidable LLM control path' в карте = РОВНО {tester, deployer} (C И вердикт деривируем из exit-кодов); reviewer помечен control-path-но-keep (C, вердикт НЕ деривируем — настоящее суждение); analyst/architect/developer помечены НЕ control path (P, artifact-producer). Любое добавление/удаление роли в avoidable-набор без обновления карты, либо пометка analyst/architect/developer 'control path', либо пометка reviewer 'avoidable' → красный. Закрывает корень R4-блокера: 'есть LLM на стадии' ≠ 'это avoidable LLM control path'."
|
||||
module: tests/test_llm_call_site_inventory.py
|
||||
expected: PASS
|
||||
@@ -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
|
||||
</content>
|
||||
</invoke>
|
||||
43
docs/work-items/ORCH-118/10-tech-risks.md
Normal file
43
docs/work-items/ORCH-118/10-tech-risks.md
Normal file
@@ -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.
|
||||
</content>
|
||||
123
docs/work-items/ORCH-118/12-review.md
Normal file
123
docs/work-items/ORCH-118/12-review.md
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
verdict: APPROVED
|
||||
work_item: ORCH-118
|
||||
stage: review
|
||||
author_agent: reviewer
|
||||
status: approved
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
type: review
|
||||
work_item_id: ORCH-118
|
||||
version: 1
|
||||
---
|
||||
|
||||
# Review ORCH-118 — карта LLM-консультаций, control-path-ось «avoidable», roadmap и политика
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из `verdict:` во frontmatter. `APPROVED` → дальше по конвейеру.
|
||||
|
||||
## Summary
|
||||
|
||||
ORCH-118 — зонтичная **inventory-first, docs + tests only** задача (RCA-трек ORCH-110…117): выпускает
|
||||
доказательную карту LLM-консультаций, нормативную политику использования LLM, упорядоченный roadmap
|
||||
детерминизации и набор структурных анти-дрейф тестов. Реализация раннеров — вне скоупа (FR-7).
|
||||
|
||||
Работа выполнена на эталонном уровне: скоуп выдержан **байт-в-байт** (`src/**` не тронут вообще),
|
||||
каждый `file:line`-якорь карты резолвится в реальный код, все FR-1…FR-8 и AC-1…AC-10 закрыты, golden-source
|
||||
синхронизирован, полный прогон `pytest tests/ -q` — **зелёный (2081 passed)**. Один **P2** (косметика):
|
||||
в трёх docs-артефактах (оба ADR + 10-tech-risks) на EOF протекли служебные теги `</content>`/`</invoke>`
|
||||
из механизма записи файла. Это не ломает гейты/тесты/парсинг машинных блоков и не влияет на рантайм →
|
||||
не блокирует. Рекомендуется зачистить.
|
||||
|
||||
Проверка проведена буквально по файлам: запущены оба новых тест-файла (13 passed), `test_system_docs.py`
|
||||
(29 passed) и полный `tests/` (2081 passed); сверены ключевые якоря `launcher.py` и `src/qg/checks.py`;
|
||||
подтверждено `git diff origin/main...HEAD -- src/` = **пусто**.
|
||||
|
||||
> ⚠️ **Замечание по базе ревью.** Локальный `main` в worktree **устаревший** (`64ba121`, до мержей
|
||||
> ORCH-112/113/114/117). Истинная база ветки — `origin/main` (`13589fc`, мерж ORCH-117). Ревью
|
||||
> проводилось против `origin/main...HEAD` (реальный changeset ORCH-118 — 16 файлов, +2365), а НЕ против
|
||||
> stale `main...HEAD` (который ложно тянет чужие уже-смерженные `src/**`-правки ORCH-110…117).
|
||||
|
||||
## Оси проверки
|
||||
|
||||
### 1. Соответствие ТЗ (02-trz / 03-acceptance-criteria)
|
||||
- **FR-1 / AC-1 (инвентарь полон и привязан):** карта `llm-call-sites.md` перечисляет `S0`, `A1…A6`,
|
||||
`D1/D2` со всеми обязательными полями (`location`/`trigger`/`stage`/`output`/`machine-verdict key`/
|
||||
`output consumer`/`est. tokens`/`consults-LLM`/`axis`/`classification`/`rationale`). **Все якоря
|
||||
резолвятся** — сверено: `launcher.py:472`=`def _spawn(`, `610-614`=сборка `--system-prompt "$(cat …)"`,
|
||||
`838`=`_monitor_agent`, `389/394`=перехваты `deploy-finalizer`/`post-deploy-monitor`, `407/428`=«Not an
|
||||
LLM spawn»; `checks.py:33/62/82/182/226/336/413/473/538/599/657` — все 11 точно совпали с целевыми
|
||||
`def`. ✔
|
||||
- **FR-2 / AC-2 (таксономия 4 класса, выведена из оси):** классы определены, каждому site присвоен ровно
|
||||
один (TC-04 тотальность), правило вывода `P→keep`, `C+!деривируем→keep`, `C+деривируем→replace/hybrid`
|
||||
закреплено тестом (TC-04 axis-consistency). ✔
|
||||
- **FR-3 / AC-3 (детерминизм не-агентских путей):** §3 карты + TC-02 с file:line; дискриминатор —
|
||||
«консультирует LLM», а не «спавнит subprocess». ✔
|
||||
- **FR-4 / AC-4 (roadmap + первый срез):** машинный блок roadmap с rank/deps/savings(источник `agent_runs`)/
|
||||
risk/hybrid/followup_type/first_slice; ровно один `first_slice=yes`=deployer (TC-07). ✔
|
||||
- **FR-5 / AC-5 (политика):** `llm-usage-policy.md` нормативна, критерии keep/replace через ось,
|
||||
определение «avoidable» как двухбитный предикат (TC-08). ✔
|
||||
- **FR-6 / AC-6 (структурные тесты):** TC-01…TC-14 покрывают единственный транспорт (a)+отсутствие иного
|
||||
(f/TC-12), детерминированные пути (b), промпты↔файлы (c), тотальность (d), capability≠consultation (e),
|
||||
control-path-разметку (g/TC-13), avoidable-набор (h/TC-14). Offline, stdlib-only, осмысленные. ✔
|
||||
- **FR-7 / AC-7 (скоуп-гард):** `git diff origin/main...HEAD -- src/` пусто; `STAGE_TRANSITIONS`/
|
||||
`QG_CHECKS`/`check_*`/machine-verdict/схема БД — нетронуты (TC-09 фиксирует снимок); новых раннеров нет. ✔
|
||||
- **FR-8 / AC-10 (control-path-ось + «avoidable»):** ось C/P размечена по ролям с доказательством-потребителем,
|
||||
термин определён, набор `{tester, deployer}` назван поимённо и отделён от `{reviewer}` (C-keep) и
|
||||
`{analyst,architect,developer}` (P), сверено с `src/qg/checks.py` (TC-13/TC-14). ✔
|
||||
- **AC-9 (анти-фабрикация ID):** follow-up'ы названы по роли; все `ORCH-1XX`-ссылки резолвятся в реальные
|
||||
work-item-папки (TC-11 зелёный; подтверждено наличие ORCH-110/111/112/113/114/117). ✔
|
||||
|
||||
### 2. Соответствие ADR (06-adr / adr-0047 / глобальные ADR)
|
||||
- Деливерабл-доки **зеркалят** канонические таблицы ADR-001 D2/D4 без расхождений (поля инвентаря,
|
||||
классы, потребители, avoidable-набор, первый срез=deployer-staging). ✔
|
||||
- D1 (3 durable-дока в `docs/architecture/`), D3 (3 оси + определение), D5 (offline-тесты, конъюнкция
|
||||
признаков транспорта против false-positive на `preflight.py`/`config.py`), D6 (deployer первым), D7
|
||||
(скоуп-гард) — реализованы верно. ✔
|
||||
- **Глобальные ADR не нарушены:** machine-verdict-ключи и реестр `QG_CHECKS` байт-в-байт (TC-09);
|
||||
трассировка ORCH-078 — маркированные инварианты `src/**` не правились (src не тронут). ✔
|
||||
|
||||
### 3. Качество кода (тестов)
|
||||
- Тесты содержательные, не тривиальные: парсят машинные блоки карты/roadmap/политики и сверяют с
|
||||
ground-truth кода (`src/qg/checks.py`, `launcher.py`, `.openclaw/agents/`). `_function_body` устойчив к
|
||||
дрейфу строк; TC-01 корректно требует **конъюнкцию** `CLAUDE_BIN`+`--system-prompt`+launcher (исключая
|
||||
ложные срабатывания); TC-12 закрывает «второй транспорт». Без сети/LLM/subprocess-к-модели. ✔
|
||||
- **Регресс-тест-фиксатор (ORCH-019 BR-4) — N/A:** ORCH-118 — не багфикс-трек, а полный маршрут
|
||||
design/inventory; обязательного теста-фиксатора дефекта не требуется. ✔
|
||||
- Security/утечки — N/A (рантайм не меняется). ✔
|
||||
|
||||
### 4. Документация (golden-source — обязательная ось)
|
||||
- **AC-8 синхронизация:** `docs/architecture/README.md` (секция + ссылка adr-0047), витрина
|
||||
`docs/overview/tech-quality-security.md` (раздел «Где уместен LLM» + ссылки на все 3 дока, ORCH-011),
|
||||
`CHANGELOG.md` — обновлены в этом же PR. `test_system_docs.py` зелёный (29 passed). ✔
|
||||
- ADR заведён (work-item ADR-001 + сквозной adr-0047). ✔
|
||||
- Поскольку `src/**` не менялся, ось «изменён src → обнови доку» неприменима как P0; обзорные доки
|
||||
(ORCH-079/011) обновлены. ✔
|
||||
|
||||
## Findings
|
||||
|
||||
### P0 — Blocker
|
||||
- (нет)
|
||||
|
||||
### P1 — Must fix
|
||||
- (нет)
|
||||
|
||||
### P2 — Should fix
|
||||
- [ ] **Протёкшие служебные теги на EOF трёх docs-артефактов.** На конце файлов остались артефакты
|
||||
механизма записи, не относящиеся к содержимому (контент перед ними полный):
|
||||
- `docs/work-items/ORCH-118/06-adr/ADR-001-llm-call-site-map-and-determinization-roadmap.md:294-295`
|
||||
→ `</content>` + `</invoke>`
|
||||
- `docs/architecture/adr/adr-0047-llm-usage-policy-and-call-site-map.md:114` → `</content>`
|
||||
- `docs/work-items/ORCH-118/10-tech-risks.md:43` → `</content>`
|
||||
|
||||
Правило: «Документация = golden source» (CLAUDE.md §2) — durable-ADR не должен нести постороннюю
|
||||
разметку. Не блокирует (тесты/гейты/машинные блоки карты не затронуты, рантайм-риска нет; три
|
||||
новых dev-дока `llm-call-sites.md`/`llm-usage-policy.md`/`llm-determinization-roadmap.md` — **чистые**).
|
||||
Рекомендация: удалить хвостовые строки с тегами в этом же PR.
|
||||
|
||||
## Документация
|
||||
**Обновлена полностью и корректно.** В PR синхронизированы все golden-source точки: `docs/architecture/README.md`
|
||||
(+секция ORCH-118 и ссылка на adr-0047), витрина `docs/overview/tech-quality-security.md` (раздел про
|
||||
карту LLM + ссылки на 3 дока), `CHANGELOG.md`, оба ADR (work-item + сквозной adr-0047). Три новых durable-дока
|
||||
в `docs/architecture/` (`llm-call-sites.md`/`llm-determinization-roadmap.md`/`llm-usage-policy.md`) консистентны
|
||||
между собой и с ADR; машинные блоки прибиты тестами. Единственное замечание по документации — косметический
|
||||
P2 выше (хвостовые служебные теги в 2 ADR + tech-risks), не требующий отката.
|
||||
80
docs/work-items/ORCH-118/13-test-report.md
Normal file
80
docs/work-items/ORCH-118/13-test-report.md
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
result: PASS
|
||||
work_item: ORCH-118
|
||||
stage: testing
|
||||
author_agent: tester
|
||||
status: pass
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
type: test-report
|
||||
work_item_id: ORCH-118
|
||||
---
|
||||
|
||||
# Test Report — ORCH-118
|
||||
|
||||
> Машинный вердикт читается ТОЛЬКО из `result:` во frontmatter. `PASS` → задача переходит на `deploy-staging`.
|
||||
|
||||
## Окружение
|
||||
- Python: 3.12.13
|
||||
- pytest: 8.3.3 (plugins: cov-5.0.0, anyio-4.13.0, asyncio-0.23.8)
|
||||
- Worktree: `/repos/_wt/orchestrator/feature_ORCH-118-orch-replace-avoidable-llm-con` (ветка `feature/ORCH-118-orch-replace-avoidable-llm-con`)
|
||||
- Дата: 2026-06-16
|
||||
|
||||
## Предусловия
|
||||
- Review-вердикт (`12-review.md`): **APPROVED** (verdict читается из frontmatter). ✔
|
||||
- Скоуп ORCH-118 — inventory-first, docs + tests only: `git diff origin/main...HEAD -- src/` пусто (рантайм не тронут).
|
||||
|
||||
## Smoke API (read-only)
|
||||
- `GET /health` → `{"status":"ok","service":"orchestrator"}` ✔
|
||||
- `GET /status` → активная задача ORCH-118 на стадии `testing` ✔
|
||||
- `GET /queue` → присутствует блок `serial_gate` (ORCH-088: `orchestrator.active_task = ORCH-118/testing`, не frozen) ✔; присутствует блок `auto_labels` ✔; breaker `closed`, preflight OK. Смок-регресса нет.
|
||||
|
||||
## Результаты (покрытие 04-test-plan.yaml → 03-acceptance-criteria.md)
|
||||
|
||||
| TC ID | Описание | AC | Результат |
|
||||
|-------|----------|----|-----------|
|
||||
| TC-01 | Единственный ТРАНСПОРТ LLM-консультации = `launcher._spawn` (CLAUDE_BIN + `--system-prompt` + Popen) | AC-1/AC-6a | PASS |
|
||||
| TC-02 | Детерминированные leaf-модули не консультируют LLM (дискриминатор «консультирует LLM», не «спавнит subprocess») | AC-3/AC-6b | PASS |
|
||||
| TC-03 | Анти-дрейф промптов: карта ↔ `.openclaw/agents/` (6 файлов, двусторонняя сверка) | AC-1/AC-6c | PASS |
|
||||
| TC-04 | Тотальность классификации (4 класса) + согласованность с осью C/P | AC-2/AC-6d | PASS |
|
||||
| TC-05 | keep-LLM требует названного суждения; C-keep (reviewer) фиксирует не-деривируемость | AC-2 | PASS |
|
||||
| TC-06 | Capability ≠ consultation: deploy-finalizer/post-deploy-monitor перехвачены до `_spawn` | AC-3/AC-6e | PASS |
|
||||
| TC-07 | Полнота roadmap + ровно один `first_slice=yes` (deployer) | AC-4 | PASS |
|
||||
| TC-08 | Политика LLM нормативна + определение «avoidable LLM control path» | AC-5/AC-10 | PASS |
|
||||
| TC-09 | Скоуп-гард: снимок ролей `STAGE_TRANSITIONS` и имён `QG_CHECKS` не изменился | AC-7 | PASS |
|
||||
| TC-10 | Полный регресс `pytest tests/ -q` зелёный | AC-6/AC-7 | PASS (2081 passed) |
|
||||
| TC-11 | Анти-фабрикация follow-up Plane-ID (кандидаты по роли) | AC-9 | PASS |
|
||||
| TC-12 | Отсутствие иного LLM-транспорта (нет anthropic/openai SDK, прямого HTTP, второго model-subprocess) | AC-1/AC-6f | PASS |
|
||||
| TC-13 | Control-path-ось C/P согласована с фактическим потребителем в `src/qg/checks.py` | AC-10/AC-6g | PASS |
|
||||
| TC-14 | Avoidable-набор зафиксирован = {tester, deployer}; reviewer=C-keep; analyst/architect/developer=P | AC-10/AC-6h | PASS |
|
||||
|
||||
Все 14 TC выполнены и сопоставлены с критериями приёмки. Пропусков нет.
|
||||
|
||||
## Вывод pytest
|
||||
|
||||
Целевые тесты ORCH-118:
|
||||
```
|
||||
tests/test_llm_call_site_inventory.py::test_tc01_single_llm_transport PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc12_no_alternative_llm_transport PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc02_deterministic_modules_no_llm_consultation PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc03_prompt_files_match_map PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc04_classification_total_and_axis_consistent PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc05_keep_llm_named_judgment PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc06_capability_not_consultation PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc09_runtime_contract_snapshot PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc13_control_path_axis_correct PASSED
|
||||
tests/test_llm_call_site_inventory.py::test_tc14_avoidable_set_fixed PASSED
|
||||
tests/test_llm_determinization_docs.py::test_tc07_roadmap_completeness_and_first_slice PASSED
|
||||
tests/test_llm_determinization_docs.py::test_tc08_policy_normative_and_defines_avoidable PASSED
|
||||
tests/test_llm_determinization_docs.py::test_tc11_no_fabricated_followup_ids PASSED
|
||||
======================== 13 passed, 1 warning in 0.41s =========================
|
||||
```
|
||||
|
||||
Полный регресс (TC-10):
|
||||
```
|
||||
2081 passed, 1 warning in 89.94s (0:01:29)
|
||||
```
|
||||
(Единственный warning — PydanticDeprecatedSince20 в `src/config.py:8`, предсуществующий, не связан с ORCH-118.)
|
||||
|
||||
## Итог
|
||||
**PASS** — все 14 тест-кейсов зелёные, полный регресс `tests/` зелёный (2081 passed), smoke API (`/health`, `/status`, `/queue` с блоками `serial_gate` + `auto_labels`) — без регресса. Задача готова к переходу на `deploy-staging`.
|
||||
12
docs/work-items/ORCH-118/14-deploy-log.md
Normal file
12
docs/work-items/ORCH-118/14-deploy-log.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
deploy_status: SUCCESS
|
||||
work_item: ORCH-118
|
||||
hook_exit_code: 0
|
||||
deployed_by: deploy-finalizer
|
||||
---
|
||||
|
||||
# Deploy log — ORCH-036 executable self-deploy
|
||||
|
||||
Прод-деплой завершён хост-хуком с exit-code `0` -> `deploy_status: SUCCESS`.
|
||||
|
||||
Вердикт зафиксирован детерминированным finalizer'ом (Фаза C), не LLM.
|
||||
29
docs/work-items/ORCH-118/15-staging-log.md
Normal file
29
docs/work-items/ORCH-118/15-staging-log.md
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
staging_status: SUCCESS
|
||||
work_item: ORCH-118
|
||||
stage: deploy-staging
|
||||
author_agent: deployer
|
||||
status: success
|
||||
created_at: 2026-06-16
|
||||
model_used: claude-opus-4-8
|
||||
timestamp: 2026-06-15T21:28:29Z
|
||||
base_url: http://localhost:8501
|
||||
---
|
||||
|
||||
# Staging Gate Log
|
||||
|
||||
Staging test suite completed against the live `orchestrator-staging` instance (port 8501),
|
||||
run canonically inside the container (`docker exec orchestrator-staging python3
|
||||
/repos/orchestrator/scripts/staging_check.py --base-url http://localhost:8501 --mode stub`).
|
||||
|
||||
**Result: 8/10 checks PASS — exit code 0 → `staging_status: SUCCESS`.**
|
||||
|
||||
All REAL pipeline checks are green (Block A SMOKE: A1/A2/A3; Block B ACCESS: B4/B5/B6
|
||||
registry isolation; Block C E2E: C7 create issue, C8 trigger pipeline). The only failures
|
||||
are the two known sandbox-infra checks (C9a/C9b), which are tolerated under ORCH-061 because
|
||||
SANDBOX bot accounts are not project members — they do not reflect a pipeline regression.
|
||||
|
||||
INFRA-WAIVED: C9a Branch appears in orchestrator-sandbox, C9b Analyst job enqueued in staging queue (known sandbox-infra; real checks green)
|
||||
VERDICT: SUCCESS (exit 0) — SUCCESS (infra-waived): ['C9a Branch appears in orchestrator-sandbox', 'C9b Analyst job enqueued in staging queue'] are known sandbox-infra checks; all real checks green
|
||||
|
||||
Cleanup ran in `finally`: Plane SANDBOX test issue deleted (HTTP 204); no branch to delete.
|
||||
448
tests/test_llm_call_site_inventory.py
Normal file
448
tests/test_llm_call_site_inventory.py
Normal file
@@ -0,0 +1,448 @@
|
||||
# ORCH-118 (FR-6 / AC-1, AC-3, AC-6, AC-7, AC-10): structural anti-drift tests for
|
||||
# the LLM call-site map (docs/architecture/llm-call-sites.md).
|
||||
#
|
||||
# UNIT OF INVENTORY = an *LLM consultation* (a control-path consumes an LLM
|
||||
# judgment), NOT "a process is spawned / Claude CLI exists" (R4, BRD §0). The
|
||||
# discriminator of every check below is therefore **"consults an LLM"**, never
|
||||
# "spawns a subprocess": the orchestrator spawns dozens of deterministic tools
|
||||
# (git / pytest / docker / ssh / scanners / staging_check.py) and those are
|
||||
# explicitly NOT matched — otherwise the test would degenerate into "count every
|
||||
# Popen" (the corruption these tests exist to avoid).
|
||||
#
|
||||
# These tests are fully offline and deterministic: no network, no LLM, no
|
||||
# subprocess-to-a-model. They read repository source + the canonical machine
|
||||
# blocks embedded in the map and assert the map stays in sync with the code.
|
||||
#
|
||||
# The map carries two machine-readable blocks the parser keys on:
|
||||
# * ORCH-118-INVENTORY-BLOCK — the call-site table (8 columns, D2)
|
||||
# * ORCH-118-KEEP-JUSTIFICATION-BLOCK — keep-LLM named-judgment list (TC-05)
|
||||
# Both are human-readable markdown AND machine-parseable (stdlib split), per
|
||||
# ADR-001 D2/D5 (no brittle prose regex).
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from src.qg.checks import QG_CHECKS
|
||||
from src.stages import STAGE_TRANSITIONS
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = REPO_ROOT / "src"
|
||||
WATCHDOG = REPO_ROOT / "watchdog"
|
||||
AGENTS_DIR = REPO_ROOT / ".openclaw" / "agents"
|
||||
MAP = REPO_ROOT / "docs" / "architecture" / "llm-call-sites.md"
|
||||
|
||||
# The single allowed transport (S0): launcher._spawn builds + launches the Claude CLI.
|
||||
TRANSPORT_FILE = "src/agents/launcher.py"
|
||||
|
||||
# Roles split by control-path axis (§0-bis). Ground truth lives in src/qg/checks.py
|
||||
# (the deterministic consumers) — the map must mirror it.
|
||||
P_ROLES = frozenset({"analyst", "architect", "developer"})
|
||||
C_ROLES = frozenset({"reviewer", "tester", "deployer"})
|
||||
AGENT_ROLES = P_ROLES | C_ROLES
|
||||
|
||||
# P-roles are consumed by deterministic gates that judge an ARTIFACT (file
|
||||
# presence / CI) independently of any LLM self-report.
|
||||
P_CONSUMERS = frozenset(
|
||||
{"check_analysis_complete", "check_architecture_done", "check_ci_green"}
|
||||
)
|
||||
# C-roles are consumed by verdict-parsers that READ a machine-verdict the LLM wrote
|
||||
# — the LLM judgment branches the control flow (PASS->advance / FAIL->rollback).
|
||||
C_CONSUMERS = frozenset(
|
||||
{
|
||||
"check_reviewer_verdict",
|
||||
"_parse_tests_verdict",
|
||||
"_parse_staging_status",
|
||||
"_parse_deploy_status",
|
||||
}
|
||||
)
|
||||
|
||||
ALLOWED_CLASSES = frozenset(
|
||||
{
|
||||
"keep-LLM",
|
||||
"replace-deterministic-now",
|
||||
"replace-later/risky",
|
||||
"needs-hybrid-fallback",
|
||||
"already-deterministic",
|
||||
}
|
||||
)
|
||||
|
||||
AGENT_IDS = frozenset({"A1", "A2", "A3", "A4", "A5", "A6"})
|
||||
ALL_IDS = frozenset({"S0", "A1", "A2", "A3", "A4", "A5", "A6", "D1", "D2"})
|
||||
|
||||
# Deterministic leaf modules / routing that must NOT consult an LLM (FR-3 / AC-3).
|
||||
DETERMINISTIC_MODULES = (
|
||||
"stages",
|
||||
"stage_engine",
|
||||
"serial_gate",
|
||||
"merge_gate",
|
||||
"coverage_gate",
|
||||
"security_gate",
|
||||
"staging_verdict",
|
||||
"review_parse",
|
||||
"error_classifier",
|
||||
"frontmatter",
|
||||
"self_deploy",
|
||||
"post_deploy",
|
||||
"transition_lease",
|
||||
"reconciler",
|
||||
"job_reaper",
|
||||
)
|
||||
|
||||
# Alternative-LLM-transport signatures forbidden anywhere in src/** + watchdog/**
|
||||
# (TC-12 / FR-6f): an LLM SDK import or a direct Anthropic/Claude HTTP endpoint.
|
||||
_FORBIDDEN_TRANSPORT_RE = (
|
||||
re.compile(r"^\s*(?:from|import)\s+anthropic\b", re.M),
|
||||
re.compile(r"^\s*(?:from|import)\s+openai\b", re.M),
|
||||
re.compile(r"api\.anthropic\.com"),
|
||||
re.compile(r"/v1/messages"),
|
||||
)
|
||||
|
||||
# Frozen snapshot of the runtime contract (TC-09 / FR-7 / AC-7). ORCH-118 is
|
||||
# docs+tests-only; if this drifts, the map task touched the stage machine / gates.
|
||||
EXPECTED_STAGE_AGENTS = frozenset(
|
||||
{"analyst", "architect", "developer", "reviewer", "tester", "deployer"}
|
||||
)
|
||||
EXPECTED_QG_CHECKS = frozenset(
|
||||
{
|
||||
"check_analysis_approved",
|
||||
"check_analysis_complete",
|
||||
"check_architecture_done",
|
||||
"check_ci_green",
|
||||
"check_review_approved",
|
||||
"check_tests_passed",
|
||||
"check_reviewer_verdict",
|
||||
"check_tests_local",
|
||||
"check_deploy_status",
|
||||
"check_staging_status",
|
||||
"check_branch_mergeable",
|
||||
"check_staging_image_fresh",
|
||||
"check_security_gate",
|
||||
"check_coverage_gate",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers (stdlib only)
|
||||
# ---------------------------------------------------------------------------
|
||||
def _src_py_files() -> list[Path]:
|
||||
return sorted(SRC.glob("**/*.py"))
|
||||
|
||||
|
||||
def _src_and_watchdog_py_files() -> list[Path]:
|
||||
files = list(SRC.glob("**/*.py"))
|
||||
if WATCHDOG.is_dir():
|
||||
files.extend(WATCHDOG.glob("**/*.py"))
|
||||
return sorted(files)
|
||||
|
||||
|
||||
def _rel(p: Path) -> str:
|
||||
return p.relative_to(REPO_ROOT).as_posix()
|
||||
|
||||
|
||||
def _function_body(source: str, name: str) -> str:
|
||||
"""Return the source text of ``def <name>`` up to (excluding) the next
|
||||
same-or-lower-indent def/class/decorator. Robust to line drift."""
|
||||
lines = source.splitlines()
|
||||
start = None
|
||||
indent = 0
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.lstrip()
|
||||
if stripped.startswith(f"def {name}("):
|
||||
start = i
|
||||
indent = len(line) - len(stripped)
|
||||
break
|
||||
assert start is not None, f"def {name}( not found in source"
|
||||
body = [lines[start]]
|
||||
for line in lines[start + 1 :]:
|
||||
if not line.strip():
|
||||
body.append(line)
|
||||
continue
|
||||
cur_indent = len(line) - len(line.lstrip())
|
||||
head = line.lstrip()
|
||||
if cur_indent <= indent and head.startswith(("def ", "class ", "@")):
|
||||
break
|
||||
body.append(line)
|
||||
return "\n".join(body)
|
||||
|
||||
|
||||
def _extract_block(text: str, name: str) -> str:
|
||||
start = f"<!-- {name}:START -->"
|
||||
end = f"<!-- {name}:END -->"
|
||||
assert start in text, f"missing block start marker {start!r} in map"
|
||||
assert end in text, f"missing block end marker {end!r} in map"
|
||||
return text.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
|
||||
def _parse_pipe_table(block: str) -> list[dict]:
|
||||
"""Parse a GitHub-style pipe table into a list of {column: value} dicts."""
|
||||
header = None
|
||||
rows: list[dict] = []
|
||||
for raw in block.splitlines():
|
||||
line = raw.strip()
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
joined = "".join(cells)
|
||||
if joined and set(joined) <= set("-: "):
|
||||
continue # separator row |---|---|
|
||||
if header is None:
|
||||
header = [c.lower() for c in cells]
|
||||
continue
|
||||
rows.append(dict(zip(header, cells)))
|
||||
return rows
|
||||
|
||||
|
||||
def _inventory_rows() -> list[dict]:
|
||||
block = _extract_block(MAP.read_text(encoding="utf-8"), "ORCH-118-INVENTORY-BLOCK")
|
||||
rows = _parse_pipe_table(block)
|
||||
assert rows, "inventory block parsed to zero rows"
|
||||
return rows
|
||||
|
||||
|
||||
def _agent_rows() -> list[dict]:
|
||||
return [r for r in _inventory_rows() if r["id"] in AGENT_IDS]
|
||||
|
||||
|
||||
def _by_role() -> dict[str, dict]:
|
||||
return {r["role"]: r for r in _agent_rows()}
|
||||
|
||||
|
||||
def _parse_justifications() -> dict[str, str]:
|
||||
"""Parse the keep-LLM named-judgment list: ``- role: justification text``."""
|
||||
block = _extract_block(
|
||||
MAP.read_text(encoding="utf-8"), "ORCH-118-KEEP-JUSTIFICATION-BLOCK"
|
||||
)
|
||||
out: dict[str, str] = {}
|
||||
for raw in block.splitlines():
|
||||
line = raw.strip()
|
||||
m = re.match(r"^[-*]\s*([A-Za-z_-]+)\s*:\s*(.+)$", line)
|
||||
if m:
|
||||
out[m.group(1).strip()] = m.group(2).strip()
|
||||
return out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-01 — single LLM-consultation transport (necessary, completed by TC-12).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc01_single_llm_transport():
|
||||
"""Exactly one place in src/** assembles+launches the Claude CLI, matched by
|
||||
the CONJUNCTION of transport signals (CLAUDE_BIN AND --system-prompt AND a
|
||||
process launcher) — and it is launcher._spawn. The conjunction is mandatory:
|
||||
bare CLAUDE_BIN would false-positive on preflight.py (existence check) and
|
||||
config.py (path literal), neither of which consults an LLM (ADR D5a)."""
|
||||
hits = []
|
||||
for f in _src_py_files():
|
||||
text = f.read_text(encoding="utf-8")
|
||||
launches = ("Popen" in text) or ('"bash"' in text) or ("'bash'" in text)
|
||||
if "--system-prompt" in text and "CLAUDE_BIN" in text and launches:
|
||||
hits.append(_rel(f))
|
||||
assert hits == [TRANSPORT_FILE], (
|
||||
"expected the single LLM-transport to be launcher._spawn; got: " + repr(hits)
|
||||
)
|
||||
# The transport assembly lives inside _spawn specifically.
|
||||
launcher = (SRC / "agents" / "launcher.py").read_text(encoding="utf-8")
|
||||
assert "--system-prompt" in _function_body(launcher, "_spawn"), (
|
||||
"--system-prompt is not inside def _spawn — transport moved?"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-12 — no alternative LLM transport (FR-6f / AC-1, AC-6).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc12_no_alternative_llm_transport():
|
||||
"""No LLM-SDK import, no direct Anthropic/Claude HTTP endpoint, and no SECOND
|
||||
--system-prompt-bearing subprocess builder anywhere in src/** + watchdog/**.
|
||||
Closes the gap 'one _spawn green, a new consultation grew next to it'."""
|
||||
sdk_offenders = []
|
||||
for f in _src_and_watchdog_py_files():
|
||||
text = f.read_text(encoding="utf-8")
|
||||
for rx in _FORBIDDEN_TRANSPORT_RE:
|
||||
if rx.search(text):
|
||||
sdk_offenders.append(f"{_rel(f)}: {rx.pattern}")
|
||||
assert not sdk_offenders, (
|
||||
"alternative LLM transport found (allowed transport = S0/launcher._spawn "
|
||||
"only):\n" + "\n".join(sdk_offenders)
|
||||
)
|
||||
# No second --system-prompt builder outside the allowlisted transport file.
|
||||
second_builders = [
|
||||
_rel(f)
|
||||
for f in _src_and_watchdog_py_files()
|
||||
if "--system-prompt" in f.read_text(encoding="utf-8")
|
||||
and _rel(f) != TRANSPORT_FILE
|
||||
]
|
||||
assert second_builders == [], (
|
||||
"a second --system-prompt subprocess builder appeared: " + repr(second_builders)
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-02 — deterministic modules carry no LLM consultation (FR-6b / AC-3).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc02_deterministic_modules_no_llm_consultation():
|
||||
"""The listed routing/leaf modules do not consult an LLM (no _spawn transport,
|
||||
no alternative transport). Their git/pytest/docker/ssh/scanner subprocesses are
|
||||
deterministic TOOLS, not LLM consultations — discriminator is 'consults LLM',
|
||||
not 'spawns subprocess'."""
|
||||
offenders = []
|
||||
for mod in DETERMINISTIC_MODULES:
|
||||
path = SRC / f"{mod}.py"
|
||||
assert path.is_file(), f"deterministic module missing: {path}"
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if "--system-prompt" in text:
|
||||
offenders.append(f"{mod}: builds --system-prompt (LLM transport)")
|
||||
for rx in _FORBIDDEN_TRANSPORT_RE:
|
||||
if rx.search(text):
|
||||
offenders.append(f"{mod}: {rx.pattern}")
|
||||
assert not offenders, "LLM consultation found in deterministic path:\n" + "\n".join(
|
||||
offenders
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-03 — prompt files on disk match the map, both ways (FR-6c / AC-1).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc03_prompt_files_match_map():
|
||||
on_disk = {p.stem for p in AGENTS_DIR.glob("*.md")}
|
||||
in_map = {r["role"] for r in _agent_rows()}
|
||||
assert on_disk == set(AGENT_ROLES), (
|
||||
f"prompt files on disk drifted from the 6 canonical roles: {on_disk}"
|
||||
)
|
||||
assert in_map == on_disk, (
|
||||
f"map agent roles {in_map} != prompt files on disk {on_disk}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-04 — totality + axis-consistent classification (FR-6d / FR-2 / AC-2).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc04_classification_total_and_axis_consistent():
|
||||
rows = _inventory_rows()
|
||||
ids = [r["id"] for r in rows]
|
||||
assert len(ids) == len(set(ids)), f"duplicate call-site ids: {ids}"
|
||||
assert set(ids) == set(ALL_IDS), f"call-site id set drifted: {set(ids)}"
|
||||
|
||||
for r in rows:
|
||||
cls = r["classification"]
|
||||
if r["id"] == "S0":
|
||||
assert cls in ("-", "—"), f"S0 (transport) must not be classified: {cls!r}"
|
||||
else:
|
||||
assert cls in ALLOWED_CLASSES or cls in ("-", "—"), (
|
||||
f"{r['id']} class out of taxonomy: {cls!r}"
|
||||
)
|
||||
|
||||
# Class is DERIVED from the axis (not postulated): P->keep; C+!avoidable->keep;
|
||||
# C+avoidable->replace-*/hybrid.
|
||||
for r in _agent_rows():
|
||||
axis = r["axis"].upper()
|
||||
avoidable = r["avoidable"].lower()
|
||||
cls = r["classification"]
|
||||
if axis == "P":
|
||||
assert cls == "keep-LLM", f"{r['role']} is P but class {cls!r}"
|
||||
elif axis == "C" and avoidable == "no":
|
||||
assert cls == "keep-LLM", f"{r['role']} is C-keep but class {cls!r}"
|
||||
elif axis == "C" and avoidable == "yes":
|
||||
assert cls in {
|
||||
"replace-deterministic-now",
|
||||
"replace-later/risky",
|
||||
"needs-hybrid-fallback",
|
||||
}, f"{r['role']} is avoidable but class {cls!r}"
|
||||
else:
|
||||
raise AssertionError(f"{r['role']}: bad axis/avoidable {axis!r}/{avoidable!r}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-05 — keep-LLM requires a named judgment; C-keep states non-derivability.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc05_keep_llm_named_judgment():
|
||||
keep_roles = {r["role"] for r in _agent_rows() if r["classification"] == "keep-LLM"}
|
||||
assert keep_roles == {"analyst", "architect", "developer", "reviewer"}, (
|
||||
f"keep-LLM role set drifted: {keep_roles}"
|
||||
)
|
||||
just = _parse_justifications()
|
||||
for role in keep_roles:
|
||||
assert just.get(role, "").strip(), f"keep-LLM role {role} has no named judgment"
|
||||
# reviewer is C-keep: its justification must explain NON-derivability of the verdict.
|
||||
assert "deriv" in just["reviewer"].lower(), (
|
||||
"reviewer (C-keep) justification must state the verdict is NOT derivable "
|
||||
"from an exit-code"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-06 — capability != consultation: D1/D2 intercepted before _spawn (FR-6e).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc06_capability_not_consultation():
|
||||
launcher = (SRC / "agents" / "launcher.py").read_text(encoding="utf-8")
|
||||
body = _function_body(launcher, "launch_job")
|
||||
i_finalizer = body.find('"deploy-finalizer"')
|
||||
i_monitor = body.find('"post-deploy-monitor"')
|
||||
i_spawn = body.find("self._spawn(")
|
||||
assert i_finalizer != -1, "deploy-finalizer guard not found in launch_job"
|
||||
assert i_monitor != -1, "post-deploy-monitor guard not found in launch_job"
|
||||
assert i_spawn != -1, "self._spawn( call not found in launch_job"
|
||||
assert i_finalizer < i_spawn, "deploy-finalizer guard must precede _spawn"
|
||||
assert i_monitor < i_spawn, "post-deploy-monitor guard must precede _spawn"
|
||||
assert "return self._run_deploy_finalizer_job" in body
|
||||
assert "return self._run_post_deploy_monitor_job" in body
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-09 — runtime contract snapshot unchanged (FR-7 / AC-7).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc09_runtime_contract_snapshot():
|
||||
agents = {t["agent"] for t in STAGE_TRANSITIONS.values() if t["agent"]}
|
||||
assert agents == set(EXPECTED_STAGE_AGENTS), (
|
||||
f"STAGE_TRANSITIONS agent set changed: {agents}"
|
||||
)
|
||||
assert set(QG_CHECKS) == set(EXPECTED_QG_CHECKS), (
|
||||
f"QG_CHECKS name set changed: {set(QG_CHECKS)}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-13 — control-path axis is consistent with the real consumer (FR-6g / AC-10).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc13_control_path_axis_correct():
|
||||
checks_src = (SRC / "qg" / "checks.py").read_text(encoding="utf-8")
|
||||
rows = _agent_rows()
|
||||
for r in rows:
|
||||
role = r["role"]
|
||||
axis = r["axis"].upper()
|
||||
consumer = r["output_consumer"].split(":")[0].strip()
|
||||
assert re.search(rf"def {re.escape(consumer)}\(", checks_src), (
|
||||
f"{role}: output_consumer {consumer!r} is not a def in src/qg/checks.py"
|
||||
)
|
||||
if role in P_ROLES:
|
||||
assert axis == "P", f"{role} must be axis P, got {axis!r}"
|
||||
assert consumer in P_CONSUMERS, (
|
||||
f"{role} (P) consumer {consumer!r} is not a deterministic artifact gate"
|
||||
)
|
||||
elif role in C_ROLES:
|
||||
assert axis == "C", f"{role} must be axis C, got {axis!r}"
|
||||
assert consumer in C_CONSUMERS, (
|
||||
f"{role} (C) consumer {consumer!r} is not a verdict-parser"
|
||||
)
|
||||
else:
|
||||
raise AssertionError(f"unexpected agent role in map: {role!r}")
|
||||
assert {r["role"] for r in rows if r["axis"].upper() == "P"} == set(P_ROLES)
|
||||
assert {r["role"] for r in rows if r["axis"].upper() == "C"} == set(C_ROLES)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-14 — avoidable LLM control-path set is exactly {tester, deployer} (FR-6h).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc14_avoidable_set_fixed():
|
||||
rows = _agent_rows()
|
||||
by_role = {r["role"]: r for r in rows}
|
||||
avoidable = {r["role"] for r in rows if r["avoidable"].lower() == "yes"}
|
||||
assert avoidable == {"tester", "deployer"}, (
|
||||
f"avoidable LLM control-path set drifted from {{tester, deployer}}: {avoidable}"
|
||||
)
|
||||
# reviewer: control path (C) but KEEP — verdict not derivable.
|
||||
assert by_role["reviewer"]["axis"].upper() == "C"
|
||||
assert by_role["reviewer"]["avoidable"].lower() == "no"
|
||||
# analyst / architect / developer: NOT control path (P artifact-producer).
|
||||
for role in ("analyst", "architect", "developer"):
|
||||
assert by_role[role]["axis"].upper() == "P", f"{role} must be P (not control path)"
|
||||
assert by_role[role]["avoidable"].lower() == "no", f"{role} must not be avoidable"
|
||||
152
tests/test_llm_determinization_docs.py
Normal file
152
tests/test_llm_determinization_docs.py
Normal file
@@ -0,0 +1,152 @@
|
||||
# ORCH-118 (FR-4 / FR-5 / FR-8 / AC-4, AC-5, AC-9, AC-10): structural tests for the
|
||||
# determinization roadmap and the LLM usage policy.
|
||||
#
|
||||
# These are offline/deterministic (no network, no LLM, no subprocess). They assert
|
||||
# the roadmap carries the mandatory per-candidate attributes (named BY ROLE, never a
|
||||
# fabricated Plane-ID), that the policy is normative and defines "avoidable LLM
|
||||
# control path" as a checkable predicate, and that NO doc binds a candidate to a
|
||||
# non-existent follow-up Plane-ID (R3 / NFR-6 anti-fabrication).
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
ARCH = REPO_ROOT / "docs" / "architecture"
|
||||
MAP = ARCH / "llm-call-sites.md"
|
||||
ROADMAP = ARCH / "llm-determinization-roadmap.md"
|
||||
POLICY = ARCH / "llm-usage-policy.md"
|
||||
WORK_ITEMS = REPO_ROOT / "docs" / "work-items"
|
||||
|
||||
# A follow-up Plane-ID pattern in the ORCH-1XX range. ORCH-118 itself is allowed;
|
||||
# any OTHER ORCH-1XX referenced in a doc must resolve to a real work-item dir —
|
||||
# this catches the R2 anti-pattern of binding the map to invented IDs
|
||||
# (ORCH-115 / ORCH-116, which do not exist).
|
||||
_PLANE_ID_RE = re.compile(r"ORCH-1\d\d")
|
||||
_SELF_ID = "ORCH-118"
|
||||
|
||||
|
||||
def _extract_block(text: str, name: str) -> str:
|
||||
start = f"<!-- {name}:START -->"
|
||||
end = f"<!-- {name}:END -->"
|
||||
assert start in text, f"missing block start marker {start!r}"
|
||||
assert end in text, f"missing block end marker {end!r}"
|
||||
return text.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
|
||||
def _parse_pipe_table(block: str) -> list[dict]:
|
||||
header = None
|
||||
rows: list[dict] = []
|
||||
for raw in block.splitlines():
|
||||
line = raw.strip()
|
||||
if not line.startswith("|"):
|
||||
continue
|
||||
cells = [c.strip() for c in line.strip("|").split("|")]
|
||||
joined = "".join(cells)
|
||||
if joined and set(joined) <= set("-: "):
|
||||
continue
|
||||
if header is None:
|
||||
header = [c.lower() for c in cells]
|
||||
continue
|
||||
rows.append(dict(zip(header, cells)))
|
||||
return rows
|
||||
|
||||
|
||||
def _roadmap_rows() -> list[dict]:
|
||||
block = _extract_block(ROADMAP.read_text(encoding="utf-8"), "ORCH-118-ROADMAP-BLOCK")
|
||||
rows = _parse_pipe_table(block)
|
||||
assert rows, "roadmap block parsed to zero rows"
|
||||
return rows
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-07 — roadmap completeness + recommended first slice (FR-4 / AC-4).
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc07_roadmap_completeness_and_first_slice():
|
||||
rows = _roadmap_rows()
|
||||
roles = {r["role"] for r in rows}
|
||||
# The two avoidable LLM control paths are the roadmap candidates.
|
||||
assert {"deployer", "tester"} <= roles, f"roadmap missing candidates: {roles}"
|
||||
|
||||
ranks = []
|
||||
first_slice_roles = []
|
||||
for r in rows:
|
||||
role = r["role"]
|
||||
assert r["dependencies"].strip(), f"{role}: empty dependencies"
|
||||
# Savings estimate must cite its source (agent_runs / usage).
|
||||
assert "agent_runs" in r["savings_estimate_source"], (
|
||||
f"{role}: savings estimate not sourced from agent_runs"
|
||||
)
|
||||
assert r["security_risk"].strip(), f"{role}: empty security_risk"
|
||||
assert r["hybrid_needed"].lower() in {"yes", "no"}, (
|
||||
f"{role}: hybrid_needed must be yes/no, got {r['hybrid_needed']!r}"
|
||||
)
|
||||
# follow-up is named BY ROLE, never a Plane-ID (R3 / NFR-6 / AC-9).
|
||||
ftype = r["followup_type"]
|
||||
assert ftype.strip(), f"{role}: empty followup_type"
|
||||
assert not re.search(r"ORCH-\d+", ftype), (
|
||||
f"{role}: followup_type binds a Plane-ID ({ftype!r}) — forbidden (AC-9)"
|
||||
)
|
||||
assert role in ftype, f"{role}: followup_type must name the role, got {ftype!r}"
|
||||
ranks.append(int(r["rank"]))
|
||||
if r["first_slice"].lower() == "yes":
|
||||
first_slice_roles.append(role)
|
||||
|
||||
assert ranks == sorted(ranks), f"roadmap not ordered by rank: {ranks}"
|
||||
assert len(set(ranks)) == len(ranks), f"duplicate ranks: {ranks}"
|
||||
# Exactly one recommended first slice, and it is the deployer (staging) replacement.
|
||||
assert first_slice_roles == ["deployer"], (
|
||||
f"recommended first slice must be exactly [deployer]; got {first_slice_roles}"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-08 — policy exists, is normative, and defines "avoidable LLM control path".
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc08_policy_normative_and_defines_avoidable():
|
||||
assert POLICY.is_file(), "llm-usage-policy.md missing"
|
||||
text = POLICY.read_text(encoding="utf-8")
|
||||
|
||||
# Principle: LLM only where genuine judgment is needed.
|
||||
assert "настоящее суждение" in text, "policy missing the core principle"
|
||||
# keep vs replace criteria, framed through the control-path axis.
|
||||
low = text.lower()
|
||||
assert "keep" in low and "replace" in low, "policy missing keep/replace criteria"
|
||||
assert "control path" in low or "control-path" in low, (
|
||||
"policy keep/replace criteria not framed through the control-path axis"
|
||||
)
|
||||
|
||||
# The defined term appears as a defined term.
|
||||
assert "avoidable llm control path" in low, (
|
||||
"policy does not define the term 'avoidable LLM control path'"
|
||||
)
|
||||
# Machine-readable definition block: two-bit predicate (C consultation AND
|
||||
# derivable verdict).
|
||||
block = _extract_block(text, "ORCH-118-AVOIDABLE-DEFINITION-BLOCK").lower()
|
||||
assert "control" in block, "definition missing the control-path condition (i)"
|
||||
assert "deriv" in block, "definition missing the derivability condition (ii)"
|
||||
# The verdict-derivability condition names a real tool signal.
|
||||
assert any(sig in block for sig in ("exit-code", "exit code", "pytest", "staging_check")), (
|
||||
"derivability condition does not reference a concrete tool signal"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# TC-11 — anti-fabrication: no candidate bound to a non-existent follow-up ID.
|
||||
# ---------------------------------------------------------------------------
|
||||
def test_tc11_no_fabricated_followup_ids():
|
||||
"""Every ORCH-1XX referenced in the map / roadmap / policy (other than ORCH-118
|
||||
itself) MUST resolve to a real docs/work-items/ dir. This catches the R2 defect
|
||||
of pinning the map to invented IDs (ORCH-115 / ORCH-116)."""
|
||||
offenders = []
|
||||
for doc in (MAP, ROADMAP, POLICY):
|
||||
assert doc.is_file(), f"doc missing: {doc}"
|
||||
text = doc.read_text(encoding="utf-8")
|
||||
for token in set(_PLANE_ID_RE.findall(text)):
|
||||
if token == _SELF_ID:
|
||||
continue
|
||||
if not (WORK_ITEMS / token).is_dir():
|
||||
offenders.append(f"{doc.name}: references non-existent work item {token}")
|
||||
assert not offenders, (
|
||||
"fabricated / unresolvable follow-up Plane-ID(s) found (name follow-ups BY "
|
||||
"ROLE, not by invented ID — R3 / NFR-6 / AC-9):\n" + "\n".join(offenders)
|
||||
)
|
||||
Reference in New Issue
Block a user