## 1. Признак узнаётся по смыслу

- [x] 1.1 В `internal/controller/worker/worker.go:51` заменить приведение
  `err.(*contract.NoopJobError)` на `errors.As` с целью типа
  `*contract.NoopJobError`; порядок ветвей (счётчик, затем журнал) сохранить.
- [x] 1.2 В `internal/service/transcribe.go:392` заменить приведение
  `err.(*contract.JobNotFoundError)` на `errors.As`.
- [x] 1.3 `golangci-lint run` не даёт замечаний `errorlint` — проверить прогоном.

## 2. Отказ закрытия не теряется

- [x] 2.1 В `.golangci.yml` включить `errcheck.check-blank: true`. Без этого
  `_ = conn.Close()` снимает замечание, и оракул раздела 4 не различает годную
  реализацию от негодной. Прогон обязан остаться на прежних 4 замечаниях — новых
  мест правило не открывает (проверено при разборе дизайна).
- [x] 2.2 В `internal/adapter/recognizer/yandex/speechkit.go:55` собрать отказ
  закрытия `sttConn` с отказом соединения через `errors.Join`; `nil` от закрытия
  форму ошибки не меняет.
- [x] 2.3 В `main.go:124` заменить `defer recognizer.Close()` на `defer` с телом,
  пишущим отказ закрытия в журнал.
- [x] 2.4 Проверить, что между постановкой этого `defer` и штатным завершением
  `main` нет вызова `os.Exit`: иначе запись не появится (риск из `design.md`).
- [x] 2.5 `golangci-lint run` не даёт замечаний `errcheck` — проверить прогоном.
- [x] 2.6 Мутация оракула: временно заменить оба места на `_ = …Close()` —
  `golangci-lint run` обязан покраснеть обоими. Не покраснел — оракул критерия 4
  не работает, и шаг 2.1 сделан неверно. Восстановить код после проверки.

## 3. Оракул на пустой прогон — оба звена пути

- [x] 3.1 Завести первый тест пакета `internal/controller/worker`; оснастку
  (подменный журнал, чтение счётчика из реестра метрик) взять по образцу тестов
  `internal/service` и `internal/controller/http`, а не писать заново.
- [x] 3.2 Тест: воркер, чья работа вернула `*contract.NoopJobError`, **обёрнутый**
  `fmt.Errorf("…: %w", err)`, не пишет в журнал ни одной записи.
- [x] 3.3 Тот же тест: счётчик `transcriber_worker_job_count` для этого воркера
  не изменился — значение читается до и после прогона.
- [x] 3.4 Тест отказа: обычный отказ шага даёт запись в журнал и рост счётчика с
  пометкой отказа — иначе оракул зелен на коде, который не считает отказом ничего.
- [x] 3.5 Тест успеха: прогон без отказа растит счётчик с пометкой успеха и не
  пишет об отказе. Без него реализация, снявшая счёт успешных прогонов, проходит
  все проверки, а доля отказов перестаёт считаться.
- [x] 3.6 **Второе звено пути**: тест в `internal/service` на `findJob` с
  подставным репозиторием, возвращающим `fmt.Errorf("…: %w",
  &contract.JobNotFoundError{…})` — ожидание `*contract.NoopJobError`. Без него
  правка 1.2 принимается только линтером, а линтер проверяет форму, не смысл.
- [x] 3.7 Мутация, поимённо по обеим строкам: вернуть приведение типа в
  `worker.go` — краснеют 3.2 и 3.3; вернуть приведение (или подставить
  несовпадающую цель `errors.As`) в `transcribe.go` — краснеет 3.6. Обе мутации
  обязаны покраснеть по отдельности. Восстановить код после проверки.

## 4. Учёт

- [x] 4.1 `task gate` зелёный целиком; в `CLAUDE.md`, разделе «Гейт», снять
  запись об известном отказе `golangci-lint` и о задаче
  `errors-as-instead-of-typecast`.
- [x] 4.2 В `docs/conventions/errors.md` снять пометку «*Расхождение, и оно
  опасно:*» о приведении типа и строку преамбулы «доменные ошибки проверяются
  приведением типа»; прочие расхождения того файла не трогать.
- [x] 4.3 Дописать `## Purpose` в спеку `pipeline` — что это за capability и что
  сознательно **не** описано (переходы состояний, захват, срок протухания,
  отмена контекста, освобождение ресурсов). Без него следующий автор не отличит
  «остальное не нормировано» от «остального не бывает»; валидатор этого не ловит.
- [x] 4.4 Поправить преамбулу `docs/architecture.md`: заведены две capability,
  `intake` и `pipeline`, и в `pipeline` описан только пустой прогон воркера.
  Маркеры `<!-- канон: поведение → openspec/specs/pipeline -->` на строках про
  идемпотентность и цепочку состояний оставить долгом — но так, чтобы их нельзя
  было прочесть как «уже переехало».
- [x] 4.5 `openspec validate --strict errors-as-instead-of-typecast` проходит.

## Критерии приёмки

Дословно из записи задачи `tasks/items/errors-as-instead-of-typecast.md`:

- Обе проверки идут через `errors.As` либо через `errors.Is` по sentinel.
  Оракул — `golangci-lint run` не даёт замечаний `errorlint`.
- Обёртка `fmt.Errorf("…: %w", err)` в середине пути не ломает распознавание.
  Оракул — тест: обёрнутый `NoopJobError` воркер по-прежнему считает пустым
  прогоном и не пишет ни лога, ни метрики.
- Метрика `transcriber_worker_job_count` на пустом прогоне не растёт. Оракул —
  тот же тест, проверка значения счётчика до и после.
- Отказ `Close` не теряется молча: он либо возвращается вызывающему, либо
  попадает в лог. Оракул — `golangci-lint run` не даёт замечаний `errcheck`, и
  `task gate` зелёный целиком.

Добавлено разбором дизайна (рубрика прохода `rubric`, потолок пунктов не
применялся):

- Каждый оракул способен покраснеть на негодной реализации. Проверяется
  мутацией — шаги 2.6 и 3.7; критерий, чей единственный оракул молчание линтера,
  годится только когда линтер краснеет на **всех** негодных реализациях.
- Признак пустого прогона распознаётся на **обоих** звеньях пути, и каждое звено
  имеет свою падающую проверку.
- Классификация отказа не зависит от текста ошибки: ни подстроки `Error()`, ни
  точной формы значения.
- Норма не закрепляет контрактом то, что конвенции помечают строкой
  «Расхождение», и не объявляет обязательным недостижимое: число записей об
  отказе и уровень журнала остаются долгом `docs/conventions/logging.md`, а
  требование их не нормирует ни в ту, ни в другую сторону. Проверено ревью кода:
  первая редакция требовала «ровно один раз», чему код не соответствовал с
  первого дня.
