# ТЗ — ORCH-046: встраивание текста findings reviewer/tester в task_desc

Work Item ID: ORCH-046
Stage: analysis
Author: analyst
Date: 2026-06-06

> Вариант A (минимальный, низкий риск). Это правка ЯДРА — обязателен ADR
> (per-work-item, `docs/work-items/ORCH-046/06-adr/`).

## 1. Задействованные модули `src/`

| Модуль | Изменение |
|--------|-----------|
| `src/review_parse.py` | **НОВЫЙ** хелпер-парсер: `extract_review_findings(path) -> str`, `extract_test_failures(path) -> str`. |
| `src/stage_engine.py` | Две ветки в `_handle_qg_failure_rollbacks`: reviewer REQUEST_CHANGES (~стр. 419) и tester `check_tests_passed` FAIL (~стр. 455) — встраивают извлечённый текст в `task_desc`. |

Источники-образцы (не менять, использовать как референс паттерна «never raise» и
формата артефактов):
- `src/qg/checks.py::_parse_tests_verdict` — образец «never raise», split по `---`, `yaml.safe_load`.
- `src/frontmatter.py::read_frontmatter_value` — образец defensive-парсера.
- `.openclaw/agents/reviewer.md` — канонический формат `12-review.md`.
- `.openclaw/agents/tester.md` — канонический формат `13-test-report.md`.

## 2. Новый модуль `src/review_parse.py`

### 2.1. `extract_review_findings(path: str) -> str`

Назначение: вернуть **дословный** текст must-fix findings (P0 + P1) из
`12-review.md` для встраивания в `task_desc`.

Формат входного файла (канон reviewer.md, секция `## Findings`):

```markdown
## Findings

### P0 — Blocker
- [ ] <описание>

### P1 — Must fix
- [ ] <описание>

### P2 — Should fix
- [ ] <описание>
```

Требования к реализации:

1. **Никогда не бросает исключение.** Любая ошибка (нет файла, IOError, кривой
   markdown, нет секции `## Findings`) → возврат `""` (пустая строка).
2. Парсит **только** подсекции P0 и P1 (must-fix). P2/P3 игнорируются.
3. Заголовки подсекций распознаются устойчиво к регистру и к тире/дефису:
   соответствие по наличию токена `P0` / `P1` в строке-заголовке уровня `###`.
4. Из распознанных подсекций берётся текст до следующего заголовка `###`/`##`
   (т. е. тело подсекции дословно: пункты списка `- [ ] …` / `- …`).
5. Пустые подсекции (нет содержательных пунктов, только `(если есть)`-плейсхолдер
   или ничего) — пропускаются. Если ни одного содержательного P0/P1 пункта нет
   → возврат `""`.
6. Результат — компактный многострочный текст, пригодный для вставки в
   `task_desc` (например, заголовок подсекции + её пункты). Длина результата
   ограничивается разумным лимитом (`MAX_FINDINGS_CHARS`, напр. 2000) с
   усечением и маркером `…(truncated)`; полный контекст всё равно остаётся в
   файле.
7. Frontmatter (верхний `--- … ---`) при необходимости отбрасывается, чтобы не
   попасть в тело; парсинг секции делается по телу markdown.

Сигнатура и контракт (стабильны):
```python
def extract_review_findings(path: str) -> str:
    """Дословный текст P0/P1 findings из 12-review.md. Never raises; '' при ошибке/пусто."""
```

### 2.2. `extract_test_failures(path: str) -> str`

Назначение: вернуть текст причины падения тестов из `13-test-report.md` для
встраивания в `task_desc`.

Формат входного файла (канон tester.md): frontmatter `result: PASS|FAIL`, далее
тело с секциями `## Результаты` (таблица TC), `## Вывод pytest`, `## Итог`.

Требования к реализации:

1. **Никогда не бросает исключение.** Любая ошибка → возврат `""`.
2. Извлекает релевантный фрагмент тела, помогающий понять причину FAIL.
   Приоритет источников (берём первый непустой):
   - секция `## Вывод pytest` (вывод прогона — где видно упавшие тесты), и/или
   - строки таблицы `## Результаты`, содержащие `FAIL`, и/или
   - секция `## Итог`.
3. Результат усекается до `MAX_FAILURES_CHARS` (напр. 2000) с маркером
   `…(truncated)`.
4. Если ничего извлечь не удалось → возврат `""` (вызывающий код делает
   fallback на ссылку).

> Примечание: «reason» из самого гейта (`check_tests_passed` → второй элемент
> кортежа) у вызывающего кода уже есть (`reason`) — он добавляется в `task_desc`
> вызывающим кодом (как и сейчас в комментарии тестера). `extract_test_failures`
> добавляет **фрагмент тела отчёта** поверх этого reason.

Сигнатура и контракт (стабильны):
```python
def extract_test_failures(path: str) -> str:
    """Релевантный фрагмент тела 13-test-report.md (причина FAIL). Never raises; '' при ошибке/пусто."""
```

### 2.3. Общие требования модуля

