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
