# pipeline Specification ## Purpose Конвейер расшифровки: как задача движется по состояниям, что делает воркер, когда работы нет, и что считается отказом шага. Описан пока **только пустой прогон воркера** — тот, что нормируют проверки пакета `internal/controller/worker` и перевод признака в `internal/service`. Сознательно не описаны переходы состояний и цепочка `created → converted → transcribe → done | failed`, захват задачи и срок его протухания, отмена контекста посреди шага, освобождение ресурсов внешних клиентов. Это не значит, что такого поведения нет: оно живёт в коде, а требования на него не написаны, потому что требование без проверки — предположение, а не норма. Первая задача, которая трогает любое из перечисленного, дописывает его сюда. ## Requirements ### Requirement: Пустой прогон воркера — не отказ Воркер SHALL отличать «работы в этом состоянии сейчас нет» от отказа шага. На пустом прогоне он MUST не считать прогон отказом: не увеличивать счётчик работы и не писать о нём на уровне владельца сервиса. Признак пустого прогона MUST узнаваться по смыслу значения, а не по его точной форме, и MUST переживать пояснения, добавленные к этому значению на любом промежуточном шаге пути. Требование стоит на инварианте проекта «`NoopJobError` — не ошибка»: три воркера опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт три записи отказа в секунду и столько же засчитанных сбоев, которых не было. Признак пустого прогона MUST рождаться только ответом хранилища на опрос этим же шагом. Слой, придающий отказу собственный смысл, MUST не сохранять чужой признак в цепочке своей ошибки. Воркер узнаёт признак по смыслу на любой глубине, поэтому отказ, к которому признак примешался, тоже зачёл бы пустым прогоном: задача осталась бы в своём состоянии и переопрашивалась раз в секунду без единой записи — ровно то, что запрещает инвариант «Принятая запись не теряется молча». Отказ шага, наоборот, MUST быть виден владельцу сервиса записью в журнале и MUST быть засчитан в счётчик работы с пометкой отказа. **Сколько раз он записывается и каким уровнем — это требование не нормирует, и умолчанием тут считать нечего.** Сегодня один отказ даёт две записи: пишет шаг конвейера и следом воркер, — а уровень стоит `ERROR` там, где конвенция просит `WARN` для повторяющегося сбоя фонового цикла. И то и другое записано долгом в `docs/conventions/logging.md`, раздел «Ошибки», строкой «Расхождение, и оно системное». Долгом оно и остаётся: требование, объявившее одиночную запись нормой, сделало бы недостижимое обязательным, а требование, объявившее нормой двойную, — закрыло бы долг контрактом. Задача, которая возьмётся за этот долг, дописывает норму сюда. #### Scenario: Работы в состоянии нет - **GIVEN** ни одной задачи в опрашиваемом состоянии нет - **WHEN** воркер делает свой прогон - **THEN** на уровне владельца сервиса об этом прогоне не пишется ничего - **AND** счётчик работы воркера не растёт #### Scenario: Признак пустого прогона дошёл с пояснением - **GIVEN** работы в опрашиваемом состоянии нет - **AND** промежуточный шаг добавил к этому признаку своё пояснение - **WHEN** воркер делает свой прогон - **THEN** прогон по-прежнему считается пустым: счётчик не растёт, записи на уровне владельца нет #### Scenario: Шаг отказал - **GIVEN** шаг конвейера вернул отказ - **WHEN** воркер завершает прогон - **THEN** отказ виден владельцу сервиса записью в журнале - **AND** счётчик работы воркера растёт с пометкой отказа #### Scenario: Шаг сделал работу - **GIVEN** шаг конвейера отработал задачу без отказа - **WHEN** воркер завершает прогон - **THEN** счётчик работы воркера растёт с пометкой успеха - **AND** записи об отказе в журнале нет