From 903941f58700a253965cca9313780f69f118f4e3 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 13 Aug 2026 19:23:00 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D1=82=D0=BE=D1=87=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=87=D0=B8=D1=81=D0=BB=D0=B0=20=D0=BD=D0=B0=D0=BA?= =?UTF-8?q?=D0=BE=D0=BF=D0=BB=D0=B5=D0=BD=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=B2?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=85?= =?UTF-8?q?=20=D0=B1=D0=BE=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md, «Язык»: ссылаться можно на конкретную запись или на весь корпус разом, но не на их количество — число протухает молча, машина его не считает. Изъятие названо: неизменное число и историческое в записи о прошлом остаются. - Сняты счёты capability, прогонов ревью, типизированных ошибок, воркеров, сверок документов и правил линтера в docs/, спеке pipeline и CLAUDE.md. - Заодно исправлено то, что этот же счёт и скрывал: типизированных ошибок три, а не две — LostAcquisitionError был потерян из перечня. --- CLAUDE.md | 21 ++++++++++++++++++--- docs/architecture.md | 15 ++++++++------- docs/conventions/README.md | 2 +- docs/conventions/errors.md | 17 +++++++++-------- docs/conventions/go-linters.md | 2 +- docs/conventions/logging.md | 2 +- docs/database.md | 2 +- docs/review.md | 16 +++++++++------- openspec/specs/pipeline/spec.md | 6 +++--- 9 files changed, 51 insertions(+), 32 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index c0d6892..bf2dae8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -141,7 +141,7 @@ task gate # весь набор проверок разом **затронутых файлах** дешёвую часть: `gofmt` (правит на месте и добавляет в коммит), `golangci-lint` по пакетам тронутых файлов, `shellcheck`, `hadolint`, `gitleaks` по индексу. Только гейту остаются сборка, `go vet`, тесты целиком, - сверка версий Go, три сверки документов и `govulncheck`: они смотрят всё + сверка версий Go, сверки документов и `govulncheck`: они смотрят всё дерево либо требуют сети, а pre-commit обязан быть быстрым. - **Шагу `vulns` нужна сеть**, и он один такой: база уязвимостей живёт на vuln.go.dev. Без сети шаг краснеет, а не пропускается молча; сам инструмент @@ -155,8 +155,8 @@ task gate # весь набор проверок разом которого образ перестаёт собираться, но собираемости не проверяет. Собрать образ по-прежнему может только человек — `task image`, и на подъёме версии это обязательно; - - собираемость `Dockerfile`: `hadolint` судит форму, а не сборку, и два его - правила подавлены поимённо — `DL3007` до задачи `pin-runtime-image-base` и + - собираемость `Dockerfile`: `hadolint` судит форму, а не сборку, и часть его + правил подавлена поимённо — `DL3007` до задачи `pin-runtime-image-base` и `DL3018` по существу (alpine не держит старые версии пакетов, закрепление ломает сборку через недели). Причины стоят строками в `Taskfile.yml`; - `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс @@ -231,3 +231,18 @@ task gate # весь набор проверок разом - Документация, комментарии, сообщения коммитов — русский. - Код и идентификаторы — английский. - Текст, который видит пользователь Telegram, — русский. +- **Точного числа накопленного в документах нет.** «Три capability», «пять + прогонов ревью», «две типизированные ошибки» расходятся с действительностью на + первой же задаче, которая прибавит четвёртую, — и расходятся молча: машина + такое не считает, а читатель верит написанному. Ссылаться можно только на + **конкретную запись** (по имени, со ссылкой) либо на **весь корпус разом** + («заведённые capability», «записи журнала ниже»). Само перечисление при этом + законно: перечень обновляют вместе с предметом, а число живёт отдельно от него + и потому протухает в одиночку. + + *Изъятие:* число, которое не растёт с работой, остаётся числом — количество + уровней журнала в библиотеке, ступеней сборки образа, состояний списка на + экране. Так же законно **историческое** число в записи о прошлом: «решением от + 2026-08-13 закрыты четыре задачи» описывает событие, а не сегодняшний счёт. + Настройки с числовым значением — свой случай, их дом + [docs/database.md](docs/database.md). diff --git a/docs/architecture.md b/docs/architecture.md index c402fdd..b98a6af 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,8 +8,8 @@ [passport.md](passport.md) и в [tasks/BACKLOG.md](../tasks/BACKLOG.md); что из этого ещё не решено — в разделе «Открытые вопросы». -Заведены четыре capability, и все нормируют **поведение сервиса** для его -потребителей. Инструмент, которым сервис собирают, спеками не нормируется вовсе: +Заведённые capability нормируют **поведение сервиса** для его потребителей — +все до одной. Инструмент, которым сервис собирают, спеками не нормируется вовсе: у набора проверок и сборки другой потребитель — тот, кто собирает, — и решением от 2026-08-13 его нормы живут в самих шагах, их проверках и [conventions/go-linters.md](conventions/go-linters.md). @@ -87,8 +87,8 @@ -Конвейер: `created` → `converted` → `transcribe` → `done` либо `failed`. Три -воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Что +Конвейер: `created` → `converted` → `transcribe` → `done` либо `failed`. Каждый +переход двигает свой воркер, и каждый опрашивает базу раз в секунду. Что делает задача, исчерпавшая попытки, нормирует [pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние «мертва»». @@ -141,7 +141,7 @@ `transcriber_worker_job_count` с меткой `error="true"` и по логам контейнера. Отдельного оповещения нет. - **Характер потока:** непрерывный, но разреженный. Бот держит длинный опрос, - три воркера опрашивают базу вхолостую с паузой из + воркеры опрашивают базу вхолостую с паузой из [database.md](database.md), «Настройки с числовым значением». ## Единые точки проекта @@ -228,8 +228,9 @@ конвертер этот случай не проверялся. - **Очередь.** Модель очереди сделана задачей `pocketbase-storage` 2026-08-12 ([ADR](adr/ADR-2026-08-11-queue-as-pocketbase-collection.md)) и нормирована - спекой `pipeline`. Не решено, отказываться ли от холостого опроса: три воркера - дают 259 200 запросов к базе в сутки — расчёт из паузы воркера, а не замер + спекой `pipeline`. Не решено, отказываться ли от холостого опроса: он + даёт сотни тысяч запросов к базе в сутки — расчёт из числа воркеров и их + паузы, а не замер ([research/job-queue.md](research/job-queue.md), «Как снималось»), — при нагрузке в единицы записей в день, и во что это обходится, никто не мерил. - **Наблюдаемость.** `/metrics` остаётся и развивается. Чем — дописывать diff --git a/docs/conventions/README.md b/docs/conventions/README.md index 01babf9..791b50e 100644 --- a/docs/conventions/README.md +++ b/docs/conventions/README.md @@ -21,7 +21,7 @@ severity — в [CLAUDE.md](../../CLAUDE.md). UUID вместо ULID, лог пишется на каждом шаге и дублируется воркером, `msg` — предложение с заглавной буквы вместо константной категории. -Из этого перечня закрыты два. Доменные ошибки проверялись приведением типа до +Часть перечня закрыта. Доменные ошибки проверялись приведением типа до 2026-08-11, задача `errors-as-instead-of-typecast`. Время брали `time.Now()` по месту до 2026-08-13 — теперь его читает единая точка `internal/clock`, и правило держит линтер. Оба места больше не долг, а регрессия. diff --git a/docs/conventions/errors.md b/docs/conventions/errors.md index e7fc914..ca8ba84 100644 --- a/docs/conventions/errors.md +++ b/docs/conventions/errors.md @@ -65,14 +65,15 @@ transcriber — **приложение, а не библиотека**: внеш вызывающему нужны **данные** ошибки. Достаём `errors.As`. Не плодим типы там, где хватает sentinel. -Сегодня в проекте две типизированные ошибки, и обе несут данные: -`contract.JobNotFoundError` (состояние и сообщение) и `contract.NoopJobError` -(состояние). Третья, `tg.EmptyBotTokenError`, была ровно тем случаем, против -которого написано правило — тип без полей, — и снята задачей -`local-run-without-telegram-token` 2026-08-13; её место занял sentinel -`telegram.ErrEmptyToken`. Рядом с ним живёт `contract.ErrDeliveryChannelDown` — -тоже sentinel и по той же причине: заглушка отправителя не знает ни задачи, ни -чата, и нести ей нечего. +Типизированные ошибки проекта несут данные все до одной: +`contract.JobNotFoundError` (состояние и сообщение), `contract.NoopJobError` +(состояние), `contract.LostAcquisitionError` (идентификатор задачи). + +`tg.EmptyBotTokenError` был ровно тем случаем, против которого написано правило — +тип без полей, — и снят задачей `local-run-without-telegram-token` 2026-08-13; +его место занял sentinel `telegram.ErrEmptyToken`. Рядом живёт +`contract.ErrDeliveryChannelDown` — тоже sentinel и по той же причине: заглушка +отправителя не знает ни задачи, ни чата, и нести ей нечего. ## Граница и трансляция: приватный и публичный канал diff --git a/docs/conventions/go-linters.md b/docs/conventions/go-linters.md index f74152e..af29117 100644 --- a/docs/conventions/go-linters.md +++ b/docs/conventions/go-linters.md @@ -91,7 +91,7 @@ | Ядро (`internal/service`) не знает ни адаптеров, ни транспортов | `internal/archrules` → `TestЯдроНеЗнаетОбАдаптерах`, `TestЯдроНеЗнаетОТранспортах` | | Транспорты (`controller/http`, `controller/tg`, `controller/worker`) не знают друг о друге | `internal/archrules` → `TestТранспортыНеЗнаютДругОДруге` | | Адаптер не знает ни ядра, ни транспортов | `internal/archrules` → `TestАдаптерыНеЗнаютНиЯдра_НиТранспортов` | -| Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` → четыре правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда | +| Колонки очереди согласованы: перечень захвата ↔ структура захвата ↔ шаг схемы ↔ запись коллекции ↔ перенос поля в задачу | `internal/archrules` → правила о захвате. Закрывает инвариант «колонки правятся в четырёх местах» (CLAUDE.md, major), которого компилятор не держит. Литерал колонки ищется в телах нужных функций: по файлу целиком условие выполнялось бы тегами `db:"…"` самой структуры, и правило было бы зелёным всегда | ### Отмена и внешний собеседник diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index 0ea30dd..ff19daa 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -242,7 +242,7 @@ Object Storage, скачивание файла из Telegram и опрос оп Обращения к Telegram этому правилу следуют, и точка чистки одна на все вызовы — `internal/adapter/telegram`, `NewBot`. Токен стоит в пути **каждого** обращения к -Bot API, поэтому чистка на месте употребления закрывала бы один вызов из пяти: +Bot API, поэтому чистка на месте употребления закрывала бы один вызов из всех: - отказ транспорта разворачивает в первопричину клиент бота (`safeClient`), а библиотека отдаёт наш отказ вызывающему нетронутым — этим закрыты `getFile`, diff --git a/docs/database.md b/docs/database.md index 0ff1a9b..5707688 100644 --- a/docs/database.md +++ b/docs/database.md @@ -39,7 +39,7 @@ CGO сборке не нужен. ### `files` Один файл на одну физическую копию: исходник, результат конвертации и копия в -Object Storage — три разные записи. +Object Storage — каждая своей записью. | Поле | Тип | Что | | --- | --- | --- | diff --git a/docs/review.md b/docs/review.md index ee7b3cf..38b7491 100644 --- a/docs/review.md +++ b/docs/review.md @@ -2,13 +2,15 @@ ## Как настроен конвейер -Артефакты семи прогонов лежат в `openspec/changes/archive//review/`: у трёх -ранних, начиная с `fix-http-handler-tests` 2026-08-11, это `triage.md`, у трёх -поздних — `report.md`, у седьмого (`start-without-telegram-token` 2026-08-13) — -снова `triage.md`. Сверх них конвейер прогонялся 2026-08-13 на работе, шедшей -без своего изменения openspec; артефакта в архиве у тех прогонов нет, и урожай их -виден только записями журнала ниже. Разделы ниже заведены наперёд по коду -2026-08-11 и с тех пор правятся урожаем прогонов. +Артефакты прогонов лежат в `openspec/changes/archive//review/` — под именем +`triage.md` либо `report.md`: имя менялось по ходу, и оба встречаются. Самый +ранний — `fix-http-handler-tests` 2026-08-11, самый поздний — +`start-without-telegram-token` 2026-08-13. + +Конвейер прогонялся и на работе, шедшей без своего изменения openspec; артефакта +в архиве у таких прогонов нет, и урожай их виден только записями журнала ниже. +Разделы ниже заведены наперёд по коду 2026-08-11 и с тех пор правятся урожаем +прогонов. **Проход, поднявший сервис, обязан его остановить.** Живой прогон стал доступен 2026-08-13 (см. «Недоступно проверке»), и первый же им воспользовался: враждебный diff --git a/openspec/specs/pipeline/spec.md b/openspec/specs/pipeline/spec.md index bfeaa69..751b2a8 100644 --- a/openspec/specs/pipeline/spec.md +++ b/openspec/specs/pipeline/spec.md @@ -25,9 +25,9 @@ done | failed`, отмена контекста посреди шага и ос узнаваться по смыслу значения, а не по его точной форме, и MUST переживать пояснения, добавленные к этому значению на любом промежуточном шаге пути. -Требование стоит на инварианте проекта «`NoopJobError` — не ошибка»: три воркера -опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт три -записи отказа в секунду и столько же засчитанных сбоев, которых не было. +Требование стоит на инварианте проекта «`NoopJobError` — не ошибка»: воркеры +опрашивают базу раз в секунду, и пустой прогон, принятый за отказ, даёт от +каждого запись отказа в секунду и столько же засчитанных сбоев, которых не было. Признак пустого прогона MUST рождаться только ответом хранилища на опрос этим же шагом. Слой, придающий отказу собственный смысл, MUST не сохранять чужой признак