- признаки «работы нет» и «задача не найдена» узнаются по смыслу, а не приведением типа: обёртка `%w` на пути больше не превращает пустой прогон воркера в отказ раз в секунду - отказ закрытия соединения с распознавателем доходит до вызывающего (`errors.Join`) либо до журнала; у `errcheck` включён `check-blank`, иначе критерий принимал реализацию, выбрасывающую отказ в пустоту - заведены первые тесты пакета worker и capability `pipeline`; долг из четырёх замечаний линтера закрыт, гейт зелёный целиком
50 lines
4.2 KiB
Markdown
50 lines
4.2 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`, Решение 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), где оно нужно
|
||
автору в момент письма. Второй адрес ссылается на первый и нормой не является —
|
||
разойтись они могут только правкой, сделанной мимо спеки.
|