docs: autotests.md стал переносимым документом об автопроверках
- лестница механизации из пяти ступеней, два круга проверок, перечень правил таблицами по родам, перечень подавлений с причинами, порядок заведения правила; названы остатки правил и отклонённые подъёмы - утверждения «Механизировано» в четырёх записях конвенций и единые точки в architecture.md приведены к сегодняшнему состоянию; изъятие «транспорт знает адаптер хранилища» названо строкой - в журнал дефектов записаны две находки: узнавание конца потока по тексту и правило гейта, обходимое одной лишней строкой
This commit is contained in:
+11
-4
@@ -56,7 +56,13 @@
|
||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||
`internal/contract`; ffmpeg, Yandex, Telegram и хранилище подставляются в
|
||||
`main.go`.
|
||||
`main.go`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||
они же держат обратные направления: транспорты не знают друг о друге, адаптер
|
||||
не знает ни ядра, ни транспортов.
|
||||
*Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http`
|
||||
импортирует `adapter/repo/pocketbase`, потому что HTTP-поверхность и есть
|
||||
роутер этого хранилища, а не наш сервер поверх него. Правила на это
|
||||
направление нет намеренно.
|
||||
|
||||
## Компоненты
|
||||
|
||||
@@ -136,13 +142,14 @@
|
||||
| Переход задачи в состояние | `entity.TranscribeJob.MoveToState` — чистит служебные поля прошлого состояния |
|
||||
| Завершение и отказ | `TranscribeService.completeJob` и `failJob` — они же отвечают пользователю |
|
||||
| Разбор конфигурации | `internal/config.LoadConfig` |
|
||||
| Чтение времени | `internal/clock` — `Now` даёт метку в UTC, `Start` — начало измерения длительности; `time.Now` вне пакета запрещён правилом линтера |
|
||||
| Метрики | `internal/metrics`, префикс имени `transcriber_` |
|
||||
| Значения метки формата | `internal/metrics.FormatLabel` — приводит расширение к закрытому перечню, прочее заменяет на `other`; нормирует спека `intake` |
|
||||
|
||||
Единых точек, которых **нет** и которые ожидались бы: идентификаторы
|
||||
генерируются вызовом `uuid.NewString()` по месту, время — вызовом `time.Now()`
|
||||
по месту, отображения доменной ошибки в код HTTP-ответа нет — обработчик решает
|
||||
сам.
|
||||
генерируются вызовом `uuid.NewString()` по месту, отображения доменной ошибки в
|
||||
код HTTP-ответа нет — обработчик решает сам. Время из этого перечня ушло
|
||||
2026-08-13: его читает `internal/clock`, и запрет держит линтер.
|
||||
|
||||
## Деплой
|
||||
|
||||
|
||||
+176
-20
@@ -1,8 +1,15 @@
|
||||
# Автопроверки
|
||||
|
||||
Чем машина судит код: перечень свойств, доведённых до проверки, и место, где
|
||||
каждое настроено. Документ — дом темы ревью `autotests`: проход, которому эта
|
||||
тема досталась, читает его, а не перечисляет инструменты по памяти.
|
||||
Чем машина судит код этого проекта: какие свойства доведены до проверки, чем
|
||||
каждое проверяется, когда оно запускается и что остаётся человеку. Документ —
|
||||
дом темы ревью `autotests`: проход, которому эта тема досталась, читает его, а не
|
||||
перечисляет инструменты по памяти.
|
||||
|
||||
Документ **переносимый**: устройство ниже — не особенность transcriber, а способ
|
||||
вести автопроверки в Go-проекте, и разделы «Лестница механизации», «Два круга» и
|
||||
«Как заводят новое правило» переносятся в другой проект как есть. Своё здесь —
|
||||
перечень правил и подавлений; он назван так, чтобы отличать переносимое от
|
||||
местного.
|
||||
|
||||
Тема заведена 2026-08-13. Прежде перечень лежал разделом «Механизировано» в
|
||||
[conventions/README.md](conventions/README.md), и это был чужой дом: конвенции
|
||||
@@ -25,40 +32,189 @@
|
||||
- **поведение сервиса** — нормативные спеки `openspec/specs/`. У шага сверки
|
||||
версий Go поведение нормировано отдельно, спекой
|
||||
[toolchain](../openspec/specs/toolchain/spec.md): это единственная проверка
|
||||
проекта, у которой есть своя capability.
|
||||
проекта, у которой есть своя capability, и потому единственная, чьи сценарии
|
||||
проверяются построчно (`scripts/check_go_version_test.go`).
|
||||
|
||||
Домов настройки четыре: `.golangci.yml` — линтеры и форматтер, `lefthook.yml` —
|
||||
проверки на pre-commit, `Taskfile.yml` — шаги гейта и их обёртки, `scripts/` —
|
||||
единственный собственный скрипт проверки. Скрипты `docs.py`, `tasks.py` и
|
||||
`openspec.py` живут вне репозитория, в плагинах, и Taskfile знает их путями.
|
||||
## Лестница механизации
|
||||
|
||||
Свойство поднимается по ступеням, и ступень выбирают не по вкусу, а по тому,
|
||||
чем свойство выражается. Верхняя ступень дешевле нижней в эксплуатации и дороже
|
||||
в заведении, поэтому прыгать через ступень без нужды не надо.
|
||||
|
||||
1. **Проза конвенции.** Свойство названо словами, проверяет человек на каждом
|
||||
ревью заново. Это ступень по умолчанию и худшая из всех: она стоит внимания
|
||||
каждого прогона и молча перестаёт работать, когда внимание кончилось.
|
||||
2. **Настройка готового линтера.** Свойство совпало с чужим правилом —
|
||||
включается строкой в `.golangci.yml`. Дешевле всего; ограничение в том, что
|
||||
правило чужое и говорит о том, о чём его написали.
|
||||
3. **Запрет по имени** (`forbidigo`, `depguard`). Свойство выражается через «эту
|
||||
функцию/пакет тут звать нельзя». Дешёво и точно, но требует **единой точки**,
|
||||
куда запрещённое переносят: запрет без дома оставляет код без способа сделать
|
||||
нужное.
|
||||
4. **Тест-сканер исходников** (`internal/archrules`). Свойство — о структуре, а
|
||||
не о вызове: направление зависимостей, согласованность двух перечней,
|
||||
отсутствие идиомы. Пишется руками на `go/parser` или регулярном выражении,
|
||||
зато читается как тест и ломается заметно.
|
||||
5. **Свой шаг проверки** (`scripts/`, шаги `Taskfile.yml`). Свойство выходит за
|
||||
пределы кода на Go: версия инструмента, форма `Dockerfile`, раскладка
|
||||
документов. Дороже всех — у шага появляется своя норма и свои тесты.
|
||||
|
||||
Ступень, выбранная неверно, видна сразу. Запрет по имени, обходимый одной
|
||||
лишней строкой, — это ступень 4, наряженная третьей: так было с правилом о
|
||||
заголовках ответа, которое сначала запретило текст `\.Header\(\)\.Get`, а
|
||||
обходилось присваиванием в переменную. Правило переписано на суждение **по типу
|
||||
приёмника** (`analyze-types`), и это уже настоящая третья ступень.
|
||||
|
||||
## Два круга: pre-commit и гейт
|
||||
|
||||
Проверки идут двумя кругами, и круг выбирается по цене прогона.
|
||||
|
||||
| | pre-commit (`lefthook.yml`) | гейт (`task gate`) |
|
||||
| --- | --- | --- |
|
||||
| Когда | на каждый коммит | перед тем как считать задачу сделанной |
|
||||
| На чём | на **затронутых файлах** | на всём дереве |
|
||||
| Сколько идёт | около секунды | десятки секунд |
|
||||
| Что делает с находкой | `gofmt` правит и добавляет в коммит, прочее роняет коммит | роняет прогон |
|
||||
|
||||
Перечень работ pre-commit и то, что остаётся только гейту, — в
|
||||
[CLAUDE.md](../CLAUDE.md), раздел «Гейт». Здесь важен принцип: **pre-commit не
|
||||
подменяет гейт**. Он ловит дешёвое и местное, а сборка, тесты целиком, сверки
|
||||
документов и запрос к базе уязвимостей идут в гейте — иначе коммит стоил бы
|
||||
минуту, и хук отключили бы через день.
|
||||
|
||||
Полный набор проверок в pre-commit не переносится сознательно; обратное решение
|
||||
— «гонять всё на каждый коммит» — известно и отклонено по этой же причине.
|
||||
|
||||
## Механизировано
|
||||
|
||||
Проверяется командами из [CLAUDE.md](../CLAUDE.md); прозой не дублируется и в
|
||||
промптах ревью не пересказывается.
|
||||
|
||||
### Ошибки и отказы
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Сравнение ошибок через `errors.Is` и `errors.As`, не `==` и не приведением типа | `.golangci.yml` → `errorlint` |
|
||||
| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close` и `send` |
|
||||
| Ошибка не узнаётся сравнением текста сообщения (`strings.Contains(err.Error(), …)`, `err.Error() == …`) | `internal/archrules` → `TestОшибкаНеУзнаётсяПоТексту` |
|
||||
| Непроверенное возвращаемое значение ошибки | `.golangci.yml` → `errcheck`, включая присваивание в `_` (`check-blank`). Отказ, который решено не проверять, объявляют в `exclude-functions` поимённо — там сегодня `defer Close`, `os.Remove` и `send` |
|
||||
| Ошибки — только stdlib, без сторонних пакетов | `.golangci.yml` → `depguard` |
|
||||
|
||||
### Структура и границы
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Ядро (`internal/service`) не знает ни адаптеров, ни транспортов | `internal/archrules` → `TestЯдроНеЗнаетОбАдаптерах`, `TestЯдроНеЗнаетОТранспортах` |
|
||||
| Транспорты (`controller/http`, `controller/tg`, `controller/worker`) не знают друг о друге | `internal/archrules` → `TestТранспортыНеЗнаютДругОДруге` |
|
||||
| Адаптер не знает ни ядра, ни транспортов | `internal/archrules` → `TestАдаптерыНеЗнаютНиЯдра_НиТранспортов` |
|
||||
| Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` → четыре правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда |
|
||||
|
||||
### Время, вывод, конфигурация
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Время читают `clock.Now` (метка, UTC) и `clock.Start` (длительность, монотонные часы) — не `time.Now` по месту | `.golangci.yml` → `forbidigo`; единая точка — `internal/clock` |
|
||||
| Вывод идёт через `slog`, а не `fmt.Print*` и не встроенными `print`/`println` | `.golangci.yml` → `forbidigo`. Не ловит `fmt.Fprintln(os.Stdout, …)` — первый аргумент по имени функции не судится; остаток прозой в [conventions/logging.md](conventions/logging.md) |
|
||||
| Конфигурация приезжает из TOML, а не из окружения | `.golangci.yml` → `forbidigo`: `os.Getenv`, `os.LookupEnv`, `os.Environ`, `os.ExpandEnv` — все четыре, иначе запрет обходится соседним именем |
|
||||
| Форма вызова `slog`: только пары «ключ-значение», атрибуты (`slog.String` и прочие) не употребляются вовсе; `msg` — константа | `.golangci.yml` → `sloglint` (`kv-only` запрещает атрибуты целиком, а не только смешение) |
|
||||
|
||||
### Проверки о самих проверках
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml` → `forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` |
|
||||
| Форматирование исходников | `.golangci.yml` → `gofmt` |
|
||||
| Каждый сценарий нормы шага сверки версий проверен мутацией, а не памятью | `scripts/check_go_version_test.go` — 20 сценариев спеки `toolchain` плюс два свойства самого шага: исход не зависит от установленного `go`, и шаг не зовёт ни `go`, ни `docker`, ни сеть |
|
||||
|
||||
### Форма кода и файлов вне Go
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Форматирование исходников | `.golangci.yml` → `gofmt`; на pre-commit правится на месте |
|
||||
| Подозрительные конструкции языка | `.golangci.yml` → `govet`, `staticcheck`, `ineffassign`, `unused` |
|
||||
| Опечатка в комментарии и в тексте ошибки | `.golangci.yml` → `misspell` |
|
||||
| Скрипты оболочки | `Taskfile.yml` → шаг `shell` (`shellcheck`), он же на pre-commit |
|
||||
| Форма `Dockerfile` | `Taskfile.yml` → шаг `dockerfile` (`hadolint`), он же на pre-commit |
|
||||
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
||||
|
||||
### Хранилище, документы, секреты, зависимости
|
||||
|
||||
| Правило | Где механизировано |
|
||||
| --- | --- |
|
||||
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` |
|
||||
| Согласованность каталога задач, форма `openspec/config.yaml` | `tasks.py check`, `openspec.py check` |
|
||||
| Секреты в коммите | `lefthook.yml` → `gitleaks git --staged` |
|
||||
| Достижимая из кода уязвимость в зависимостях | `Taskfile.yml` → шаг `vulns` (`govulncheck ./...`) |
|
||||
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` |
|
||||
| Одно число версии Go в `go.mod`, `Dockerfile`, `CLAUDE.md` и `README.md` | `Taskfile.yml` → шаг `go-version` (`scripts/check-go-version.sh`) |
|
||||
|
||||
Не названное здесь место механизации означает, что проход по конвенциям будет
|
||||
добросовестно проверять уже проверенное.
|
||||
|
||||
## Подавления: что и почему
|
||||
|
||||
Подавление — это решение, а не настройка, поэтому каждое названо поимённо и с
|
||||
причиной. Причина живёт строкой рядом с подавлением (в `.golangci.yml` или
|
||||
`Taskfile.yml`), а здесь — их перечень, чтобы видеть все разом.
|
||||
|
||||
| Подавлено | Где | Почему |
|
||||
| --- | --- | --- |
|
||||
| `errcheck` на `defer Close`, `os.Remove` и `send` | `.golangci.yml`, `exclude-functions` | Отказ, который решено не проверять, объявляют поимённо — так он заметен |
|
||||
| Правило о заголовках вне `*_test.go` | `.golangci.yml`, `exclusions` | В рабочем коде `Header()` и есть способ отдать заголовок |
|
||||
| `time.Now` внутри `internal/clock` | там же | Единой точке чтения времени нечем читать время иначе |
|
||||
| Чтение времени и окружения в `*_test.go` | там же | Проверка строит вход прогона — фикстуру времени, `PATH`, окружение дочернего процесса, — а не метку домена и не настройки приложения. Исключение объявлено по тексту сообщения: правило называет четыре имени, и исключение обязано покрывать те же четыре |
|
||||
| `SC1007` в `scripts/check-go-version.sh` | директива в скрипте | Ложное срабатывание на идиому `CDPATH= cd`, которая защищает `cd` от чужого `CDPATH` |
|
||||
| `DL3007` (`alpine:latest`) | `Taskfile.yml`, шаг `dockerfile` | Открытая задача `pin-runtime-image-base`; до её решения шаг краснел бы на известном |
|
||||
| `DL3018` (закрепить версии `apk`) | там же | Alpine не держит старые версии пакетов в репозитории: закрепление ломает сборку через недели |
|
||||
|
||||
## Что остаётся прозой
|
||||
|
||||
**Из перечисленного в записях конвенций правилом выражено одно** — сравнение
|
||||
ошибок через `errors.Is` и `errors.As` (`errorlint`, строка таблицы выше). Прозой
|
||||
остаётся всё прочее: ни константный `msg` лога (`sloglint`), ни запрет
|
||||
`fmt.Print*` и `os.Getenv` (`forbidigo` заведён, но правило у него одно — о том,
|
||||
чем судят ответ в проверках; этих двух запретов в нём нет), ни запрет сторонних
|
||||
пакетов ошибок (`depguard`), ни архитектурные тесты-сканеры. Это следующий шаг
|
||||
переноса в правило: свойство, оставшееся прозой, проверяет человек на каждом
|
||||
ревью заново.
|
||||
**Из перечисленного в записях конвенций правилом выражено не всё.** Прозой
|
||||
остаётся то, чему нет ни готового правила, ни детерминированного оракула:
|
||||
уровень лога по адресату, единая логирующая точка на доменной границе, словарь
|
||||
имён полей, канонический вид идентификатора, естественные ключи у деталей.
|
||||
Свойство, оставшееся прозой, проверяет человек на каждом ревью заново — это и
|
||||
есть первая ступень лестницы, и подъём с неё всегда выигрыш.
|
||||
|
||||
Названы поимённо и **остатки правил** — то, что правило не ловит и потому
|
||||
осталось человеку:
|
||||
|
||||
- вывод в stdout через `fmt.Fprintln(os.Stdout, …)` и `os.Stdout.WriteString`:
|
||||
`forbidigo` судит по имени вызванной функции, а не по её первому аргументу;
|
||||
- проверка, судящая ответ мимо recorder — через свой `http.ResponseWriter`;
|
||||
- направление «транспорт не знает адаптера»: сегодня оно нарушено осознанно —
|
||||
`controller/http` импортирует адаптер хранилища, потому что HTTP-поверхность и
|
||||
есть роутер этого хранилища. Изъятие названо в
|
||||
[architecture.md](architecture.md), «Принципы», и правила на это направление
|
||||
нет.
|
||||
|
||||
Отдельно названы **правила, чей подъём отклонён**:
|
||||
|
||||
- `key-naming-case` у `sloglint` — словарь полей намеренно смешанный: доменные
|
||||
поля `snake_case`, системные домены с точкой (`http.method`, `ext.service`);
|
||||
- `msg-style: lowercased` у `sloglint` — это ровно конвенция «`msg` — короткая
|
||||
константа в нижнем регистре», но код называет сообщения предложениями с
|
||||
заглавной, и это объявленное *Расхождение*. Цена подъёма — переписать больше
|
||||
ста вызовов, и она не заплачена;
|
||||
- закрепление версий пакетов `apk` (`DL3018`) — см. подавления выше.
|
||||
|
||||
**Кандидат, ждущий решения:** `clock.Now` и `clock.Start` отдают один тип, поэтому
|
||||
`time.Since(clock.Now())` компилируется и молча меряет длительность настенными
|
||||
часами — ровно то, против чего пакет и написан. Держал бы это компилятор, будь у
|
||||
`Start` свой тип с методом `Elapsed()`. Сегодня таких мест нет.
|
||||
|
||||
## Как заводят новое правило
|
||||
|
||||
Порядок один и тот же, и последние два шага пропускать нельзя.
|
||||
|
||||
1. **Найти дом.** Ступень лестницы выбирается по тому, чем свойство
|
||||
выражается, а не по тому, что проще включить.
|
||||
2. **Написать причину рядом.** Правило без причины снимают при первом же
|
||||
неудобстве: тот, кто снимает, не знает, что оно ловило.
|
||||
3. **Починить находки, а не подавить.** Подавление годится, когда правило
|
||||
говорит не о том, что мы имели в виду; тогда оно попадает в перечень выше с
|
||||
причиной. Подавление «пока некогда» — это отложенная работа, и её место в
|
||||
каталоге задач, а не в конфиге.
|
||||
4. **Проверить мутацией.** Внести ровно то нарушение, против которого правило
|
||||
написано, и убедиться, что проверка краснеет и называет место. Правило,
|
||||
принятое молчанием инструмента, — это не правило: прецеденты есть, и записаны
|
||||
они в [review.md](review.md) (журнал 2026-08-11 про недостижимую норму,
|
||||
2026-08-13 про обходимый текстовый запрет).
|
||||
5. **Записать строкой здесь** и удалить прозу из конвенции, если правило её
|
||||
заменило.
|
||||
|
||||
@@ -18,12 +18,13 @@ severity — в [CLAUDE.md](../../CLAUDE.md).
|
||||
|
||||
Четыре записи перенесены из проекта jellybit — тот же Go, тот же автор, те же
|
||||
задачи. Код transcriber написан раньше и **части правил не следует**: ключи —
|
||||
UUID вместо ULID, время берётся `time.Now()` по месту, лог пишется на каждом
|
||||
шаге и дублируется воркером.
|
||||
UUID вместо ULID, лог пишется на каждом шаге и дублируется воркером, `msg` —
|
||||
предложение с заглавной буквы вместо константной категории.
|
||||
|
||||
Из этого перечня одно уже закрыто: доменные ошибки проверялись приведением типа
|
||||
до 2026-08-11, задача `errors-as-instead-of-typecast`. Приведение типа на этом
|
||||
месте больше не долг, а регрессия.
|
||||
Из этого перечня закрыты два. Доменные ошибки проверялись приведением типа до
|
||||
2026-08-11, задача `errors-as-instead-of-typecast`. Время брали `time.Now()` по
|
||||
месту до 2026-08-13 — теперь его читает единая точка `internal/clock`, и правило
|
||||
держит линтер. Оба места больше не долг, а регрессия.
|
||||
|
||||
Пятая, `web-ui.md`, тоже пришла оттуда, но не прижилась: jellybit работает на
|
||||
htmx, а здесь решено делать SPA — и перенесённый текст снят целиком.
|
||||
@@ -52,8 +53,9 @@ htmx, а здесь решено делать SPA — и перенесённы
|
||||
|
||||
Перечень правил, доведённых до проверки, и место настройки каждого — в
|
||||
[../autotests.md](../autotests.md). Там же сказано, что из перечисленного в
|
||||
записях осталось прозой и потому проверяется человеком на каждом ревью заново:
|
||||
сегодня правилом выражено ровно одно свойство из всех записей.
|
||||
записях осталось прозой и потому проверяется человеком на каждом ревью заново, и
|
||||
там же названы остатки правил — то, что правило не ловит. Числа механизированного
|
||||
здесь нет намеренно: оно протухает при каждом новом правиле.
|
||||
|
||||
Здесь этот перечень не повторяется. Дом у него один, и он не тут: конвенции
|
||||
говорят, как писать код, а не какими инструментами его читают.
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
комментариями снабжена половина полей; валидации на старте нет вовсе, кроме
|
||||
проверки пустых ключей внутри адаптеров.
|
||||
|
||||
**Механизировано:** ничего. Запрет `os.Getenv` для конфигурации правилом линтера
|
||||
не выражен, и `godotenv` в `main.go` загружает `.env` — то есть окружение сейчас
|
||||
участвует.
|
||||
**Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml`
|
||||
([../autotests.md](../autotests.md), «Механизировано»). Он держит правило «настройки
|
||||
приезжают из TOML»; `godotenv` в `main.go` по-прежнему загружает `.env`, но кладёт
|
||||
его в окружение процесса, а не в настройки приложения.
|
||||
|
||||
## Принципы
|
||||
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
Как мы устраиваем таблицы и ключи. Актуальная схема — [../database.md](../database.md).
|
||||
|
||||
**Взято из проекта jellybit целиком.** Сегодняшний код transcriber этому не
|
||||
следует ни в одном пункте: ключи — UUID v4, а не ULID; время — `time.Now()` по
|
||||
месту вызова в локальной зоне, а не единой точкой в UTC; единой точки генерации
|
||||
и разбора нет. Правила действуют на новый код; переписывание существующего —
|
||||
**Взято из проекта jellybit целиком.** Сегодняшний код transcriber следует
|
||||
этому частью: ключи — UUID v4, а не ULID, и единой точки их генерации нет. Время
|
||||
единой точкой читается с 2026-08-13 — `internal/clock`, метка в UTC, — и правило
|
||||
держит линтер. Правила действуют на новый код; переписывание существующего —
|
||||
отдельная работа, и до неё расхождение читается как долг, а не как нарушение.
|
||||
|
||||
**Механизировано:** одно — сверка изменённого шага схемы с
|
||||
[../database.md](../database.md), шаг гейта `docs.py check`
|
||||
([../autotests.md](../autotests.md), «Механизировано»). Под прочие пункты ни правила
|
||||
линтера, ни теста-сканера в transcriber нет.
|
||||
**Механизировано:** сверка изменённого шага схемы с
|
||||
[../database.md](../database.md) (`docs.py check`), чтение времени единой точкой
|
||||
(`forbidigo` плюс `internal/clock`) и согласованность колонок очереди
|
||||
(тест-сканер `internal/archrules`). Прочие пункты — прозой; адреса —
|
||||
[../autotests.md](../autotests.md), «Механизировано».
|
||||
|
||||
## Первичные ключи — ULID, не автоинкремент
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@
|
||||
Главное: единой точки отображения доменной ошибки в ответ нет, обработчики
|
||||
решают сами.
|
||||
|
||||
**Механизировано:** приведение типа и `err == ErrX` ловит `errorlint` в
|
||||
`.golangci.yml`. Запрета сторонних пакетов ошибок (`depguard`) нет — сторонних
|
||||
пакетов ошибок в проекте и так нет.
|
||||
**Механизировано:** приведение типа и `err == ErrX` ловит `errorlint`,
|
||||
сторонние пакеты ошибок — `depguard`, узнавание ошибки по тексту сообщения —
|
||||
тест-сканер `internal/archrules`. Перечень и адреса —
|
||||
[../autotests.md](../autotests.md), «Механизировано».
|
||||
|
||||
## Базовая идиома: stdlib
|
||||
|
||||
|
||||
@@ -11,10 +11,13 @@ OpenSpec.
|
||||
категория; шаг конвейера логирует и себя, и свой исход, и при этом возвращает
|
||||
ошибку выше, где её логируют снова.
|
||||
|
||||
**Механизировано:** ничего из перечисленного ниже. `forbidigo` в `.golangci.yml`
|
||||
включён, но правило у него одно и о другом — чем судят ответ в проверках
|
||||
([../autotests.md](../autotests.md), «Механизировано»); `sloglint` не заведён, и ни один
|
||||
пункт этой записи правилом не выражен.
|
||||
**Механизировано:** форма вызова — `sloglint`: только пары
|
||||
«ключ-значение», `msg` константой, **атрибуты (`slog.String` и прочие) не
|
||||
употребляются вовсе**. Запрет `fmt.Print*` и встроенных `print`/`println` —
|
||||
`forbidigo`; вывод в stdout через `fmt.Fprintln(os.Stdout, …)` правилом не
|
||||
ловится и остаётся прозой этой записи. Прозой остаются также уровень по адресату,
|
||||
единая логирующая точка и словарь имён полей: оракула у них нет. Адреса —
|
||||
[../autotests.md](../autotests.md), «Механизировано».
|
||||
|
||||
## Принципы
|
||||
|
||||
|
||||
@@ -238,6 +238,47 @@ API и имя не откатываются обратной правкой по
|
||||
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
|
||||
выдумывать его задним числом нельзя.
|
||||
|
||||
## 2026-08-13 — конец потока распознавания узнавался по тексту сообщения [пойман сканером]
|
||||
|
||||
- **Где:** `internal/adapter/recognizer/yandex/speechkit.go`, чтение потока
|
||||
результата распознавания
|
||||
- **Симптом:** сегодня не наблюдался — путь рабочий, пока библиотека отдаёт конец
|
||||
потока значением `io.EOF`. Отказ с текстом «EOF» был бы принят за конец потока,
|
||||
и расшифровка вернулась бы усечённой: пользователь получил бы половину записи
|
||||
как готовый результат
|
||||
- **Причина:** конец потока узнавался сравнением `err.Error() == "EOF"`. Текст
|
||||
сообщения — не признак: его носит и чужая ошибка, а сменит его библиотека —
|
||||
условие перестанет срабатывать вовсе, и оба исхода молчаливы
|
||||
- **Чем воспроизведён:** не воспроизводился на живом сервисе — прогон на реальных
|
||||
ключах запрещён. Найден тестом-сканером `internal/archrules` при его заведении
|
||||
- **Почему не поймали раньше:** `errorlint` видит `err == ErrX` и приведение типа,
|
||||
но матчинг по тексту не видит; прозой это правило записано не было, и ревью его
|
||||
не спрашивало
|
||||
- **Что меняем:** узнавание переведено на `errors.Is(err, io.EOF)`; класс закрыт
|
||||
тестом-сканером (docs/autotests.md, «Ошибки и отказы»)
|
||||
|
||||
## 2026-08-13 — правило гейта обходилось одной лишней строкой [пойман ревью]
|
||||
|
||||
- **Где:** `.golangci.yml`, правило `forbidigo` о суждении по живой карте
|
||||
заголовков — заведено в тот же день задачей `response-assertions-judge-result`
|
||||
- **Симптом:** правило ловило только прямую цепочку `w.Header().Get`. Присваивание
|
||||
в переменную (`h := w.Header()`), чтение по индексу карты, обход `range` и поле
|
||||
`HeaderMap` проходили гейт зелёными — то есть класс, стоивший трёх зелёных
|
||||
гейтов, возвращался четвёртый раз, и уже без человеческой страховки: документы
|
||||
успели снять его с прохода ревью
|
||||
- **Причина:** `forbidigo` по умолчанию судит по печатному тексту вызова, а не по
|
||||
типу значения. Правило, записанное текстом, отсекает одну форму записи, а не
|
||||
свойство
|
||||
- **Чем воспроизведён:** прогоном линтера на файле проверок с шестью формами
|
||||
чтения живой карты: помечена была одна
|
||||
- **Почему не поймали раньше:** правило проверили ровно тем нарушением, против
|
||||
которого писали. Мутация была, но одна — нужна была по одной на каждую форму
|
||||
- **Что меняем:** правило судит по типу приёмника (`analyze-types`,
|
||||
`httptest.ResponseRecorder.Header` и `.HeaderMap`) и ловит все шесть форм;
|
||||
проверено мутацией по каждой. Отсюда же строка в docs/autotests.md, «Лестница
|
||||
механизации»: запрет по имени, обходимый лишней строкой, — это ступень
|
||||
тест-сканера, наряженная запретом
|
||||
|
||||
## 2026-08-12 — закрыли поверхность так, что войти не мог никто [пойман ревью]
|
||||
|
||||
- **Где:** шаг схемы `202608120001` задачи `oidc-login`, правило создания записи
|
||||
|
||||
Reference in New Issue
Block a user