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