доменные ошибки сравниваются через errors.As, отказ Close не теряется
- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# 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`, Решение 3
|
||||
|
||||
## Решение
|
||||
|
||||
Признак «работы нет» узнаётся через `errors.As`, то есть на любой глубине цепочки
|
||||
ошибки. Встречный риск — отказ, к которому признак примешался по дороге, — закрыт
|
||||
**требованием спеки**, а не проверкой в коде воркера.
|
||||
|
||||
Дословно из источника:
|
||||
|
||||
> Граница ставится **нормой, а не кодом**: спека требует, чтобы признак рождался
|
||||
> только ответом хранилища на опрос этим же шагом, и запрещает слою сохранять
|
||||
> чужой признак в цепочке своей ошибки.
|
||||
|
||||
## Почему
|
||||
|
||||
Приведение типа видело только вершину цепочки — потому и ломалось от первой же
|
||||
обёртки. `errors.As` эту проблему устраняет, но устраняет симметрично: признак
|
||||
теперь виден и там, где его никто не клал осознанно. Отказ, к которому признак
|
||||
примешался обёрткой или `errors.Join`, воркер зачёл бы пустым прогоном — задача
|
||||
осталась бы в своём состоянии и переопрашивалась раз в секунду без единой записи.
|
||||
Это тот же класс, от которого защищает инвариант «Принятая запись не теряется
|
||||
молча», только с обратным знаком относительно чинимого дефекта.
|
||||
|
||||
Кодовый вариант рассмотрен и отвергнут по цене:
|
||||
|
||||
> **Рассмотрено и отвергнуто — научить воркер различать «признак на вершине» от
|
||||
> «признака в глубине».** Отвергнуто по цене: `errors.As` такого различения не
|
||||
> даёт вовсе, пришлось бы либо проверять вершину вручную (то есть вернуть
|
||||
> приведение типа, которое чинится), либо заводить свой обход цепочки. Код
|
||||
> усложняется ради случая, которого сегодня нет ни одного, а защита от него нужна
|
||||
> на входе — при написании нового слоя, — где норма работает, а проверка в
|
||||
> рантайме опоздала бы.
|
||||
|
||||
## Последствия
|
||||
|
||||
- `+` код остался коротким: одна проверка вместо разбора цепочки вручную.
|
||||
- `+` защита стоит там, где ошибку совершают, — за письменным столом автора
|
||||
нового слоя, а не в рантайме, где она уже случилась.
|
||||
- `−` норма не механизирована: её нарушение поймает только ревью или чтение.
|
||||
Единственный `MUST` требования `pipeline` без машинного оракула — этот.
|
||||
- `−` правило живёт в двух документах: требованием в
|
||||
[openspec/specs/pipeline/spec.md](../../openspec/specs/pipeline/spec.md) и
|
||||
прозой в [conventions/errors.md](../conventions/errors.md), где оно нужно
|
||||
автору в момент письма. Второй адрес ссылается на первый и нормой не является —
|
||||
разойтись они могут только правкой, сделанной мимо спеки.
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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` с полным именем метода. Для одноразового случая это
|
||||
заметная церемония.
|
||||
- `−` список исключений будет расти, и каждая его строка — это политика на весь
|
||||
проект, а не на одно место. Разрастание списка — сигнал, что правило выбрано
|
||||
неверно, и повод пересмотреть эту запись.
|
||||
@@ -32,6 +32,8 @@
|
||||
|
||||
| Дата | Запись | Статус |
|
||||
| --- | --- | --- |
|
||||
| 2026-08-11 | [Границу распознавания доменного признака держит норма, а не код](ADR-2026-08-11-domain-marker-boundary-by-norm.md) | |
|
||||
| 2026-08-11 | [Отказ, который решено не проверять, объявляется поимённо](ADR-2026-08-11-errcheck-check-blank.md) | |
|
||||
| 2026-08-11 | [Наружу расширение выходит только приведённым к перечню](ADR-2026-08-11-known-format-label.md) | |
|
||||
| 2026-08-11 | [Приложение пишем на Vue, а Node входит в гейт и в образ](ADR-2026-08-11-spa-on-vue.md) | |
|
||||
| 2026-08-11 | [Очередь остаётся своей таблицей, но коллекцией PocketBase](ADR-2026-08-11-queue-as-pocketbase-collection.md) | |
|
||||
|
||||
Reference in New Issue
Block a user