- Модуль логирует диагностические сообщения на уровне `logger.debug`
  (`logging.getLogger("orchestrator.review_parse")`), как `frontmatter.py`.
- Никаких сетевых вызовов, только чтение файла с диска.
- Константы лимитов вынесены модульными (`MAX_FINDINGS_CHARS`,
  `MAX_FAILURES_CHARS`).

## 3. Изменения `src/stage_engine.py`

### 3.1. Ветка reviewer REQUEST_CHANGES (внутри `_handle_qg_failure_rollbacks`)

Текущее (~стр. 418–424):
```python
task_desc = (
    f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\n"
    f"Stage: development\nNote: REQUEST_CHANGES from reviewer "
    f"(attempt {retry_count+1}/3). Fix findings in "
    f"docs/work-items/{work_item_id}/12-review.md"
)
```

Целевое поведение:
- Сформировать путь к `12-review.md` через `get_worktree_path(repo, branch)` +
  `docs/work-items/{work_item_id}/12-review.md` (как в `_check_review_approved_by_branch`).
- Вызвать `extract_review_findings(path)`.
- Если результат непустой — встроить findings **дословно** в `task_desc`
  (под подзаголовком, напр. `Findings (P0/P1):\n<text>`), а ссылку на файл
  оставить как «полный контекст» (`Полный контекст: docs/work-items/<id>/12-review.md`).
- Если результат пустой (graceful fallback) — `task_desc` остаётся **как
  сейчас** (ссылка-строка). Никаких исключений.
- Префиксная часть (`Work item / Repo / Branch / Stage / Note: REQUEST_CHANGES …
  (attempt N/3)`) сохраняется без изменений.

### 3.2. Ветка tester FAIL (`check_tests_passed`, внутри `_handle_qg_failure_rollbacks`)

Текущее (~стр. 454–459):
```python
task_desc = (
    f"Work item: {work_item_id}\nRepo: {repo}\nBranch: {branch}\n"
    f"Stage: development\nNote: Tests FAILED. "
    f"Fix failures described in docs/work-items/{work_item_id}/13-test-report.md"
)
```

Целевое поведение:
- Сформировать путь к `13-test-report.md` аналогично.
- Вызвать `extract_test_failures(path)`.
- В `task_desc` всегда включить `reason` (он уже доступен в этой ветке —
  передаётся в `_handle_qg_failure_rollbacks`).
- Если фрагмент тела непустой — встроить его дословно
  (`Причина: {reason}\nДетали:\n<fragment>`), плюс ссылку на файл как полный
  контекст.
- Если фрагмент пустой — `task_desc` содержит `reason` + ссылку (graceful
  fallback, не хуже текущего поведения). Никаких исключений.
- Префиксная часть и существующий Plane-комментарий тестера
  (`❌ Тесты не прошли: {reason}…`) НЕ меняются.

### 3.3. Инварианты (НЕ менять поведение)

- Последовательность rollback в обеих ветках: `update_task_stage(task_id,
  "development")` → `notify_stage_change` → `plane_notify_stage` →
  (`set_issue_in_progress` для тестера) → проверка `_developer_retry_count` <
  `MAX_DEVELOPER_RETRIES` → `enqueue_job("developer", …)` либо
  `send_telegram` alert. Порядок и условия идентичны.
- `result.rolled_back_to`, `result.enqueued_agent`, `result.enqueued_job_id`,
  `result.alerted` выставляются как сейчас.
- Меняется **только содержимое строки `task_desc`**, передаваемой в
  `enqueue_job`.
- Импорт нового модуля — `from .review_parse import extract_review_findings,
  extract_test_failures` в шапке `stage_engine.py`.

## 4. Изменения API

Нет. Публичные HTTP-эндпоинты (`/health`, `/status`, `/queue`,
`/webhook/plane`, `/webhook/gitea`) не затрагиваются.

## 5. Изменения схемы БД

Нет. Таблицы `tasks`, `agent_runs`, `jobs`, `events` не меняются.
`enqueue_job` вызывается с прежней сигнатурой.

## 6. Требования к новым QG checks

Нет. Реестр `QG_CHECKS` и все `check_*` не трогаются (явно out of scope).

## 7. Артефакты pipeline (создать/обновить в этом PR)

- `src/review_parse.py` — новый модуль.
- `tests/test_review_parse.py` — юнит-тесты парсера (см. 04-test-plan.yaml).
- Возможные дополнения в `tests/test_stage_engine.py` — проверка встраивания
  текста в `task_desc` (rollback-ветки).
- `docs/work-items/ORCH-046/06-adr/ADR-001-*.md` — ADR (правка ядра).
- `docs/architecture/README.md` / `internals.md` — описание нового хелпера и
  поведения заворотов (если reviewer сочтёт необходимым; компонент описать в
  разделе Stage Engine / Откаты).
- `CHANGELOG.md` — запись о ORCH-046.

## 8. Контроль качества / проверка

```bash
python -m pytest tests/ -q      # в контейнере; все тесты зелёные
```

Обязательно: стадия `deploy-staging` (8501) перед прод-деплоем (self-hosting).
