- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
55 lines
4.4 KiB
Markdown
55 lines
4.4 KiB
Markdown
# ADR-2026-08-11. Отказ, который решено не проверять, объявляется поимённо
|
||
|
||
- **Дата:** 2026-08-11
|
||
- **Источник:** [openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md](../../openspec/changes/archive/2026-08-11-errors-as-instead-of-typecast/design.md), раздел `Decisions`, Решение 2
|
||
|
||
## Решение
|
||
|
||
У `errcheck` включена настройка `check-blank`: присваивание отказа в `_` больше
|
||
не снимает замечание линтера. Место, где отказ решено не проверять, вносится в
|
||
`exclude-functions` поимённо.
|
||
|
||
Дословно из источника:
|
||
|
||
> Правило `errcheck` сегодня молчит на `_ = conn.Close()`: настройка
|
||
> `check-blank` не выставлена, а её умолчание — «пропускать». То есть
|
||
> реализация, выбрасывающая отказ в пустоту, удовлетворяет критерию приёмки
|
||
> «линтер не даёт замечаний `errcheck`», не удовлетворяя самому критерию —
|
||
> «отказ возвращается либо попадает в журнал».
|
||
|
||
## Почему
|
||
|
||
Решение принято не ради строгости, а потому что **оракул не мог упасть**. Задача
|
||
`errors-as-instead-of-typecast` закрывала два непроверенных `Close`, и её
|
||
критерий приёмки опирался на молчание линтера. Ревью дизайна показало, что этому
|
||
критерию удовлетворяет и негодная реализация: `_ = conn.Close()` теряет отказ
|
||
целиком, а линтер молчит. Критерий, который нельзя уронить, не проверяет ничего —
|
||
и вместе с ним в `CLAUDE.md` снималась запись о долге, то есть сигнал исчез бы
|
||
навсегда и без следа.
|
||
|
||
Очевидный путь был другим и отвергнут намеренно:
|
||
|
||
> **Рассмотрено и отвергнуто — дописать оба типа в `exclude-functions`
|
||
> `.golangci.yml`.** Соблазн сильный: список исключений там уже есть, и в нём
|
||
> записана ровно эта политика […] Отвергнуто: политика в конфиге относится к
|
||
> закрытию, у которого **отказ ничего не значит** […] Записав их в исключения,
|
||
> мы бы расширили политику молча, самим фактом добавления строки, и потеряли бы
|
||
> оба сигнала навсегда.
|
||
|
||
Включение проверено прогоном до правки кода: на тогдашнем коде правило не давало
|
||
ни одного нового замечания, то есть включалось чисто и отдельного коммита
|
||
приведения не требовало.
|
||
|
||
## Последствия
|
||
|
||
- `+` критерий «отказ не теряется молча» стал проверяемым машиной: мутация
|
||
(замена обоих мест на `_ = …Close()`) роняет линтер — проверено прогоном.
|
||
- `+` умолчание сместилось в сторону заметности: спрятать отказ по месту больше
|
||
нельзя, отказ от проверки виден в одном файле списком.
|
||
- `−` осознанное игнорирование подорожало: вместо одного символа `_` нужна строка
|
||
в `exclude-functions` с полным именем метода. Для одноразового случая это
|
||
заметная церемония.
|
||
- `−` список исключений будет расти, и каждая его строка — это политика на весь
|
||
проект, а не на одно место. Разрастание списка — сигнал, что правило выбрано
|
||
неверно, и повод пересмотреть эту запись.
|