---
work_item: ORCH-063
stage: analysis
author_agent: analyst
status: ready-for-review
created_at: 2026-06-09
model_used: claude-opus-4-8
---

# 03 — Критерии приёмки (Acceptance Criteria): ORCH-063 — INFRA: мониторинг диска mva154 + алерт при >85%

Work Item: **ORCH-063** · Repo: **orchestrator** · Стадия: analysis

Формат: каждый критерий имеет **PASS** (что должно быть истинно для приёмки) и **FAIL** (что
считается провалом). Reviewer/tester проверяет их буквально по файлам репозитория и тестам.

---

## AC-1 — Heartbeat-демон запускается с приложением

**Условие:** фоновый disk-watchdog периодически измеряет заполнение диска и стартует вместе с
приложением без ручного запуска.
- **PASS:** есть daemon-поток (паттерн `reconciler`/`job_reaper`: `threading.Thread(daemon=True)` +
  `threading.Event`), стартующий в `main.lifespan` (после `reaper.start()`) и останавливающийся на
  shutdown; период замера = `disk_monitor_interval_s`; есть метод `status()`.
- **FAIL:** watchdog не стартует автоматически; блокирующий `time.sleep` без чистого stop; замер
  выполняется в обработчике вебхука/в горячем пути конвейера, а не в отдельном демоне.

---

## AC-2 — Алерт при заполнении ≥ порога

**Условие:** при заполнении отслеживаемого пути ≥ `disk_monitor_threshold_pct` (дефолт 85%) оператор
получает Telegram-алерт с действенными деталями.
- **PASS:** при `used_pct ≥ threshold` вызывается `send_telegram` (notifying, не silent) с
  сообщением, содержащим путь/точку монтирования, занято %, свободно (ГБ или %) и порог.
- **FAIL:** алерт не отправляется при превышении; отправляется silent (`disable_notification=True`)
  и не пингует; сообщение без действенных деталей (нет %/пути/свободно).

---

## AC-3 — Анти-спам: не на каждом тике

**Условие:** при длительном превышении порога алерт не дублируется на каждом тике.
- **PASS:** алерт отправляется при пересечении порога (переход «ниже→на/выше»); пока заполнение
  остаётся выше порога, повторный алерт шлётся не чаще `disk_monitor_realert_s`. Решение об отправке
  выражено чистой функцией от `(current_pct, threshold, previous_state, now)` и покрыто юнит-тестом.
- **FAIL:** алерт шлётся на каждом тике при стабильном превышении; нет cooldown/состояния; логика
  отправки не тестируема (зашита в поток с реальным таймером).

---

## AC-4 — Recovery при возврате ниже порога

**Условие:** при возврате заполнения ниже порога состояние сбрасывается и приходит однократное
сообщение восстановления.
- **PASS:** переход «выше→ниже порога» сбрасывает состояние алерта и отправляет ровно одно
  recovery-сообщение «диск ниже порога»; последующее новое превышение снова алертит (цикл повторяем).
- **FAIL:** после спада ниже порога состояние не сбрасывается (новое превышение молчит из-за
  «залипшего» cooldown); recovery шлётся повторно на каждом тике ниже порога.

---

## AC-5 — Конфигурируемость и kill-switch

**Условие:** порог, период, период повтора, пути и включение конфигурируемы; выключение даёт нулевую
регрессию.
- **PASS:** в `config.py` есть `disk_monitor_enabled` / `disk_monitor_interval_s` /
  `disk_monitor_threshold_pct` / `disk_monitor_realert_s` / `disk_monitor_paths` (с env-маппингом);
  при `disk_monitor_enabled=False` демон не стартует, `/queue`-блок отдаёт `{"enabled": false}`,
  поведение приложения идентично текущему. Новые env задокументированы в `.env.example`.
- **FAIL:** значения захардкожены; нет kill-switch; при выключении меняется поведение конвейера;
  env не задокументированы.

---

## AC-6 — never-raise (изоляция от конвейера)

**Условие:** любой сбой watchdog не роняет и не блокирует конвейер.
- **PASS:** замер по несуществующему/недоступному пути, ошибка `send_telegram`, исключение в тике —
  логируются и **не** пробрасываются; демон продолжает работу; конвейер и enduro-trails не
  затронуты. Покрыто тестом (замер по битому пути / исключение в отправке → тик не падает).
- **FAIL:** исключение в тике останавливает поток или всплывает в приложение; недоступный путь
  роняет замер всех путей.

---

## AC-7 — Наблюдаемость в `GET /queue`

**Условие:** состояние watchdog видно в `GET /queue`.
- **PASS:** ответ `GET /queue` содержит аддитивный блок `disk_monitor` с `enabled`, `threshold_pct`,
  `interval_s`, `paths` (последний замер: `used_pct`, свободно), `alerting`, `last_alert_at`;
  существующие ключи ответа не изменены; блок never-raise (при ошибке — минимальный словарь).
- **FAIL:** блока нет; изменены/сломаны существующие ключи `/queue`; блок может выбросить исключение.

---

## AC-8 — Корректный источник замера (хост-ФС)

**Условие:** замер отражает заполнение хост-раздела, а не overlay-ФС контейнера.
- **PASS:** дефолтный набор путей — смонтированные хост-пути (`/repos`, `/app/data`); замер по ним
  репрезентативен для заполняющегося хост-раздела. Источником истины **не** является `shutil.disk_usage("/")`
  (overlay контейнера).
- **FAIL:** мониторится только `/` контейнера → ложно-низкое заполнение при реально полном хосте
  (риск повтора инцидента 07.06).

---

## AC-9 — Документация обновлена (golden source)

**Условие:** документация обновлена в том же PR (CLAUDE.md §2; reviewer-ось).
- **PASS:** обновлены `docs/architecture/README.md` (компонент + блок `/queue`),
  `docs/operations/INFRA.md` (что мониторится, порог, как отключить, реакция на алерт),
  `.env.example` (новые `ORCH_DISK_*`), `CHANGELOG.md` (`feat:`); создан
  `docs/work-items/ORCH-063/06-adr/ADR-001-*.md`.
- **FAIL:** функционал добавлен, но обзорные/операционные доки или ADR не обновлены.

---

## Сводная матрица AC ↔ FR/BR

| AC | Покрывает |
|----|-----------|
| AC-1 | BR-1 / BR-8 / FR-1 |
| AC-2 | BR-2 / FR-2 / FR-3 |
| AC-3 | BR-3 / FR-4 |
| AC-4 | BR-4 / FR-4 |
| AC-5 | BR-5 / FR-5 / NFR-4 |
| AC-6 | BR-6 / NFR-1 |
| AC-7 | BR-7 / FR-6 |
| AC-8 | NFR-3 / FR-2 |
| AC-9 | CLAUDE.md §2 (документация = golden source) |
