docs: точного числа накопленного в документах больше нет

- CLAUDE.md, «Язык»: ссылаться можно на конкретную запись или на весь корпус
  разом, но не на их количество — число протухает молча, машина его не считает.
  Изъятие названо: неизменное число и историческое в записи о прошлом остаются.
- Сняты счёты capability, прогонов ревью, типизированных ошибок, воркеров,
  сверок документов и правил линтера в docs/, спеке pipeline и CLAUDE.md.
- Заодно исправлено то, что этот же счёт и скрывал: типизированных ошибок три,
  а не две — LostAcquisitionError был потерян из перечня.
This commit is contained in:
av
2026-08-13 19:23:00 +03:00
parent 4c87220d90
commit 903941f587
9 changed files with 51 additions and 32 deletions
+18 -3
View File
@@ -141,7 +141,7 @@ task gate # весь набор проверок разом
**затронутых файлах** дешёвую часть: `gofmt` (правит на месте и добавляет в **затронутых файлах** дешёвую часть: `gofmt` (правит на месте и добавляет в
коммит), `golangci-lint` по пакетам тронутых файлов, `shellcheck`, `hadolint`, коммит), `golangci-lint` по пакетам тронутых файлов, `shellcheck`, `hadolint`,
`gitleaks` по индексу. Только гейту остаются сборка, `go vet`, тесты целиком, `gitleaks` по индексу. Только гейту остаются сборка, `go vet`, тесты целиком,
сверка версий Go, три сверки документов и `govulncheck`: они смотрят всё сверка версий Go, сверки документов и `govulncheck`: они смотрят всё
дерево либо требуют сети, а pre-commit обязан быть быстрым. дерево либо требуют сети, а pre-commit обязан быть быстрым.
- **Шагу `vulns` нужна сеть**, и он один такой: база уязвимостей живёт на - **Шагу `vulns` нужна сеть**, и он один такой: база уязвимостей живёт на
vuln.go.dev. Без сети шаг краснеет, а не пропускается молча; сам инструмент vuln.go.dev. Без сети шаг краснеет, а не пропускается молча; сам инструмент
@@ -155,8 +155,8 @@ task gate # весь набор проверок разом
которого образ перестаёт собираться, но собираемости не проверяет. Собрать которого образ перестаёт собираться, но собираемости не проверяет. Собрать
образ по-прежнему может только человек — `task image`, и на подъёме версии образ по-прежнему может только человек — `task image`, и на подъёме версии
это обязательно; это обязательно;
- собираемость `Dockerfile`: `hadolint` судит форму, а не сборку, и два его - собираемость `Dockerfile`: `hadolint` судит форму, а не сборку, и часть его
правила подавлены поимённо — `DL3007` до задачи `pin-runtime-image-base` и правил подавлена поимённо — `DL3007` до задачи `pin-runtime-image-base` и
`DL3018` по существу (alpine не держит старые версии пакетов, закрепление `DL3018` по существу (alpine не держит старые версии пакетов, закрепление
ломает сборку через недели). Причины стоят строками в `Taskfile.yml`; ломает сборку через недели). Причины стоят строками в `Taskfile.yml`;
- `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс - `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс
@@ -231,3 +231,18 @@ task gate # весь набор проверок разом
- Документация, комментарии, сообщения коммитов — русский. - Документация, комментарии, сообщения коммитов — русский.
- Код и идентификаторы — английский. - Код и идентификаторы — английский.
- Текст, который видит пользователь Telegram, — русский. - Текст, который видит пользователь Telegram, — русский.
- **Точного числа накопленного в документах нет.** «Три capability», «пять
прогонов ревью», «две типизированные ошибки» расходятся с действительностью на
первой же задаче, которая прибавит четвёртую, — и расходятся молча: машина
такое не считает, а читатель верит написанному. Ссылаться можно только на
**конкретную запись** (по имени, со ссылкой) либо на **весь корпус разом**
(«заведённые capability», «записи журнала ниже»). Само перечисление при этом
законно: перечень обновляют вместе с предметом, а число живёт отдельно от него
и потому протухает в одиночку.
*Изъятие:* число, которое не растёт с работой, остаётся числом — количество
уровней журнала в библиотеке, ступеней сборки образа, состояний списка на
экране. Так же законно **историческое** число в записи о прошлом: «решением от
2026-08-13 закрыты четыре задачи» описывает событие, а не сегодняшний счёт.
Настройки с числовым значением — свой случай, их дом
[docs/database.md](docs/database.md).
+8 -7
View File
@@ -8,8 +8,8 @@
[passport.md](passport.md) и в [tasks/BACKLOG.md](../tasks/BACKLOG.md); что из [passport.md](passport.md) и в [tasks/BACKLOG.md](../tasks/BACKLOG.md); что из
этого ещё не решено — в разделе «Открытые вопросы». этого ещё не решено — в разделе «Открытые вопросы».
Заведены четыре capability, и все нормируют **поведение сервиса** для его Заведённые capability нормируют **поведение сервиса** для его потребителей —
потребителей. Инструмент, которым сервис собирают, спеками не нормируется вовсе: все до одной. Инструмент, которым сервис собирают, спеками не нормируется вовсе:
у набора проверок и сборки другой потребитель — тот, кто собирает, — и решением у набора проверок и сборки другой потребитель — тот, кто собирает, — и решением
от 2026-08-13 его нормы живут в самих шагах, их проверках и от 2026-08-13 его нормы живут в самих шагах, их проверках и
[conventions/go-linters.md](conventions/go-linters.md). [conventions/go-linters.md](conventions/go-linters.md).
@@ -87,8 +87,8 @@
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: цепочка переходов состояний --> <!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: цепочка переходов состояний -->
Конвейер: `created``converted``transcribe``done` либо `failed`. Три Конвейер: `created``converted``transcribe``done` либо `failed`. Каждый
воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Что переход двигает свой воркер, и каждый опрашивает базу раз в секунду. Что
делает задача, исчерпавшая попытки, нормирует делает задача, исчерпавшая попытки, нормирует
[pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние [pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние
«мертва»». «мертва»».
@@ -141,7 +141,7 @@
`transcriber_worker_job_count` с меткой `error="true"` и по логам контейнера. `transcriber_worker_job_count` с меткой `error="true"` и по логам контейнера.
Отдельного оповещения нет. Отдельного оповещения нет.
- **Характер потока:** непрерывный, но разреженный. Бот держит длинный опрос, - **Характер потока:** непрерывный, но разреженный. Бот держит длинный опрос,
три воркера опрашивают базу вхолостую с паузой из воркеры опрашивают базу вхолостую с паузой из
[database.md](database.md), «Настройки с числовым значением». [database.md](database.md), «Настройки с числовым значением».
## Единые точки проекта ## Единые точки проекта
@@ -228,8 +228,9 @@
конвертер этот случай не проверялся. конвертер этот случай не проверялся.
- **Очередь.** Модель очереди сделана задачей `pocketbase-storage` 2026-08-12 - **Очередь.** Модель очереди сделана задачей `pocketbase-storage` 2026-08-12
([ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md)) и нормирована ([ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md)) и нормирована
спекой `pipeline`. Не решено, отказываться ли от холостого опроса: три воркера спекой `pipeline`. Не решено, отказываться ли от холостого опроса: он
дают 259 200 запросов к базе в сутки — расчёт из паузы воркера, а не замер даёт сотни тысяч запросов к базе в сутки — расчёт из числа воркеров и их
паузы, а не замер
([research/job-queue.md](research/job-queue.md), «Как снималось»), — при ([research/job-queue.md](research/job-queue.md), «Как снималось»), — при
нагрузке в единицы записей в день, и во что это обходится, никто не мерил. нагрузке в единицы записей в день, и во что это обходится, никто не мерил.
- **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать - **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать
+1 -1
View File
@@ -21,7 +21,7 @@ severity — в [CLAUDE.md](../../CLAUDE.md).
UUID вместо ULID, лог пишется на каждом шаге и дублируется воркером, `msg` UUID вместо ULID, лог пишется на каждом шаге и дублируется воркером, `msg`
предложение с заглавной буквы вместо константной категории. предложение с заглавной буквы вместо константной категории.
Из этого перечня закрыты два. Доменные ошибки проверялись приведением типа до Часть перечня закрыта. Доменные ошибки проверялись приведением типа до
2026-08-11, задача `errors-as-instead-of-typecast`. Время брали `time.Now()` по 2026-08-11, задача `errors-as-instead-of-typecast`. Время брали `time.Now()` по
месту до 2026-08-13 — теперь его читает единая точка `internal/clock`, и правило месту до 2026-08-13 — теперь его читает единая точка `internal/clock`, и правило
держит линтер. Оба места больше не долг, а регрессия. держит линтер. Оба места больше не долг, а регрессия.
+9 -8
View File
@@ -65,14 +65,15 @@ transcriber — **приложение, а не библиотека**: внеш
вызывающему нужны **данные** ошибки. Достаём `errors.As`. Не плодим типы там, вызывающему нужны **данные** ошибки. Достаём `errors.As`. Не плодим типы там,
где хватает sentinel. где хватает sentinel.
Сегодня в проекте две типизированные ошибки, и обе несут данные: Типизированные ошибки проекта несут данные все до одной:
`contract.JobNotFoundError` (состояние и сообщение) и `contract.NoopJobError` `contract.JobNotFoundError` (состояние и сообщение), `contract.NoopJobError`
(состояние). Третья, `tg.EmptyBotTokenError`, была ровно тем случаем, против (состояние), `contract.LostAcquisitionError` (идентификатор задачи).
которого написано правило — тип без полей, — и снята задачей
`local-run-without-telegram-token` 2026-08-13; её место занял sentinel `tg.EmptyBotTokenError` был ровно тем случаем, против которого написано правило —
`telegram.ErrEmptyToken`. Рядом с ним живёт `contract.ErrDeliveryChannelDown` тип без полей, — и снят задачей `local-run-without-telegram-token` 2026-08-13;
тоже sentinel и по той же причине: заглушка отправителя не знает ни задачи, ни его место занял sentinel `telegram.ErrEmptyToken`. Рядом живёт
чата, и нести ей нечего. `contract.ErrDeliveryChannelDown` — тоже sentinel и по той же причине: заглушка
отправителя не знает ни задачи, ни чата, и нести ей нечего.
## Граница и трансляция: приватный и публичный канал ## Граница и трансляция: приватный и публичный канал
+1 -1
View File
@@ -91,7 +91,7 @@
| Ядро (`internal/service`) не знает ни адаптеров, ни транспортов | `internal/archrules``TestЯдроНеЗнаетОбАдаптерах`, `TestЯдроНеЗнаетОТранспортах` | | Ядро (`internal/service`) не знает ни адаптеров, ни транспортов | `internal/archrules``TestЯдроНеЗнаетОбАдаптерах`, `TestЯдроНеЗнаетОТранспортах` |
| Транспорты (`controller/http`, `controller/tg`, `controller/worker`) не знают друг о друге | `internal/archrules``TestТранспортыНеЗнаютДругОДруге` | | Транспорты (`controller/http`, `controller/tg`, `controller/worker`) не знают друг о друге | `internal/archrules``TestТранспортыНеЗнаютДругОДруге` |
| Адаптер не знает ни ядра, ни транспортов | `internal/archrules``TestАдаптерыНеЗнаютНиЯдра_НиТранспортов` | | Адаптер не знает ни ядра, ни транспортов | `internal/archrules``TestАдаптерыНеЗнаютНиЯдра_НиТранспортов` |
| Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` четыре правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда | | Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` → правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда |
### Отмена и внешний собеседник ### Отмена и внешний собеседник
+1 -1
View File
@@ -242,7 +242,7 @@ Object Storage, скачивание файла из Telegram и опрос оп
Обращения к Telegram этому правилу следуют, и точка чистки одна на все вызовы — Обращения к Telegram этому правилу следуют, и точка чистки одна на все вызовы —
`internal/adapter/telegram`, `NewBot`. Токен стоит в пути **каждого** обращения к `internal/adapter/telegram`, `NewBot`. Токен стоит в пути **каждого** обращения к
Bot API, поэтому чистка на месте употребления закрывала бы один вызов из пяти: Bot API, поэтому чистка на месте употребления закрывала бы один вызов из всех:
- отказ транспорта разворачивает в первопричину клиент бота (`safeClient`), а - отказ транспорта разворачивает в первопричину клиент бота (`safeClient`), а
библиотека отдаёт наш отказ вызывающему нетронутым — этим закрыты `getFile`, библиотека отдаёт наш отказ вызывающему нетронутым — этим закрыты `getFile`,
+1 -1
View File
@@ -39,7 +39,7 @@ CGO сборке не нужен.
### `files` ### `files`
Один файл на одну физическую копию: исходник, результат конвертации и копия в Один файл на одну физическую копию: исходник, результат конвертации и копия в
Object Storage — три разные записи. Object Storage — каждая своей записью.
| Поле | Тип | Что | | Поле | Тип | Что |
| --- | --- | --- | | --- | --- | --- |
+9 -7
View File
@@ -2,13 +2,15 @@
## Как настроен конвейер ## Как настроен конвейер
Артефакты семи прогонов лежат в `openspec/changes/archive/<id>/review/`: у трёх Артефакты прогонов лежат в `openspec/changes/archive/<id>/review/` — под именем
ранних, начиная с `fix-http-handler-tests` 2026-08-11, это `triage.md`, у трёх `triage.md` либо `report.md`: имя менялось по ходу, и оба встречаются. Самый
поздних`report.md`, у седьмого (`start-without-telegram-token` 2026-08-13) ранний`fix-http-handler-tests` 2026-08-11, самый поздний
снова `triage.md`. Сверх них конвейер прогонялся 2026-08-13 на работе, шедшей `start-without-telegram-token` 2026-08-13.
без своего изменения openspec; артефакта в архиве у тех прогонов нет, и урожай их
виден только записями журнала ниже. Разделы ниже заведены наперёд по коду Конвейер прогонялся и на работе, шедшей без своего изменения openspec; артефакта
2026-08-11 и с тех пор правятся урожаем прогонов. в архиве у таких прогонов нет, и урожай их виден только записями журнала ниже.
Разделы ниже заведены наперёд по коду 2026-08-11 и с тех пор правятся урожаем
прогонов.
**Проход, поднявший сервис, обязан его остановить.** Живой прогон стал доступен **Проход, поднявший сервис, обязан его остановить.** Живой прогон стал доступен
2026-08-13 (см. «Недоступно проверке»), и первый же им воспользовался: враждебный 2026-08-13 (см. «Недоступно проверке»), и первый же им воспользовался: враждебный
+3 -3
View File
@@ -25,9 +25,9 @@ done | failed`, отмена контекста посреди шага и ос
узнаваться по смыслу значения, а не по его точной форме, и MUST переживать узнаваться по смыслу значения, а не по его точной форме, и MUST переживать
пояснения, добавленные к этому значению на любом промежуточном шаге пути. пояснения, добавленные к этому значению на любом промежуточном шаге пути.
Требование стоит на инварианте проекта «`NoopJobError` — не ошибка»: три воркера Требование стоит на инварианте проекта «`NoopJobError` — не ошибка»: воркеры
опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт три опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт от
записи отказа в секунду и столько же засчитанных сбоев, которых не было. каждого запись отказа в секунду и столько же засчитанных сбоев, которых не было.
Признак пустого прогона MUST рождаться только ответом хранилища на опрос этим же Признак пустого прогона MUST рождаться только ответом хранилища на опрос этим же
шагом. Слой, придающий отказу собственный смысл, MUST не сохранять чужой признак шагом. Слой, придающий отказу собственный смысл, MUST не сохранять чужой признак