Files
orchestrator/docs/architecture/llm-usage-policy.md
claude-bot 9d16ee473a feat(testing): deterministic test-runner replacing LLM tester on the testing stage (ORCH-116)
Second realised slice of the determinization-roadmap (ORCH-118 A5,
needs-hybrid-fallback): on the `testing` stage for the self-hosting
`orchestrator` repo the LLM `tester` agent is replaced by a deterministic
test-runner (src/test_runner.py), intercepted in launch_job BEFORE _spawn
(deploy-finalizer / post-deploy-monitor / staging-runner precedent).

It runs the regression `python -m pytest <target>` in the task worktree via
proc_group (tree-kill) + an optional read-only smoke (/health, /status, /queue
+ serial_gate), maps the exit-code -> result: PASS|FAIL via the existing
self_deploy.map_exit_code_to_status contract, writes 13-test-report.md and
initiates the EXISTING check_tests_passed gate exactly as a finished LLM-tester.

Invariant (NFR-1): only the *producer* changes — the artifact contract
(13-test-report.md / result:), the gate check_tests_passed / _parse_tests_verdict,
STAGE_TRANSITIONS and the DB schema are byte-for-byte UNCHANGED. Additive, under
a kill-switch (test_runner_enabled), never-raise, fail-closed, self-hosting scope,
two-level outcome (tool-error DEFER, anti ORCH-110), hybrid (LLM strictly
off-control-path). 52c-`status:` is aligned with the verdict (D6.1) so the
three-field _parse_tests_verdict never false-negatives a PASS.

Docs (ORCH-118 NFR-6, atomic with code): llm-call-sites.md (A5 implemented),
llm-determinization-roadmap.md (rank 2 implemented), llm-usage-policy.md,
README/internals/overview, tester.md, CLAUDE.md, CHANGELOG.md. Coverage:
tests/test_orch116_test_runner.py (TC-01..TC-14); LLM anti-drift tests green.
Full suite: 2137 passed.

Refs: ORCH-116
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:37:40 +03:00

109 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
- **Реализованный срез (ORCH-115).** Снятие C-консультации с деривируемым вердиктом — это разрешённое
`replace-deterministic-now`, а не ввод новой LLM-консультации. ORCH-115 снял A6/staging-status:
детерминированный `src/staging_runner.py` производит `staging_status:` без `_spawn` (перехват до
него, как `D1`/`D2`) — раннер **LLM не зовёт** и **второй транспорт не вводит**, поэтому инвариант
«единственный транспорт S0» соблюдён (TC-12 зелёный). Это образец для последующих срезов roadmap'а.
- **Реализованный срез (ORCH-116).** ORCH-116 снял A5/tester тем же паттерном: детерминированный
`src/test_runner.py` производит `result:` на `testing` для in-scope репо без `_spawn` (перехват до
него, как `D1`/`D2`) — exit-код `pytest` в worktree + read-only smoke. Это `needs-hybrid-fallback`
(детерминированное ядро вынесено; LLM-фолбэк на не-деривируемое суждение — триаж падений / маппинг
TC↔критерии — остаётся **off-control-path** и **не** производит `result:`). Раннер **LLM не зовёт** и
**второй транспорт не вводит** → инвариант «единственный транспорт S0» соблюдён (TC-12 зелёный).
Будущий off-control-path триаж — **не** новый транспорт control-path-консультации (он вне control-path).