доменные ошибки сравниваются через errors.As, отказ Close не теряется

- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не
  приведением типа: обёртка `%w` на пути больше не превращает пустой прогон
  воркера в отказ раз в секунду
- отказ закрытия соединения с распознавателем доходит до вызывающего
  (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе
  критерий принимал реализацию, выбрасывающую отказ в пустоту
- заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх
  замечаний линтера закрыт, гейт зелёный целиком
This commit is contained in:
av
2026-08-11 18:04:25 +03:00
parent b7dd060ba0
commit 2559d09fc8
24 changed files with 1554 additions and 48 deletions
+48 -6
View File
@@ -63,15 +63,30 @@
структуру, чьи теги и составляют проверяемый контракт, меняется вместе с ним
и никогда не ловит поломку; такое судят по сырому виду ответа. Признак ищется
мутацией: сломай проверяемое свойство и убедись, что тест краснеет (журнал,
запись 2026-08-11).
запись 2026-08-11);
- **то же и об оракуле критерия приёмки, не только о тесте.** Критерий, чей
единственный оракул — молчание линтера, годится ровно тогда, когда линтер
краснеет на **всех** негодных реализациях; проверяется той же мутацией.
Прецедент: «отказ `Close` не теряется молча» принимался молчанием `errcheck`,
а тот пропускал `_ = conn.Close()` — реализацию, теряющую отказ целиком
(журнал, запись 2026-08-11 про недостижимую норму; закрыто
[решением](adr/ADR-2026-08-11-errcheck-check-blank.md));
- **требование без сценария не имеет оракула** и потому не может быть нарушено
заметно. Норма, которую нечем уронить, расходится с кодом молча — и расходится
тем вернее, чем убедительнее написана (журнал, запись 2026-08-11).
### Типовые ложноположительные
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
логирует его и не считает в метрику. Настоящий дефект рядом другой — проверка
идёт приведением типа и сломается при первой же обёртке; он уже записан в
[conventions/errors.md](conventions/errors.md).
логирует его и не считает в метрику. Норма записана требованием
[pipeline](../openspec/specs/pipeline/spec.md).
**Оговорка, и она тут главная:** ложноположительным считается только само
молчание воркера. Проверка **формы** узнавания ложноположительной не является:
приведение типа на этом месте — настоящий дефект, закрытый 2026-08-11 задачей
`errors-as-instead-of-typecast`. Появилось снова — это регрессия, и выбрасывать
её как известную нельзя.
- **«Захват задачи не в транзакции — гонка двух воркеров».** По построению её
нет: три воркера читают три разных состояния, и одну строку они не делят.
Механика захвата и её слабые места — [database.md](database.md),
@@ -110,8 +125,9 @@
`createTranscribeJob` — сегодня через него идут оба входа
([architecture.md](architecture.md), «Единые точки проекта»).
- `architecture`: не поехало ли поведение в `architecture.md` вместо спеки —
заведена одна capability (`openspec/specs/intake`), поведение прочих узлов
живёт в обзоре под маркерами долга, и соблазн дописать туда ещё максимальный.
заведены две capability (`openspec/specs/intake` и `openspec/specs/pipeline`),
и каждая описана частично. Поведение прочих узлов живёт в обзоре под маркерами
долга, а соблазн дописать туда ещё — самый большой.
- `conventions`: новая колонка правится во всех четырёх местах репозитория
(CLAUDE.md, «Инварианты»).
- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня
@@ -181,6 +197,32 @@ API и имя не откатываются обратной правкой по
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
выдумывать его задним числом нельзя.
## 2026-08-11 — норма требовала от сервиса недостижимого [пойман ревью]
- **Где:** дельта-спека `pipeline` задачи `errors-as-instead-of-typecast`, абзац
об отказе шага
- **Симптом:** требование гласило «отказ MUST быть записан **ровно один раз**
единственной логирующей точкой». Сервис пишет дважды — сначала шаг конвейера,
следом воркер, — то есть норма не выполнялась бы с первого дня, а после
архивации стала бы посылкой для следующих задач
- **Причина:** дефект родился при починке соседнего. Первая редакция назначала
логирующей точкой воркера и фиксировала уровень `ERROR`, чем закрепляла
контрактом долг `conventions/logging.md`. Правка по этой находке ушла в
противоположную крайность: вместо «норма молчит о числе записей» получилось
«норма требует одной». Двойная запись — записанный системный долг, и обе
редакции с ним расходились, только в разные стороны
- **Чем воспроизведён:** прогоном пробы через `go test -overlay`: один отказ
хранилища даёт две записи — `Failed to find and acquire job` из шага и
`Worker error` из воркера
- **Почему не поймали раньше:** требование не имело сценария, а значит и оракула
— упасть ему было нечем. Ревью дизайна абзац читало, но код с ним не сверяло:
кода тогда не существовало. Поймал проход `specs` на ревью кода, направлением
`code → spec`, и поймал прогоном, а не чтением
- **Что меняем:** норма говорит только проверяемое сегодня — отказ виден
владельцу и засчитан в счётчик; число записей и уровень названы долгом с
адресом. В критерии приёмки добавлена строка: норма не объявляет обязательным
недостижимое — ни в ту, ни в другую сторону
## 2026-08-11 — хвост имени отправителя уезжал на открытую страницу метрик [пойман ревью]
- **Где:** `internal/service/transcribe.go`, метки `file_extension` у размера