diff --git a/.av-dev.toml b/.av-dev.toml new file mode 100644 index 0000000..91a89d8 --- /dev/null +++ b/.av-dev.toml @@ -0,0 +1,12 @@ +# Раскладка av-dev в этом проекте: версия и настройки проверок. +# Файл ведут скиллы плагина, править руками можно — комментарии свои. + +version = 1 # версия раскладки; обратной совместимости нет, есть «приведён» и «нет» + +[docs] +# каталог миграций: по нему docs.py сверяет схему с database.md +migrations = "internal/adapter/repo/pocketbase/migrations" + +[tasks] +# каталог задач от корня репозитория; имена частей — умолчания скрипта +dir = "tasks" diff --git a/CLAUDE.md b/CLAUDE.md index 9af3b14..a59213c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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` зелёный и критерии приёмки проверены поимённо. diff --git a/README.md b/README.md index 7b4f255..f2ab18f 100644 --- a/README.md +++ b/README.md @@ -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). ## Структура проекта diff --git a/Taskfile.yml b/Taskfile.yml index e9e332e..3683bd7 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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=" 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 . diff --git a/docs/.docs.json b/docs/.docs.json deleted file mode 100644 index 0439d82..0000000 --- a/docs/.docs.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "canon": 14, - "migrations": "internal/adapter/repo/pocketbase/migrations" -} diff --git a/docs/adr/README.md b/docs/adr/README.md index b9481e2..9c8af88 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -21,7 +21,10 @@ - Имя файла — `ADR-ГГГГ-ММ-ДД-slug.md`, дата — когда решение реально принято. Слаг **английский по сути, а не транслитом**: `queue-as-table`, не `ochered-tablicej`. Форму имени и слаг проверяет `docs.py check`. -- Записи неизменяемы: передумали — новая запись, старой ставится статус. +- Записи неизменяемы **в решении**: передумали — новая запись, старой ставится + статус. Уточнить прежнюю запись можно только строкой «*Уточнено ГГГГ-ММ-ДД:*» в + разделе «Последствия» и только фактом, который решения не меняет, — например + действующим адресом того, что решение завело. - Активная запись статуса не имеет. Значений два: `заменено на ADR-…` и `устарело`; ставятся полем меты записи — `- **Статус:** …` рядом с датой и источником, а не абзацем в теле. diff --git a/docs/architecture.md b/docs/architecture.md index baf74c3..5373517 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -83,12 +83,13 @@ | Шаги схемы | `internal/adapter/repo/pocketbase/migrations` | Файл на шаг, имя файла — имя шага; там же имена коллекций | | Панель владельца | `internal/adapter/repo/pocketbase`, `panel.go` | Правка задачи в панели проходит те же правила перехода, что и правка из кода | - + Конвейер: `created` → `converted` → `transcribe` → `done` либо `failed`. Три -воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Задача, -исчерпавшая попытки, уходит в `dead` мимо этой цепочки: её переводит туда не шаг, -а тот, кто её захватил. +воркера двигают по одному переходу, каждый опрашивает базу раз в секунду. Что +делает задача, исчерпавшая попытки, нормирует +[pipeline](../openspec/specs/pipeline/spec.md), «Число попыток и состояние +«мертва»». ## Внешние границы и форматы diff --git a/docs/conventions/go-linters.md b/docs/conventions/go-linters.md index f7b9081..de1e128 100644 --- a/docs/conventions/go-linters.md +++ b/docs/conventions/go-linters.md @@ -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 ./...`) | diff --git a/docs/conventions/logging.md b/docs/conventions/logging.md index 56f6c42..0ea30dd 100644 --- a/docs/conventions/logging.md +++ b/docs/conventions/logging.md @@ -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, поэтому чистка на месте употребления закрывала бы один вызов из пяти: diff --git a/docs/conventions/web-ui.md b/docs/conventions/web-ui.md index ce654eb..44c8eb4 100644 --- a/docs/conventions/web-ui.md +++ b/docs/conventions/web-ui.md @@ -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). diff --git a/docs/database.md b/docs/database.md index fd4aed6..70ee25b 100644 --- a/docs/database.md +++ b/docs/database.md @@ -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 секунд | там же | молчащий провайдер иначе держит обработчик возврата открытым | diff --git a/docs/passport.md b/docs/passport.md index 058c858..f58127a 100644 --- a/docs/passport.md +++ b/docs/passport.md @@ -22,7 +22,7 @@ | Владелец сервиса | Загрузить диктофонную запись или видео из семейного архива с телефона и получить текст. Видеть, кто сколько загрузил и во что это обошлось | | Приглашённый пользователь | Войти в приложение через свою учётную запись, загрузить запись, забрать текст, вернуться к ней через месяц. Приложение ставится на телефон; каждый видит только свои записи | | Пользователь Telegram | Отправить боту голосовое сообщение и получить текст ответом. Работает сегодня | -| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Сегодня почти не работает: приём и опрос закрыты сессией OIDC, а своего токена у программы нет — годится только кука, снятая из браузера. Токен приносит `api-tokens` | +| Внешняя программа | Отдать файл по HTTP, представившись своим токеном, и опросить готовность. Сегодня почти не работает: приём и опрос закрыты сессией OIDC, а своего токена у программы нет — годится только чужая сессия, снятая из браузера и предъявленная кукой либо заголовком `Authorization`. Токен приносит `api-tokens` | **Основной вход — приложение**, бот и HTTP API дополняют его. До 2026-08-11 основным был бот, и порядок здесь перевёрнут сознательно: диктофонная запись на diff --git a/docs/research/job-queue.md b/docs/research/job-queue.md index 9179212..ae25694 100644 --- a/docs/research/job-queue.md +++ b/docs/research/job-queue.md @@ -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` в нём есть, и на трёх горутинах разом запись получила **ровно одна**. diff --git a/docs/research/pocketbase.md b/docs/research/pocketbase.md index 6de6d5a..a6a6127 100644 --- a/docs/research/pocketbase.md +++ b/docs/research/pocketbase.md @@ -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. Что даст сборка после перевода, diff --git a/docs/review.md b/docs/review.md index 3bbac45..4adf265 100644 --- a/docs/review.md +++ b/docs/review.md @@ -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//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 — закрыли поверхность так, что войти не мог никто [пойман ревью] diff --git a/docs/security.md b/docs/security.md index b603084..da0bcd0 100644 --- a/docs/security.md +++ b/docs/security.md @@ -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), diff --git a/internal/adapter/repo/pocketbase/migrations/migrations.go b/internal/adapter/repo/pocketbase/migrations/migrations.go index dfaf3cd..b9b152a 100644 --- a/internal/adapter/repo/pocketbase/migrations/migrations.go +++ b/internal/adapter/repo/pocketbase/migrations/migrations.go @@ -8,8 +8,9 @@ // // Шаги лежат своим каталогом, а не файлом внутри пакета репозитория, и причина // внешняя: сверка документов ловит изменённый шаг схемы при нетронутом -// `docs/database.md` по префиксу пути (`docs/.docs.json`, ключ `migrations`), а -// префикс наводится только на каталог. Пока шаги лежали файлом, наводить его +// `docs/database.md` по префиксу пути (`.av-dev.toml`, ключ `migrations` секции +// `[docs]`), а префикс наводится только на каталог. Пока шаги лежали файлом, +// наводить его // было не на что, и проверка молчала на всякой правке схемы. package migrations diff --git a/openspec/config.yaml b/openspec/config.yaml index 30164c2..ddd4728 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -37,7 +37,7 @@ context: | первым молча, и заметно это становится в предложении, которое уже написано. Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом - скилл av-dev-code:review, проектная настройка — docs/review.md. + скилл av-dev:code-review, проектная настройка — docs/review.md. Конвенции кода: механизированное проверяет гейт, прозой остаётся docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не diff --git a/tasks/.tasks.json b/tasks/.tasks.json deleted file mode 100644 index f2fb1b5..0000000 --- a/tasks/.tasks.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "tasks": 1 -} diff --git a/tasks/BACKLOG.md b/tasks/BACKLOG.md index 6001530..035439b 100644 --- a/tasks/BACKLOG.md +++ b/tasks/BACKLOG.md @@ -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 ГБ — открытое противоречие с границей домена, которое владелец решил не разбирать сейчас. diff --git a/tasks/items/docs-consistency-2026-08-13.md b/tasks/items/docs-consistency-2026-08-13.md index 42d3395..7cc1af6 100644 --- a/tasks/items/docs-consistency-2026-08-13.md +++ b/tasks/items/docs-consistency-2026-08-13.md @@ -47,7 +47,7 @@ ## Критерии приёмки - Ни одно из шести мест не отвечает на свой вопрос двумя способами. Оракул — - повторный прогон `av-dev-docs:healthcheck`: перечисленные шесть находок не + повторный прогон `av-dev:doc-healthcheck`: перечисленные шесть находок не возвращаются. - Провайдер OIDC стоит в таблице внешних зависимостей со своими четырьмя столбцами отказа, и счёт зависимостей в «Открытых вопросах» сходится с diff --git a/tasks/items/gate-steps-subject-guard.md b/tasks/items/gate-steps-subject-guard.md index 4bdd8ed..10d6999 100644 --- a/tasks/items/gate-steps-subject-guard.md +++ b/tasks/items/gate-steps-subject-guard.md @@ -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` со своей проверкой предмета, либо разговор с владельцем плагина. diff --git a/tasks/items/migrations-step-norm-and-tests.md b/tasks/items/migrations-step-norm-and-tests.md index 7cc5afd..7974162 100644 --- a/tasks/items/migrations-step-norm-and-tests.md +++ b/tasks/items/migrations-step-norm-and-tests.md @@ -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` красный при внесённом нарушении шаблона имени файла шага. diff --git a/tasks/items/pipeline-spec-purpose-drift.md b/tasks/items/pipeline-spec-purpose-drift.md new file mode 100644 index 0000000..edddb3b --- /dev/null +++ b/tasks/items/pipeline-spec-purpose-drift.md @@ -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) уберёт из перечня +неописанного отмену контекста, когда доедет, — здесь эта строка остаётся на +месте. diff --git a/tasks/items/review-config-from-go-upgrade.md b/tasks/items/review-config-from-go-upgrade.md index 9fa48d7..2246b8c 100644 --- a/tasks/items/review-config-from-go-upgrade.md +++ b/tasks/items/review-config-from-go-upgrade.md @@ -42,7 +42,7 @@ ## Рамки Правится только настройка конвейера в `docs/review.md`. Устав самого конвейера -живёт в плагине `av-dev-code` и этой задачей не трогается: проект вправе +живёт в скилле `av-dev:code-review` и этой задачей не трогается: проект вправе настраивать свои темы и триггеры, но не переписывать чужой скилл. Журнал дефектов в том же файле не трогается — записи неизменяемы.