docs: раскладка переехала в .av-dev.toml, а расхождения документов сведены

- Перевод на канон 1 доделан: адреса служебного файла и имена скиллов
  переставлены в девяти местах прозы и кода, гейт зовёт три скрипта по новым
  путям, прежние docs/.docs.json и tasks/.tasks.json удалены.
- Сверка двумя агентами нашла четырнадцать расхождений, тринадцать сведены
  строками: число прогонов ревью и преамбула журнала дефектов, счёт capability,
  маршруты README, дубли инварианта захвата и кодов прогона, протухшие указатели
  записок разведки, маркер долга на переехавшем абзаце. Срок жизни сессии
  нормирует спека access, database.md на неё ссылается.
- Purpose спеки pipeline объявляет неописанным то, что в ней же и стоит; правка
  идёт изменением openspec, поэтому заведена задача pipeline-spec-purpose-drift.
This commit is contained in:
av
2026-08-13 12:36:36 +03:00
parent eacaf76d5f
commit b76f2d7c7e
25 changed files with 171 additions and 85 deletions
+12
View File
@@ -0,0 +1,12 @@
# Раскладка av-dev в этом проекте: версия и настройки проверок.
# Файл ведут скиллы плагина, править руками можно — комментарии свои.
version = 1 # версия раскладки; обратной совместимости нет, есть «приведён» и «нет»
[docs]
# каталог миграций: по нему docs.py сверяет схему с database.md
migrations = "internal/adapter/repo/pocketbase/migrations"
[tasks]
# каталог задач от корня репозитория; имена частей — умолчания скрипта
dir = "tasks"
+4 -4
View File
@@ -162,7 +162,7 @@ task gate # весь набор проверок разом
- `gitleaks` — висит на pre-commit в `lefthook.yml` и смотрит только индекс
коммита. Полную историю никто не проверяет;
- согласованность документов между собой и с кодом — её судят агенты, зовёт
их скилл `av-dev-docs:healthcheck`, и звать его надо руками;
их скилл `av-dev:doc-healthcheck`, и звать его надо руками;
- покрытие изменённых строк не считается ничем.
**Гейт на `master` сегодня зелёный целиком, и объявленных долгов у него нет.**
@@ -206,9 +206,9 @@ task gate # весь набор проверок разом
ключа конфига, любое действие с боевыми данными и с Yandex Cloud, ротация
секрета.
- **Что считается сломанным** — новый красный шаг гейта, которого не было до
твоей правки. Такое чинится прежде любой другой работы. Два объявленных долга
из раздела «Гейт» сломанным состоянием **не** считаются, пока их не закрыли
задачами.
твоей правки. Такое чинится прежде любой другой работы. Исключений из этого
правила нет: раздел «Гейт» называет оба прежних долга закрытыми, и списывать
красный шаг больше не на что.
- **Ориентир по размеру порции:** не замерялся.
- **Что такое «сделана»:** `task gate` зелёный и критерии приёмки проверены
поимённо.
+8 -4
View File
@@ -62,9 +62,13 @@ inv pl -- transcriber
## HTTP API
Четыре маршрута: `POST /api/audio` — приём записи, `GET /api/status/:id`
готовность задачи, `GET /metrics` — метрики Prometheus с префиксом
`transcriber_`, `GET /health` — проверка живости.
Семь адресов приложения: `POST /api/audio` — приём записи, `GET /api/status/:id`
готовность задачи, `GET /auth/login`, `GET /auth/callback` и
`POST /auth/logout` — вход через провайдера
([access](openspec/specs/access/spec.md)), `GET /metrics` — метрики Prometheus с
префиксом `transcriber_`, `GET /health` — проверка живости. Сверх них тем же
портом отдаётся собственная поверхность встроенного хранилища и панель `/_/` —
[docs/security.md](docs/security.md), «Из чего строятся пути и ключи».
Контракт приёма и опроса нормативен и живёт в
[openspec/specs/intake/spec.md](openspec/specs/intake/spec.md): поля запроса и
@@ -74,7 +78,7 @@ inv pl -- transcriber
## Состояния задач
Перечень состояний, переходы между ними и число воркеров —
[docs/database.md](docs/database.md), разделы «Таблицы» и «Представление
[docs/database.md](docs/database.md), разделы «Коллекции» и «Представление
данных»; как сложен конвейер целиком — [docs/architecture.md](docs/architecture.md).
## Структура проекта
+13 -12
View File
@@ -15,9 +15,9 @@ vars:
# отправлял читателя искать разъехавшееся там, где просто неполно дерево. Сам
# `task` отдаёт наружу свой 201 на любой отказ шага, поэтому словарь читается
# по коду скрипта, а не по коду `task`.
DOCS_PY: '{{.DOCS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev-docs/skills/canon/scripts/docs.py"}}'
TASKS_PY: '{{.TASKS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev-tasks/skills/tasks/scripts/tasks.py"}}'
OPENSPEC_PY: '{{.OPENSPEC_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev-code/skills/openspec/scripts/openspec.py"}}'
DOCS_PY: '{{.DOCS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/doc-canon/scripts/docs.py"}}'
TASKS_PY: '{{.TASKS_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/task-track/scripts/tasks.py"}}'
OPENSPEC_PY: '{{.OPENSPEC_PY | default "~/.claude/plugins/marketplaces/av-dev-skills/av-dev/skills/code-openspec/scripts/openspec.py"}}'
tasks:
@@ -89,19 +89,20 @@ tasks:
echo "задай свою: task migrations BASE=<rev>"
exit 3
fi
# Каталог шагов берётся из docs/.docs.json — там он уже записан ключом
# `migrations` для сверки документов. Свой литерал завёл бы факту второй
# дом: каталог переехал бы, а один из двух стражей молча позеленел.
dir=$(python3 -c 'import json,sys; print(json.load(open("docs/.docs.json"))["migrations"])' 2>/dev/null) || dir=""
# Каталог шагов берётся из .av-dev.toml — там он уже записан ключом
# `migrations` секции `[docs]` для сверки документов. Свой литерал завёл
# бы факту второй дом: каталог переехал бы, а один из двух стражей молча
# позеленел. До слияния плагинов файл звался docs/.docs.json.
dir=$(python3 -c 'import tomllib; print(tomllib.load(open(".av-dev.toml","rb"))["docs"]["migrations"])' 2>/dev/null) || dir=""
if [ -z "$dir" ] || [ ! -d "$dir" ]; then
echo "каталог шагов схемы не найден: ключ migrations в docs/.docs.json → '$dir'"
echo "каталог шагов схемы не найден: ключ [docs] migrations в .av-dev.toml → '$dir'"
exit 3
fi
# Страж предмета: правило, потерявшее файлы, стало бы вечно зелёным от
# одного переименования — тот же приём, что у правил `internal/archrules`.
if [ -z "$(ls "$dir" | grep -E '^[0-9]{12}_.*\.go$')" ]; then
echo "в $dir нет ни одного файла шага: правило потеряло предмет"
echo "поправь шаблон имени в этом шаге либо ключ migrations в docs/.docs.json"
echo "поправь шаблон имени в этом шаге либо ключ [docs] migrations в .av-dev.toml"
exit 3
fi
# Баз две, и вторая обязательна. `{{.BASE}}` отвечает на «шаг уже уехал»
@@ -175,7 +176,7 @@ tasks:
py=$(eval echo {{.DOCS_PY}})
if [ ! -f "$py" ]; then
echo "docs.py не найден: $py"
echo "поставь плагин av-dev-docs либо задай путь: task docs DOCS_PY=<путь>"
echo "поставь плагин av-dev либо задай путь: task docs DOCS_PY=<путь>"
exit 3
fi
python3 "$py" check --base {{.BASE}}
@@ -187,7 +188,7 @@ tasks:
py=$(eval echo {{.TASKS_PY}})
if [ ! -f "$py" ]; then
echo "tasks.py не найден: $py"
echo "поставь плагин av-dev-tasks либо задай путь: task tasks TASKS_PY=<путь>"
echo "поставь плагин av-dev либо задай путь: task tasks TASKS_PY=<путь>"
exit 3
fi
python3 "$py" check --dir tasks
@@ -199,7 +200,7 @@ tasks:
py=$(eval echo {{.OPENSPEC_PY}})
if [ ! -f "$py" ]; then
echo "openspec.py не найден: $py"
echo "поставь плагин av-dev-code либо задай путь: task openspec OPENSPEC_PY=<путь>"
echo "поставь плагин av-dev либо задай путь: task openspec OPENSPEC_PY=<путь>"
exit 3
fi
python3 "$py" check --dir .
-4
View File
@@ -1,4 +0,0 @@
{
"canon": 14,
"migrations": "internal/adapter/repo/pocketbase/migrations"
}
+4 -1
View File
@@ -21,7 +21,10 @@
- Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято.
Слаг **английский по сути, а не транслитом**: `queue-as-table`, не
`ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`.
- Записи неизменяемы: передумали — новая запись, старой ставится статус.
- Записи неизменяемы **в решении**: передумали — новая запись, старой ставится
статус. Уточнить прежнюю запись можно только строкой «*Уточнено ГГГГ-ММ-ДД:*» в
разделе «Последствия» и только фактом, который решения не меняет, — например
действующим адресом того, что решение завело.
- Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и
`устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и
источником, а не абзацем в теле.
+5 -4
View File
@@ -83,12 +83,13 @@
| Шаги схемы | `internal/adapter/repo/pocketbase/migrations` | Файл на шаг, имя файла — имя шага; там же имена коллекций |
| Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Правка задачи в панели проходит те же правила перехода, что и правка из кода |
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: спека заведена, но это в ней не описано -->
<!-- канон: поведение → openspec/specs/pipeline; ещё НЕ переехало: цепочка переходов состояний -->
Конвейер: `created``converted``transcribe``done` либо `failed`. Три
воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Задача,
исчерпавшая попытки, уходит в `dead` мимо этой цепочки: её переводит туда не шаг,
а тот, кто её захватил.
воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Что
делает задача, исчерпавшая попытки, нормирует
[pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние
«мертва»».
## Внешние границы и форматы
+5 -4
View File
@@ -71,7 +71,8 @@ Go-проект как есть. Своё здесь — перечень пра
документов. Дороже всех — у шага появляется своя норма и свои тесты.
Ступень, выбранная неверно, видна сразу. Запрет по имени, обходимый одной
лишней строкой, — это ступень 4, наряженная третьей: так было с правилом о
лишней строкой, — на деле правило четвёртой ступени, оформленное как правило
третьей: так было с правилом о
заголовках ответа, которое сначала запретило текст `\.Header\(\)\.Get`, а
обходилось присваиванием в переменную. Правило переписано на суждение **по типу
приёмника** (`analyze-types`), и это уже настоящая третья ступень.
@@ -146,7 +147,7 @@ Go-проект как есть. Своё здесь — перечень пра
| Проверка судит ответ по готовому ответу (`Result()`), а не по живой карте заголовков обработчика | `.golangci.yml``forbidigo` с `analyze-types`, находки только в `*_test.go`. Судит по типу приёмника (`httptest.ResponseRecorder`), поэтому ловит любую форму: цепочкой, через переменную, по индексу карты, обходом, полем `HeaderMap`. Остаётся ревью проверка, идущая мимо recorder — через свой `http.ResponseWriter` |
| Каждый сценарий нормы шага сверки версий проверен мутацией, а не памятью | `scripts/check_go_version_test.go` — 20 сценариев спеки `toolchain` плюс два свойства самого шага: исход не зависит от установленного `go`, и шаг не зовёт ни `go`, ни `docker`, ни сеть |
| Форма утверждения в проверках: «ожидалось» и «получено» не перепутаны местами, отказ судится `NoError`, а не `Nil`, `require` не зовут из горутины | `.golangci.yml``testifylint` |
| Одновременный доступ проверен детектором, а не чтением кода | `Taskfile.yml` → шаг `tests` (`go test -race ./...`). Общее у воркеров — счётчики метрик, логгер и клиент бота; захват задачи в гонку не входит, он по построению её не даёт (одно состояние на воркер) — см. «Типовые ложноположительные» в [../review.md](../review.md). Без компилятора C шаг гоняет тесты без детектора и краснеет кодом 3: гонки — не повод отнимать у гейта сами тесты |
| Одновременный доступ проверен детектором, а не чтением кода | `Taskfile.yml` → шаг `tests` (`go test -race ./...`). Общее у воркеров — счётчики метрик, логгер и клиент бота; захват задачи в гонку не входит, он по построению её не даёт (одно состояние на воркер) — см. «Типовые ложноположительные» в [../review.md](../review.md). Что делает шаг без компилятора C и каким кодом краснеет — [CLAUDE.md](../../CLAUDE.md), «Гейт» |
| Строчное подавление называет линтер и причину, а протухшее краснеет | `.golangci.yml``nolintlint` (`require-explanation`, `require-specific`, `allow-unused: false`) |
### Форма кода и файлов вне Go
@@ -164,8 +165,8 @@ Go-проект как есть. Своё здесь — перечень пра
| Правило | Где механизировано |
| --- | --- |
| Применённый шаг схемы не переписывается: у файла шага допустим один статус — `A` | `Taskfile.yml` → шаг `migrations`. Закрывает инвариант CLAUDE.md (critical), которого не держит ни компилятор, ни хранилище: применённое считается по имени файла. Баз диффа две — `BASE` и `HEAD`: первая отвечает на «шаг уже уехал» ровно настолько, насколько свежа `origin/master`, вторая ловит правку закоммиченного шага независимо от неё. Каталог берётся из ключа `migrations` в `docs/.docs.json`, чтобы у факта не было второго дома; пустой каталог роняет шаг — правило, потерявшее предмет, молчать не должно. `migrations.go` под правило не подпадает: строка `Register` нового шага прибавляется именно там |
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` в `docs/.docs.json` |
| Применённый шаг схемы не переписывается: у файла шага допустим один статус — `A` | `Taskfile.yml` → шаг `migrations`. Закрывает инвариант CLAUDE.md (critical), которого не держит ни компилятор, ни хранилище: применённое считается по имени файла. Баз диффа две — `BASE` и `HEAD`: первая отвечает на «шаг уже уехал» ровно настолько, насколько свежа `origin/master`, вторая ловит правку закоммиченного шага независимо от неё. Каталог берётся из ключа `migrations` секции `[docs]` в `.av-dev.toml`, чтобы у факта не было второго дома. Исходы шага и их коды — [CLAUDE.md](../../CLAUDE.md), «Гейт». `migrations.go` под правило не подпадает: строка `Register` нового шага прибавляется именно там |
| Раскладка документов, битые ссылки, изменённый шаг схемы без правки `database.md` | `docs.py check`; каталог шагов задаёт ключ `migrations` секции `[docs]` в `.av-dev.toml` |
| Согласованность каталога задач, форма `openspec/config.yaml` | `tasks.py check`, `openspec.py check` |
| Секреты в коммите | `lefthook.yml``gitleaks git --staged` |
| Достижимая из кода уязвимость в зависимостях | `Taskfile.yml` → шаг `vulns` (`govulncheck ./...`) |
+2 -3
View File
@@ -163,8 +163,7 @@ log := log.With("job_id", job.Id, "capability", "conversion")
*Расхождение, и оно системное:* сегодня шаг конвейера логирует ошибку `Error` и
тут же возвращает её воркеру, который логирует её второй раз. Один сбой даёт две
записи. Плюс `internal/controller/http/transcribe.go` пишет через `log.Printf`
мимо `slog` целиком.
записи.
## Внешние сервисы: логируем все вызовы
@@ -241,7 +240,7 @@ Object Storage, скачивание файла из Telegram и опрос оп
проверка `errors.Is` на причину сохраняется. Общее правило: **секрет не кладём
в URL, если у сервиса есть заголовок** — тогда его нет и в ошибке транспорта.
Разговор с Telegram этому правилу следует, и точка чистки одна на все вызовы —
Обращения к Telegram этому правилу следуют, и точка чистки одна на все вызовы —
`internal/adapter/telegram`, `NewBot`. Токен стоит в пути **каждого** обращения к
Bot API, поэтому чистка на месте употребления закрывала бы один вызов из пяти:
+4 -1
View File
@@ -78,6 +78,9 @@
- **Обёртка — единственное место, где читается код ответа.** Она же превращает
ошибку контракта в доменную ошибку приложения; экран получает готовый текст, а
не `Response`.
- **Сессия живёт кукой `transcriber_session`**, и приложение её не читает: кука
`HttpOnly`, браузер шлёт её сам, а вошедшего экран узнаёт по ответу API. Норма
— [access](../../openspec/specs/access/spec.md).
## Показ ошибок и состояний
@@ -100,5 +103,5 @@
узнала»).
- **Устройство service worker и версионирование статики** — задача
[installable-pwa](../../tasks/items/installable-pwa.md).
- **Где живёт сессия и как приложение узнаёт вошедшего** — открытый вопрос
- **Как связываются пользователь Telegram и пользователь веба** — открытый вопрос
«Учётные записи» в [../architecture.md](../architecture.md).
+10 -8
View File
@@ -16,7 +16,7 @@ CGO сборке не нужен.
Каталог у шагов свой, а не файл внутри пакета репозитория, и причина внешняя:
шаг гейта сверяет изменённые шаги схемы с правкой этого документа по **префиксу
пути** (`docs/.docs.json`, ключ `migrations`), а префикс наводится только на
пути** (`.av-dev.toml`, ключ `migrations` секции `[docs]`), а префикс наводится только на
каталог. Имена коллекций живут там же, рядом с шагом, который их заводит; пакет
репозитория берёт их оттуда.
@@ -78,10 +78,10 @@ capability, и третий смысл развёл бы одно слово п
Прежней колонки `is_error` нет: задача выбывает из выборки состоянием, и способ
этот один.
**Состояния `failed` и `dead` — разные приговоры.** В `failed` задачу переводит
шаг, рассудивший об этой записи окончательно; в `dead` она уходит без такого
суждения — мы повторяли и перестали. Ни один шаг конвейера в `dead` не переводит
сам: это делает тот, кто захватил задачу с превышенным счётчиком.
**Состояния `failed` и `dead` — разные приговоры**, и чей это приговор, нормирует
[pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние
«мертва»». Схеме принадлежит только закрытость перечня: шестое состояние
потребует нового шага.
**Правила доступа обеих коллекций пусты**, то есть перечислять и читать записи
может только владелец панели. Проверено прогоном: анонимный запрос к
@@ -118,8 +118,10 @@ capability, и третий смысл развёл бы одно слово п
поэтому захваты выстраиваются в очередь. Порядок выборки — по времени
заведения **и по ключу**: время неуникально, и без ключа порядок обработки
невоспроизводим.
- **Запись результата условна по признаку захвата.** Шаг, чей захват за время
работы достался другому, завершается без записи и без ответа отправителю.
- **Запись результата условна по признаку захвата** — инвариант «Результат пишет
только держатель захвата» в [CLAUDE.md](../CLAUDE.md), «Инварианты» (major);
норма — [pipeline](../openspec/specs/pipeline/spec.md). Здесь названо потому,
что условие проверяется тем же запросом, что и сам захват.
- **Список колонок задан четырьмя местами** — `applyToRecord`, `recordToJob`,
константой `acquireColumns` и структурой `acquiredRow`, — плюс шагом схемы.
Все четыре лежат в одном пакете, но компилятор видит два: правило правки и его
@@ -148,7 +150,7 @@ capability, и третий смысл развёл бы одно слово п
| Качество кодирования vorbis | `-q:a 4` | `adapter/converter/ffmpeg/ffmpeg.go` | — |
| Жизнь приглашения завести владельца панели | 30 минут | умолчание PocketBase | — |
| Потолок размера одной записи | 8 ГиБ | `entity.MaxRecordSize` | расчётный потолок в шесть часов с запасом на видео |
| Срок жизни сессии | 7 суток | `pbrepo.SessionDuration`, ставится при подъёме | решение владельца 2026-08-12; умолчание библиотеки в 5 суток никем не выбрано |
| Срок жизни сессии | нормирует [access](../openspec/specs/access/spec.md) | `pbrepo.SessionDuration`, ставится при подъёме | решение владельца 2026-08-12; умолчание библиотеки никем не выбрано, и спека прямо запрещает его применять |
| Потолок времени на вход у провайдера | 10 минут | `controller/http/auth.go` | дольше носитель состояния не нужен |
| Таймаут обмена кода у провайдера | 15 секунд | там же | молчащий провайдер иначе держит обработчик возврата открытым |
+1 -1
View File
@@ -22,7 +22,7 @@
| Владелец сервиса | Загрузить диктофонную запись или видео из семейного архива с телефона и получить текст. Видеть, кто сколько загрузил и во что это обошлось |
| Приглашённый пользователь | Войти в приложение через свою учётную запись, загрузить запись, забрать текст, вернуться к ней через месяц. Приложение ставится на телефон; каждый видит только свои записи |
| Пользователь Telegram | Отправить боту голосовое сообщение и получить текст ответом. Работает сегодня |
| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Сегодня почти не работает: приём и опрос закрыты сессией OIDC, а своего токена у программы нет — годится только кука, снятая из браузера. Токен приносит `api-tokens` |
| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Сегодня почти не работает: приём и опрос закрыты сессией OIDC, а своего токена у программы нет — годится только чужая сессия, снятая из браузера и предъявленная кукой либо заголовком `Authorization`. Токен приносит `api-tokens` |
**Основной вход — приложение**, бот и HTTP API дополняют его. До 2026-08-11
основным был бот, и порядок здесь перевёрнут сознательно: диктофонная запись на
+3 -2
View File
@@ -49,8 +49,9 @@
## Захват чинится одним запросом
Сегодняшний захват — два запроса подряд без транзакции
([../database.md](../database.md), «Представление данных»). Замер показал, что
Захват **на момент замера** — два запроса подряд без транзакции; после перехода
на PocketBase он свернулся в один с `RETURNING`
[../database.md](../database.md), «Представление данных». Замер показал, что
после перехода на PocketBase он сворачивается в один: движок за
`modernc.org/sqlite` v1.55.0 — версии 3.53.3, `RETURNING` в нём есть, и на трёх
горутинах разом запись получила **ровно одна**.
+4
View File
@@ -90,6 +90,10 @@ pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайн
`modernc.org/sqlite`, а не через `mattn/go-sqlite3`. Требование CGO записано
сегодня свойством стека в `../../CLAUDE.md`, и перевод его снимает.
*Уточнено 2026-08-12:* перевод состоялся, и требования CGO в стеке больше нет —
[../../CLAUDE.md](../../CLAUDE.md), «Стек»: компилятор C нужен только детектору
гонок в гейте.
Бинарник пробника — 33 954 634 байта против 43 498 904 у сегодняшнего приложения
(`go build` без флагов). **Числа не сравнимы напрямую:** в пробнике нет ни бота,
ни клиента SpeechKit, ни клиента Object Storage. Что даст сборка после перевода,
+19 -16
View File
@@ -2,11 +2,12 @@
## Как настроен конвейер
Конвейер ревью прогонялся один раз — 2026-08-11, на изменении
`fix-http-handler-tests`; его триаж лежит в
`openspec/changes/archive/2026-08-11-fix-http-handler-tests/review/triage.md`.
Разделы ниже заполнены наперёд по коду и правятся по итогам прогонов: «Типовые
ложноположительные» первым прогоном уже пользовались.
Артефакты шести прогонов лежат в `openspec/changes/archive/<id>/review/`: у трёх
ранних, начиная с `fix-http-handler-tests` 2026-08-11, это `triage.md`, у трёх
поздних — `report.md`. Сверх них конвейер прогонялся 2026-08-13 на работе, шедшей
без своего изменения openspec; артефакта в архиве у тех прогонов нет, и урожай их
виден только записями журнала ниже. Разделы ниже заведены наперёд по коду
2026-08-11 и с тех пор правятся урожаем прогонов.
Что уже проверяет машина и о чём поэтому спрашивать не нужно — конвенция
[conventions/go-linters.md](conventions/go-linters.md). Вопросы ниже — то, чего
@@ -144,9 +145,10 @@
`createTranscribeJob` — сегодня через него идут оба входа
([architecture.md](architecture.md), «Единые точки проекта»).
- `architecture`: не поехало ли поведение в `architecture.md` вместо спеки —
заведены две capability (`openspec/specs/intake` и `openspec/specs/pipeline`),
и каждая описана частично. Поведение прочих узлов живёт в обзоре под маркерами
долга, а соблазн дописать туда ещё — самый большой.
заведены пять capability (`intake`, `pipeline`, `storage`, `access`,
`toolchain`), и первые две описаны частично. Поведение прочих узлов, включая
приём из Telegram, живёт в обзоре под маркерами долга, а соблазн дописать туда
ещё — самый большой.
- `conventions`: новая колонка правится во всех четырёх местах репозитория
(CLAUDE.md, «Инварианты»).
- `autotests`: покрыт ли изменённый шаг конвейера хоть одним тестом — сегодня
@@ -240,11 +242,11 @@ API и имя не откатываются обратной правкой по
## Журнал дефектов
Верхняя запись найдена конвейером ревью на первом же его прогоне, вторая —
прогоном гейта при заведении канона 2026-08-10, две нижние восстановлены по
истории git тогда же. Три нижние помечены `проскочил`: ревью тогда не было, и
поймать их было некому. У восстановленных нет поля «Чем воспроизведён», и
выдумывать его задним числом нельзя.
Записи новые сверху. `[пойман ревью]` — дефект нашёл прогон конвейера,
`[пойман сканером]` — тест-сканер `internal/archrules`, `[проскочил]` — дефект
уехал в код, и поймать его тогда было некому. Две нижние записи восстановлены по
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
оракул, и выдумывать оракул задним числом нельзя.
## 2026-08-13 — остановка сервиса хоронила конвертируемую запись [пойман ревью]
@@ -297,7 +299,7 @@ API и имя не откатываются обратной правкой по
оценкой «сегодня она не логируется — то есть утечки нет», и оценка была
неверной. Строка лога существовала всё это время, но проза о ней не знала, а
машина прозу не проверяет
- **Что меняем:** чистка перенесена с места употребления на **границу клиента**
- **Что меняем:** чистку перенесли с места употребления на **границу клиента**
`internal/adapter/telegram`, `NewBot`: свой `Do` разворачивает отказ в
первопричину, а подменённый логгер библиотеки вычищает токен из строк длинного
опроса, которые она печатает сама, мимо нашего `slog`. Транспорт бота токена
@@ -352,8 +354,9 @@ API и имя не откатываются обратной правкой по
- **Что меняем:** правило судит по типу приёмника (`analyze-types`,
`httptest.ResponseRecorder.Header` и `.HeaderMap`) и ловит все шесть форм;
проверено мутацией по каждой. Отсюда же строка в
docs/conventions/go-linters.md, «Лестница механизации»: запрет по имени, обходимый лишней строкой, — это ступень
тест-сканера, наряженная запретом
docs/conventions/go-linters.md, «Лестница механизации»: запрет по имени,
обходимый лишней строкой, требует ступени тест-сканера, хотя выглядит запретом
по имени
## 2026-08-12 — закрыли поверхность так, что войти не мог никто [пойман ревью]
+9 -6
View File
@@ -122,8 +122,10 @@ Telegram отправителю.
- **Поверхность самого хранилища.** Вместе с переводом наружу выходят
`/api/collections/...`, `/api/logs`, `/api/backups`, `/api/settings`,
`/api/crons` и панель `/_/`. Правила доступа коллекций оставлены пустыми, то
есть доступны они только владельцу панели; проверено прогоном — записи отдают
`403`, служебные разделы `401`.
есть доступны они только владельцу панели; коды, снятые прогоном,
[database.md](database.md), «Коллекции», норма —
[storage](../openspec/specs/storage/spec.md), «Наружу хранилище отдаёт только
то, что заказано».
Целевой периметр добавляет сюда три вещи, и все три — от новых задач:
@@ -143,9 +145,10 @@ Telegram отправителю.
с фамилией), а не с числовым идентификатором. Имя пользователя Telegram
меняется владельцем в любой момент: список привязан к изменяемому значению.
- **HTTP API** — сессия, заведённая входом через OIDC у Authelia. Предъявляется
кукой `transcriber_session`, живёт семь суток, обесценивается выходом.
кукой `transcriber_session`, обесценивается выходом, срок жизни назначен числом
([database.md](database.md), «Настройки с числовым значением»).
Продление сессии закрыто: с ним предъявитель менял бы своё значение на новое
бессрочно, и семисуточный срок — единственное, чем отзыв доступа у провайдера
бессрочно, и назначенный срок — единственное, чем отзыв доступа у провайдера
доходит до сервиса, — не значил бы ничего.
Предъявленный заголовок `Authorization` принимается тоже — это та же сессия и
та же проверка, но она названа здесь отдельно, потому что это второй способ
@@ -271,8 +274,8 @@ Telegram отправителю.
`…/sendMessage`, `…/getMe`, `…/getUpdates`) и в ссылке на скачивание
(`file.Link(token)`). Сами адреса нигде не логируются, но до 2026-08-13 их
уносил **отказ транспорта**: `*url.Error` встраивает адрес целиком, а отказы
скачивания и отправки пишутся в журнал. Теперь адрес снимается на границе
клиента`internal/adapter/telegram`, `NewBot`: свой `Do` чистит отказ, а
скачивания и отправки пишутся в журнал. Теперь адрес на границе клиента снимает
свой `Do``internal/adapter/telegram`, `NewBot`: он чистит отказ, а
подменённый логгер библиотеки вычищает токен из строк длинного опроса, которые
она печатает сама. Транспорт бота токена больше не получает вовсе: клиента ему
отдают готовым. Правило — [conventions/logging.md](conventions/logging.md),
@@ -8,8 +8,9 @@
//
// Шаги лежат своим каталогом, а не файлом внутри пакета репозитория, и причина
// внешняя: сверка документов ловит изменённый шаг схемы при нетронутом
// `docs/database.md` по префиксу пути (`docs/.docs.json`, ключ `migrations`), а
// префикс наводится только на каталог. Пока шаги лежали файлом, наводить его
// `docs/database.md` по префиксу пути (`.av-dev.toml`, ключ `migrations` секции
// `[docs]`), а префикс наводится только на каталог. Пока шаги лежали файлом,
// наводить его
// было не на что, и проверка молчала на всякой правке схемы.
package migrations
+1 -1
View File
@@ -37,7 +37,7 @@ context: |
первым молча, и заметно это становится в предложении, которое уже написано.
Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
скилл av-dev-code:review, проектная настройка — docs/review.md.
скилл av-dev:code-review, проектная настройка — docs/review.md.
Конвенции кода: механизированное проверяет гейт, прозой остаётся
docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
-3
View File
@@ -1,3 +0,0 @@
{
"tasks": 1
}
+2 -1
View File
@@ -6,7 +6,7 @@
это очередь, и первая строка — то, что делают следующим. Порядок
назначает человек на груминге, машина его не выводит. Одно исключение
производно от типа — сырьё (`research` без раздела «Вопрос»)
стоит в конце: его не берут. Ведётся скиллом `tasks`.
стоит в конце: его не берут. Ведётся скиллом `av-dev:task-track`.
Секция одна — полок домена у проекта нет, и делить очередь на две
значило бы держать два порядка вместо одного.
@@ -98,4 +98,5 @@
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
- [🔬 Шаги гейта, у которых правило может потерять предмет](items/gate-steps-subject-guard.md) — У шага migrations страж предмета есть, у шагов docs, tasks и openspec неизвестно: они зовут чужие скрипты из плагинов, и правило, потерявшее файлы, зеленело бы молча.
- [🧹 Свести шесть расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; четыре из них в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
- [🧹 Свести Purpose спеки pipeline с её же требованиями](items/pipeline-spec-purpose-drift.md) — Преамбула спеки объявляет сознательно неописанными пять требований, которые в ней же и стоят с 2026-08-12: читатель узнаёт границу нормы из раздела, который ей противоречит.
- [🔬 Квота по общему размеру загруженного на пользователя](items/per-user-size-quota.md) — Паспорт и security.md запрещают отказы по квоте пользователю, а заметка владельца просит квоту по умолчанию 5 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас.
+1 -1
View File
@@ -47,7 +47,7 @@
## Критерии приёмки
- Ни одно из шести мест не отвечает на свой вопрос двумя способами. Оракул —
повторный прогон `av-dev-docs:healthcheck`: перечисленные шесть находок не
повторный прогон `av-dev:doc-healthcheck`: перечисленные шесть находок не
возвращаются.
- Провайдер OIDC стоит в таблице внешних зависимостей со своими четырьмя
столбцами отказа, и счёт зависимостей в «Открытых вопросах» сходится с
+3 -3
View File
@@ -11,9 +11,9 @@
кодом 3.
Чего не знаем: ведут ли себя так же `docs.py check`, `tasks.py check` и
`openspec.py check`. Скрипты чужие — они живут в плагинах `av-dev-docs`,
`av-dev-tasks` и `av-dev-code`, и править их в этом репозитории нельзя. Отсюда и
тип записи: способ починки зависит от ответа. Найдётся страж внутри — делать
`openspec.py check`. Скрипты чужие — они живут в плагине `av-dev`, и править их
в этом репозитории нельзя. Отсюда и тип записи: способ починки зависит от
ответа. Найдётся страж внутри — делать
нечего; не найдётся — либо обёртка в `Taskfile.yml` со своей проверкой предмета,
либо разговор с владельцем плагина.
@@ -6,7 +6,7 @@
Шаг заведён 2026-08-13 и проверен мутацией на восьми исходах вручную — правка
уехавшего шага в дереве и в коммите, удаление, переименование, новый шаг, правка
`migrations.go`, отсутствующий ключ в `docs/.docs.json`, каталог без шагов,
`migrations.go`, отсутствующий ключ в `.av-dev.toml`, каталог без шагов,
неразрешимая база диффа. Прогон был разовым: в дереве от него не осталось ничего.
Прецедент рядом. У шага сверки версий Go есть спека
@@ -24,7 +24,7 @@
## Затрагивает
- шаг `migrations` в `Taskfile.yml` — его логика разбора `git diff`;
- ключ `migrations` в `docs/.docs.json` — из него шаг берёт каталог;
- ключ `migrations` секции `[docs]` в `.av-dev.toml` — из него шаг берёт каталог;
- каталог шагов схемы `internal/adapter/repo/pocketbase/migrations/` как предмет
правила;
- возможно — новая capability в `openspec/specs/` и файл проверок рядом с
@@ -36,7 +36,7 @@
временном клоне репозитория: правка файла шага даёт код 1 и называет файл.
- Новый файл шага проверку не роняет, и правка `migrations.go` тоже: строка
`Register` нового шага прибавляется именно там. Оракул — те же два сценария.
- Каталог без единого файла шага и отсутствующий ключ в `docs/.docs.json` дают
- Каталог без единого файла шага и отсутствующий ключ в `.av-dev.toml` дают
код 3, а не тихий ноль. Оракул — два сценария на временном каталоге.
- Проверка сценариев идёт в гейте, а не руками. Оракул — `task gate` красный при
внесённом нарушении шаблона имени файла шага.
@@ -0,0 +1,54 @@
# 🧹 Свести Purpose спеки pipeline с её же требованиями
- **Тип:** chore
- **Категория:** Очередь — Правка одного раздела спеки, но откладывать её значит держать нормативный документ противоречащим себе.
- **Зачем:** Преамбула спеки объявляет сознательно неописанными пять требований, которые в ней же и стоят с 2026-08-12: читатель узнаёт границу нормы из раздела, который ей противоречит.
Нашла сверка документов 2026-08-13. `Purpose` спеки `pipeline` перечисляет как
сознательно неописанные захват задачи и срок его протухания, число попыток,
состояние «мертва» и паузу перед повтором. Ниже в той же спеке эти требования
стоят: их дописало изменение `pocketbase-storage` 2026-08-12, а преамбулу не
поправило.
Не переехала в спеку одна вещь — цепочка переходов
`created``converted``transcribe``done` либо `failed`. Маркер долга в
[architecture.md](../../docs/architecture.md) уже уточнён под это и называет
неперехавшей именно цепочку, так что после правки `Purpose` два документа
сойдутся.
Материал для замены — формулировка из отчёта сверки:
> Описаны: пустой прогон воркера, неделимость захвата и срок его протухания,
> число попыток и состояние «мертва», условность записи результата по признаку
> захвата, нарастающая пауза перед повтором. Сознательно не описаны: цепочка
> переходов `created``converted``transcribe``done` либо `failed`,
> отмена контекста посреди шага, освобождение ресурсов внешних клиентов. Это не
> значит, что такого поведения нет: оно живёт в коде, а требования на него не
> написаны, потому что требование без проверки — предположение, а не норма.
> Первая задача, которая трогает любое из перечисленного, дописывает его сюда.
## Затрагивает
- раздел `Purpose` в `openspec/specs/pipeline/spec.md` — требований спеки правка
не касается, они уже написаны;
- маркер долга о поведении в `docs/architecture.md` — как парная сторона
утверждения о том, что ещё не переехало.
## Критерии приёмки
- `Purpose` не называет неописанным ни одно требование, которое в спеке стоит.
Оракул — построчная сверка перечня из `Purpose` с заголовками `Requirement`
той же спеки: пересечения нет.
- Спека остаётся годной для инструмента. Оракул — `openspec validate --strict`
отрабатывает без отказа.
- Маркер долга в `docs/architecture.md` и `Purpose` называют неперехавшим одно и
то же. Оракул — чтение обоих мест подряд: перечни совпадают.
## Рамки
Правится преамбула, а не требования: поведение сервиса задача не меняет и кода
не трогает. Спека правится изменением openspec своим порядком, а не прямой
правкой файла. Соседняя задача
[context-cancel-in-pipeline](context-cancel-in-pipeline.md) уберёт из перечня
неописанного отмену контекста, когда доедет, — здесь эта строка остаётся на
месте.
+1 -1
View File
@@ -42,7 +42,7 @@
## Рамки
Правится только настройка конвейера в `docs/review.md`. Устав самого конвейера
живёт в плагине `av-dev-code` и этой задачей не трогается: проект вправе
живёт в скилле `av-dev:code-review` и этой задачей не трогается: проект вправе
настраивать свои темы и триггеры, но не переписывать чужой скилл.
Журнал дефектов в том же файле не трогается — записи неизменяемы.