docs: документы подняты на канон 7

- review.md переведён на словарь меток: вопросы адресованы темам, триггеры
  профиля стали триггерами метки в три списка, quick/standard/wide → small/
  medium/large, профиль deep упразднён
- openspec/config.yaml переписан по канонической форме: адреса passport и
  CLAUDE.md вместо пересказа правил ревью и конвенций
- разобраны находки doc-consistency и doc-code-drift: исключение инварианта
  сверено со спеками, единая точка времени и таблица classifyErr дополнены,
  MaxTorrentSize получил дом в database.md
This commit is contained in:
av
2026-08-07 13:01:23 +03:00
parent b450ab1fd5
commit b879c049ea
11 changed files with 174 additions and 143 deletions
+23 -17
View File
@@ -40,8 +40,12 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module
последней копии там выключен сознательно последней копии там выключен сознательно
([state-reconciliation](openspec/specs/state-reconciliation/spec.md)); ([state-reconciliation](openspec/specs/state-reconciliation/spec.md));
(2) уборка воркером **собственного** торрента, добавленного этим же `add` (2) уборка воркером **собственного** торрента, добавленного этим же `add`
секундами ранее, когда задачу отменили в окне после `add` секундами ранее, когда закрытие **любым** путём (`Cancel` или `Dismiss`)
([download-tracking](openspec/specs/download-tracking/spec.md)). Всё увело задачу из `catched` в окне после `add` — уборка привязана к состоянию,
а не к команде; признак «своё» даёт подтверждённое отсутствие инфохэша
непосредственно перед `add`
([download-tracking](openspec/specs/download-tracking/spec.md),
[state-reconciliation](openspec/specs/state-reconciliation/spec.md)). Всё
остальное под `paths.downloads` — по-прежнему `critical`. остальное под `paths.downloads` — по-прежнему `critical`.
- **Последняя копия не снимается** — `Undo` отклоняется целиком, если у цели не - **Последняя копия не снимается** — `Undo` отклоняется целиком, если у цели не
осталось других жёстких ссылок (`nlink <= 1`) или исходного файла уже нет. осталось других жёстких ссылок (`nlink <= 1`) или исходного файла уже нет.
@@ -108,7 +112,8 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module
- `govulncheck` даёт `WARN`, а не `FAIL`: находка тут — состояние - `govulncheck` даёт `WARN`, а не `FAIL`: находка тут — состояние
зависимостей, а не диффа. Разбирает агент ревью по трассам вызовов. зависимостей, а не диффа. Разбирает агент ревью по трассам вызовов.
- `-race` без gcc уходит в `SKIP` с явным «гонки НЕ проверены» — тогда их - `-race` без gcc уходит в `SKIP` с явным «гонки НЕ проверены» — тогда их
проверяет проход `ops` рассуждением, и это идёт в границы покрытия. проверяет рассуждением тема `operations` ([docs/review.md](docs/review.md)
→ «Вопросы по темам»), и это идёт в границы покрытия.
- Ничего не гоняется против **живого** qBittorrent, LLM и метабаз: - Ничего не гоняется против **живого** qBittorrent, LLM и метабаз:
интеграционные тесты за env-гейтами, запускает человек вручную. интеграционные тесты за env-гейтами, запускает человек вручную.
- Качество распознавания гейтом не проверяется вовсе и проверяться не будет: - Качество распознавания гейтом не проверяется вовсе и проверяться не будет:
@@ -140,7 +145,8 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module
- **Необратимое** (спрашивается у человека всегда): всё, что пишет в - **Необратимое** (спрашивается у человека всегда): всё, что пишет в
`paths.downloads` или удаляет оттуда; удаление раздачи из qBittorrent вместе `paths.downloads` или удаляет оттуда; удаление раздачи из qBittorrent вместе
с файлами (`Delete`) — кроме уборки собственного, только что добавленного с файлами (`Delete`) — кроме уборки собственного, только что добавленного
торрента при отмене (см. исключения инварианта выше); снятие последней копии торрента, когда закрытие любым путём увело задачу из `catched` (см.
исключения инварианта выше); снятие последней копии
данных; правка уже применённой миграции; `git push --force`; удаление или данных; правка уже применённой миграции; `git push --force`; удаление или
перезапись файла в библиотеке Jellyfin, которого мы не создавали. перезапись файла в библиотеке Jellyfin, которого мы не создавали.
- **Общий станок** — покрасневший `task gate` на `master` врывается в - **Общий станок** — покрасневший `task gate` на `master` врывается в
@@ -155,30 +161,30 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module
(CLI `openspec`, v1.x). Сначала спецификация — потом код. (CLI `openspec`, v1.x). Сначала спецификация — потом код.
- `openspec/specs/<capability>/spec.md`**нормативный дом поведения**: что - `openspec/specs/<capability>/spec.md`**нормативный дом поведения**: что
система делает сейчас. Capability — это поведение или домен (`ingest`, система делает сейчас. Capability — это поведение или домен системы, а не
`recognition`, `file-layout`, `review`, `notifications`), а не пакет кода. пакет кода.
- `openspec/changes/<id>/` — предлагаемое изменение: `proposal.md`, `design.md` - `openspec/changes/<id>/` — предлагаемое изменение: `proposal.md`, `design.md`
(для нетривиальных), дельта-спеки (`ADDED`/`MODIFIED`/`REMOVED Requirements`), (для нетривиальных), дельта-спеки (`ADDED`/`MODIFIED`/`REMOVED Requirements`),
`tasks.md`. После реализации change архивируется в `tasks.md`. После реализации change архивируется в
`openspec/changes/archive/`, дельты вливаются в `openspec/specs/`. `openspec/changes/archive/`, дельты вливаются в `openspec/specs/`.
- `openspec/config.yaml` — только нужды генерации артефактов: язык, правила - `openspec/config.yaml` — только нужды генерации артефактов: язык, правила
именования capability, придирки валидатора. именования capability, придирки валидатора — плюс адреса документов канона.
Пересказа этих документов там нет: второй дом факта расходится молча.
Поток работы — через слэш-команды `opsx:*`: `opsx:explore` (продумать), Поток работы — через слэш-команды `opsx:*`: `opsx:explore` (продумать),
`opsx:propose` (завести change), `opsx:apply` (реализовать tasks), `opsx:propose` (завести change), `opsx:apply` (реализовать tasks),
`opsx:sync`/`opsx:archive` (влить и архивировать). `opsx:sync`/`opsx:archive` (влить и архивировать).
Правила спек: Правила спек — язык, именование capability и придирки валидатора — живут в
[openspec/config.yaml](openspec/config.yaml) (`context` и `rules`), оттуда их
читает порождение артефактов; здесь не дублируются. Перед коммитом change —
`openspec validate --strict`.
- Каждое `### Requirement` ОБЯЗАНО содержать литерал `SHALL` или `MUST` Ревью — два чекпоинта: ревью дизайна на предложении (после design/specs, ДО
иначе `openspec validate` падает. кода) и ревью изменения после apply, до archive. Состав обоих выбирается по
- Структурные заголовки и ключевые слова — английские (`### Requirement:`, метке задачи (`small` / `medium` / `large`), которую разметка ставит один раз
`#### Scenario:`, `GIVEN/WHEN/THEN`, RFC 2119), остальной текст — русский. после propose. Настройка конвейера под проект и журнал дефектов —
- `openspec validate --strict` перед коммитом change. [docs/review.md](docs/review.md).
Ревью — два чекпоинта: профиль `design` на предложении (после design/specs, ДО
кода) и ревью изменения после apply, до archive. Настройка конвейера под проект
и журнал дефектов — [docs/review.md](docs/review.md).
## Документация ## Документация
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"canon": 4, "canon": 7,
"migrations": "internal/store/migrations" "migrations": "internal/store/migrations"
} }
+1 -1
View File
@@ -106,7 +106,7 @@ retry / delete / dismiss) идут командами к `worker`.
| Что | Где | | Что | Где |
| --- | --- | | --- | --- |
| Время | `store.Now()` — единственный источник меток времени в данных, всегда UTC; формат хранения — RFC 3339. Вторая санкционированная точка wall-clock — timestamp-часть ULID в `ident.NewID` (исключение в `.golangci.yml`) | | Время | `store.Now()` — единственный источник меток времени в данных, всегда UTC; формат хранения — RFC 3339. Вторая санкционированная точка wall-clock — timestamp-часть ULID в `ident.NewID` (исключение `^internal/(ident\|store)/` в `.golangci.yml`). Отдельно от меток в данных стоят замеры длительности: `cmd/jellybit` исключён из `forbidigo` целиком (правило `^cmd/`), плюс точечные `//nolint:forbidigo` в `internal/logging/ext.go` и `internal/httpapi/httpapi.go` |
| Идентификаторы | `internal/ident` — генерация и нормализация ULID; `ident.Parse` на каждой входной границе | | Идентификаторы | `internal/ident` — генерация и нормализация ULID; `ident.Parse` на каждой входной границе |
| Целевые имена и превью раскладки | `internal/naming` — одна логика для превью в UI и для реального применения | | Целевые имена и превью раскладки | `internal/naming` — одна логика для превью в UI и для реального применения |
| Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь | | Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь |
+8 -6
View File
@@ -37,18 +37,20 @@
```toml ```toml
[worker] [worker]
poll_interval = "5s" # как часто опрашивать qBittorrent; Go-duration (s/m/h) poll_interval = "<duration>" # как часто опрашивать qBittorrent; Go-duration (s/m/h)
magnet_timeout = "24h" # ждать метаданные magnet не дольше; Go-duration magnet_timeout = "<duration>" # ждать метаданные magnet не дольше; Go-duration
source_missing_threshold = 3 # тиков сверки без раздачи, чтобы счесть источник удалённым source_missing_threshold = <N> # тиков поллинга без раздачи, чтобы счесть источник удалённым
[recognition] [recognition]
auto_confidence_threshold = 0.85 # порог авто-раскладки без ревью; доля 0.01.0 auto_confidence_threshold = <0.01.0> # порог авто-раскладки без ревью; доля
[llm] [llm]
max_retries = 3 # попыток получить валидный ответ LLM; целое ≥ 0 max_retries = <N> # попыток получить валидный ответ LLM; целое ≥ 0
``` ```
Значения в примере — иллюстрация формы комментария. Действующие умолчания и их Значения намеренно заменены плейсхолдерами: предмет конвенции — форма
комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и
начинают врать молча при смене умолчания. Действующие умолчания и их
смысл живут одним домом — таблица «Настройки с числовым значением» в смысл живут одним домом — таблица «Настройки с числовым значением» в
[../database.md](../database.md); `config.example.toml` — источник истины по [../database.md](../database.md); `config.example.toml` — источник истины по
составу полей. составу полей.
+3
View File
@@ -82,7 +82,10 @@ jellybit — **приложение, а не библиотека**: внешн
|---|---|---| |---|---|---|
| `store.ErrNotFound` | 404 | «не найдено» | | `store.ErrNotFound` | 404 | «не найдено» |
| `magnet.ErrNotMagnet` / `torrent.ErrNotTorrent` | 400 | «некорректный источник» | | `magnet.ErrNotMagnet` / `torrent.ErrNotTorrent` | 400 | «некорректный источник» |
| `ingest.ErrTorrentTooLarge` (файл больше лимита) | 400 | «файл .torrent слишком большой» |
| `worker.ErrInvalidInput` (промах ввода команды) | 400 | «некорректный ввод» | | `worker.ErrInvalidInput` (промах ввода команды) | 400 | «некорректный ввод» |
| `errManualSource` (ручной ввод источника, локальный sentinel `httpapi`) | 400 | текст самой ошибки |
| `errInvalidCandidate` (выбран несуществующий кандидат, локальный sentinel `httpapi`) | 400 | текст самой ошибки |
| `worker.ErrNotReady` (источник ещё качается) | 409 | «торрент ещё качается…» | | `worker.ErrNotReady` (источник ещё качается) | 409 | «торрент ещё качается…» |
| `layout.ErrCollision` (цель занята, ушло в review) | 409 | «целевой файл уже существует…» | | `layout.ErrCollision` (цель занята, ушло в review) | 409 | «целевой файл уже существует…» |
| `worker.ErrConflict` (операция недопустима сейчас) | 409 | «действие недоступно в текущем состоянии» | | `worker.ErrConflict` (операция недопустима сейчас) | 409 | «действие недоступно в текущем состоянии» |
+6
View File
@@ -207,5 +207,11 @@ erDiagram
| `[llm].timeout` / `max_retries` | `120s` / `3` | каждая попытка порождает строку `recognition` с сырым ответом | | `[llm].timeout` / `max_retries` | `120s` / `3` | каждая попытка порождает строку `recognition` с сырым ответом |
| `[metadata.*].timeout` | `10s` | таймаут запроса к метабазе | | `[metadata.*].timeout` | `10s` | таймаут запроса к метабазе |
Пределы, зашитые константой кода, а не полем конфига:
| Константа | Значение | Что означает |
| --- | --- | --- |
| `ingest.MaxTorrentSize` | `8 MiB` | предел размера принимаемого `.torrent`; проверяется **до** разбора, поэтому bencode-аллокации на эту величину не масштабируются (см. [research/torrent-bencode-limits.md](research/torrent-bencode-limits.md)) |
**Ретеншена нет ни у одной таблицы**, лимита на размер тела ответа LLM нет, **Ретеншена нет ни у одной таблицы**, лимита на размер тела ответа LLM нет,
кэша метабаз нет — всё три пункта в беклоге. кэша метабаз нет — всё три пункта в беклоге.
+3 -2
View File
@@ -76,8 +76,9 @@ func main() {
- **Усиление огромное, и наш лимит размера от него не защищает.** 36 байт входа - **Усиление огромное, и наш лимит размера от него не защищает.** 36 байт входа
дают 128 MiB транзиентной аллокации — это ×3.7 млн, а не «крафт-8 MiB даёт дают 128 MiB транзиентной аллокации — это ×3.7 млн, а не «крафт-8 MiB даёт
128 MiB», как предполагала исходная нить ревью. Предел приёма 128 MiB», как предполагала исходная нить ревью. Предел приёма
`ingest.MaxTorrentSize` (8 MiB) стоит **до** разбора и на эту величину не `ingest.MaxTorrentSize` ([database.md](../database.md) → «Настройки с
влияет вовсе: атакующему хватает трёх десятков байт. числовым значением») стоит **до** разбора и на эту величину не влияет вовсе:
атакующему хватает трёх десятков байт.
- **Но аллокация ограничена сверху и одна на попытку разбора.** Выше потолка - **Но аллокация ограничена сверху и одна на попытку разбора.** Выше потолка
библиотека отказывает, не аллоцировав ничего; ниже — аллоцирует ровно библиотека отказывает, не аллоцировав ничего; ниже — аллоцирует ровно
объявленное и падает на чтении, обрывая разбор целиком. Дочитать несколько объявленное и падает на чтении, обрывая разбор целиком. Дочитать несколько
+91 -75
View File
@@ -1,7 +1,7 @@
# Ревью: настройка и журнал # Ревью: настройка и журнал
Проектная часть конвейера ревью: чем jellybit отличается от абстрактного Проектная часть конвейера ревью: чем jellybit отличается от абстрактного
Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (профили, Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (метки,
стадии, контракт находок) живёт в скилле, а не здесь. стадии, контракт находок) живёт в скилле, а не здесь.
## Как настроен конвейер ## Как настроен конвейер
@@ -93,126 +93,142 @@ Go-сервиса и что здесь уже проскакивало. Устр
`original_title` заполняется всегда и при неуверенности дублирует `title` `original_title` заполняется всегда и при неуверенности дублирует `title`
это контракт capability `recognition`, а не недосмотр. это контракт capability `recognition`, а не недосмотр.
### Вопросы к проходам ### Вопросы по темам
Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Журнал дефектов пока пуст, Форма: `<тема>: <вопрос> (<провенанс>)`. Адресуется теме, а не имени прохода:
поэтому провенанс у всех пунктов — инвариант или ADR, а не пойманный случай; проход переезжает между метками и упраздняется, тема переезд переживает. Задаёт
по мере накопления журнала список должен смещаться в сторону реальных промахов. вопрос тот, кто закрывает тему на текущем прогоне.
- `adversary`: можно ли, управляя только именами файлов в раздаче и текстом - `security`: можно ли, управляя только именами файлов в раздаче и текстом
контекста, добиться целевого пути вне `paths.movies`/`series` — включая путь контекста, добиться целевого пути вне `paths.movies`/`series` — включая путь
через юникод, длину сверх лимита ФС и коллизию после нормализации? через юникод, длину сверх лимита ФС и коллизию после нормализации?
(инвариант «целевой путь строго под библиотекой», [security.md](security.md)) (инвариант «целевой путь строго под библиотекой», [security.md](security.md))
- `adversary`: есть ли последовательность команд, после которой снимается - `security`: есть ли последовательность команд, после которой снимается
**последняя** копия данных — с учётом `superseded`-ссылок и гонки со сверкой? **последняя** копия данных — с учётом `superseded`-ссылок и гонки со сверкой?
(инвариант «источник неприкосновенен») (инвариант «источник неприкосновенен»)
- `adversary`: что даёт крафт-магнет с чужим или подставным инфохэшем — - `security`: что даёт крафт-магнет с чужим или подставным инфохэшем —
присоединение к чужой активной загрузке, отравление владения? присоединение к чужой активной загрузке, отравление владения?
(открытая задача про идентичность инфохэшей) (открытая задача про идентичность инфохэшей)
- `adversary`: где признак «это наше» снимается с одной сущности, а действие - `security`: где признак «это наше» снимается с одной сущности, а действие
применяется к другой — присутствие раздачи в qBittorrent против байтов на применяется к другой — присутствие раздачи в qBittorrent против байтов на
диске, запись в БД против файла, инфохэш против содержимого? (журнал, диске, запись в БД против файла, инфохэш против содержимого? (журнал,
2026-08-06: уборка своего торрента сносила чужие файлы) 2026-08-06: уборка своего торрента сносила чужие файлы)
- `ops`: что делает эта ветка, когда qBittorrent недоступен несколько минут - `operations`: что делает эта ветка, когда qBittorrent недоступен несколько
подряд — сколько ERROR-строк в секунду и меняется ли состояние задач? минут подряд — сколько ERROR-строк в секунду и меняется ли состояние задач?
(задача про ERROR-шторм фоновых циклов) (задача про ERROR-шторм фоновых циклов)
- `ops`: как это ведёт себя при сотне загрузок в базе и десятках тысяч - `operations`: как это ведёт себя при сотне загрузок в базе и десятках тысяч
`file_link` — есть ли запрос без индекса и полный проход по таблице? `file_link` — есть ли запрос без индекса и полный проход по таблице?
(задача про масштаб 100/1000, [database.md](database.md) → «Настройки») (задача про масштаб 100/1000, [database.md](database.md) → «Настройки»)
- `ops`: что остаётся на диске и в базе, если процесс убит посреди раскладки - `operations`: что остаётся на диске и в базе, если процесс убит посреди
батча? (состояние `linking` и его восстановление) раскладки батча? (состояние `linking` и его восстановление)
- `code`: логирующий чекпоинт один на операцию — или ошибка залогирована и - `conventions`: логирующий чекпоинт один на операцию — или ошибка залогирована
возвращена вверх, где залогирована снова? и возвращена вверх, где залогирована снова?
([conventions/logging.md](conventions/logging.md)) ([conventions/logging.md](conventions/logging.md))
- `code`: новое поле конфига появилось в `config.example.toml` с описанием - `conventions`: новое поле конфига появилось в `config.example.toml` с
назначения, диапазона и единиц? ([conventions/config.md](conventions/config.md)) описанием назначения, диапазона и единиц?
- `specs`: не завелось ли поведение, которого спека не заказывала — тихий ([conventions/config.md](conventions/config.md))
- `requirements`: не завелось ли поведение, которого спека не заказывала — тихий
дефолт, проглоченная ошибка, ретрай «на всякий случай», отброшенное поле? дефолт, проглоченная ошибка, ретрай «на всякий случай», отброшенное поле?
- `architecture`: не появился ли второй способ делать то, что уже делается — - `architecture`: не появился ли второй способ делать то, что уже делается —
второе место, где генерится время или id, второй парсер источника, вторая второе место, где генерится время или id, второй парсер источника, вторая
логика целевых имён мимо `naming`? логика целевых имён мимо `naming`?
([architecture.md](architecture.md) → «Единые точки проекта») ([architecture.md](architecture.md) → «Единые точки проекта»)
### Триггеры профиля ### Триггеры метки
Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `standard`; Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `medium`;
миграция схемы и публичный контракт ступень **не** поднимают: их проверяют миграция схемы и публичный контракт метку **не** поднимают: их проверяют
проходы, которые в `standard` и так есть. проходы, которые в `medium` и так есть. Списка три: два поднимают до `large`,
по одному на ось, третий опускает до `small`.
**Новым понятием или структурной единицей здесь считается** (→ `wide`): новый **Крупное здесь** — про объём, сколько узлов и слоёв трогает изменение:
пакет `internal/*`; новая capability в `openspec/specs/`; новый транспорт приёма
или уведомлений рядом с REST, веб-UI, ботом и CLI; новый провайдер метабазы за
существующим интерфейсом; новое состояние в графе переходов загрузки; перенос
ответственности между `worker`, `recognition`, `layout` и `store`.
**Правила идентичности, слияния и разбора живут здесь** (→ `deep`): владение - перенос ответственности между `worker`, `recognition`, `layout` и `store`;
раздачей по инфохэшу и сверка с qBittorrent (`ident`, `internal/store`, - новое состояние в графе переходов загрузки: оно тянет за собой воркер, спеку,
отображение в веб-UI и боте и восстановление после рестарта;
- правка, идущая насквозь по цепочке приём → распознавание → раскладка;
- новый провайдер метабазы за существующим интерфейсом: клиент, поле конфига с
образцом, слияние полей кандидата, ветка «провайдера нет».
**Незнакомое здесь** — про форму решения, которую предстоит нащупать по ходу:
- новый пакет `internal/*` или новая capability в `openspec/specs/`;
- новый транспорт приёма или уведомлений рядом с REST, веб-UI, ботом и CLI;
- заводится или меняется **правило идентичности, слияния или разбора**: ключ
владения раздачей и сверка с qBittorrent (`ident`, `internal/store`,
`state-reconciliation`); построение целевых путей и санитизация имён `state-reconciliation`); построение целевых путей и санитизация имён
(`internal/layout`, `naming`); разбор недоверенного входа — bencode, magnet, (`internal/layout`, `naming`); новый вид входа или новая ветка неоднозначности
текст контекста, ответ LLM (`internal/torrent`, `internal/magnet`, у разбора недоверенного — bencode, magnet, текст контекста, ответ LLM
`internal/tgbot/parse.go`, разбор ответа модели); выбор кандидата метабазы и (`internal/torrent`, `internal/magnet`, `internal/tgbot/parse.go`); новый
слияние его полей с догадкой LLM (`internal/metadata`, `metadata-match`); источник или новый победитель при конфликте в слиянии кандидата метабазы
merge-раскладка при повторном добавлении раздачи. (`internal/metadata`, `metadata-match`); merge-раскладка при повторном
добавлении раздачи.
- «Поведение, видимое снаружи» здесь включает **тексты и карточки Telegram** - «Поведение, видимое снаружи» здесь включает **тексты и карточки Telegram**
для единственного пользователя это и есть интерфейс. для единственного пользователя это и есть интерфейс.
**Место из списка ступень не поднимает — поднимает правило.** Перечни выше **Место из перечня метку не поднимает — поднимает правило.** Перечни выше
отвечают «здесь такие правила водятся», а не «любая правка здесь идёт в `deep`». отвечают «здесь такие правила водятся», а не «любая правка здесь идёт в
Ступень поднимает то, что даёт работу новому проходу: заводится ключ сравнения `large`». Метку поднимает то, что даёт работу новому проходу: заводится ключ
или меняется его состав; у разбора появляется новый вид входа или новая ветка сравнения или меняется его состав; у разбора появляется новый вид входа или
неоднозначности; в слияние добавляется источник или меняется победитель при новая ветка неоднозначности; в слияние добавляется источник или меняется
конфликте. победитель при конфликте.
**Отсекающие условия — проверяются до выбора профиля, любое сработавшее держит **Мелкое здесь** — опускает до `small`. Перечень закрытый, каждый пункт
ступень внизу.** Перечень закрытый, каждый пункт проверяется взглядом на дифф и проверяется взглядом на дифф и дельта-спеку, любое сработавшее держит метку
дельта-спеку: внизу:
- **дельта-спека называет исход поимённо.** Независимая реализация окупается - **дельта-спека называет исход поимённо** — сценарий уже говорит, что даёт
выбором, которого спека не сделала. Если сценарий уже говорит, что даёт вырожденный вход, и решать в коде нечего;
вырожденный вход, реализация повторит спеку, и дифф покажет расхождение в
форме, а не в решении;
- **новых сценариев в дельта-спеке нет** — изменение уточняет уже описанное - **новых сценариев в дельта-спеке нет** — изменение уточняет уже описанное
поведение, а не заказывает новое; поведение, а не заказывает новое;
- **правка сообщения, комментария, записи журнала, имени или теста** в узле из - **правка сообщения, комментария, записи журнала, имени или теста** в узле из
перечня; перечней выше;
- **сужение уже существующей нормализации** без нового вида входа: вход остался - **сужение уже существующей нормализации** без нового вида входа: вход остался
тот же, изменился исход на одном его значении. тот же, изменился исход на одном его значении.
**Ориентир частоты.** `standard` закрывает большинство задач, `wide` — редкий Отрицательный тест поверх перечня: что после мерджа не откатывается обратной
случай, `deep` — исключение на крупной функциональности, а не на уборке. Задача правкой — миграция, формат на диске, публичный контракт, имя, — **не** `small`,
типа `chore` или `bugfix`, собранная из нитей прошлого ревью, идёт в `quick` или каким бы маленьким ни был дифф.
`standard`, даже когда трогает файл из перечня `deep`. Верхняя ступень чаще одной
задачи на спринт означает ошибку в критерии, а не спринт из сложных задач. **Ориентир частоты.** `medium` закрывает большинство задач, `large` рассчитана
на 5–10% и приходится на крупную функциональность, а не на уборку: задача типа
`chore` или `fix`, собранная из нитей прошлого ревью, идёт в `small` или
`medium`, даже когда трогает файл из перечней выше. `large` чаще одной задачи на
спринт означает ошибку в критерии, а не спринт из сложных задач.
### Недоступно проверке ### Недоступно проверке
**Не проверит ни один проход** — принципиальная граница, по факту промаха не **Не проверит ни один проход** — принципиальная граница, по факту промаха не
пересматривается. пересматривается.
- История инцидентов на umbar и то, что уже ломалось в проде. - `operations`: история инцидентов на umbar и то, что уже ломалось в проде.
- Поведение таблицы SQLite под реальным объёмом и профилем нагрузки: реального - `operations`: поведение таблицы SQLite под реальным объёмом и профилем
профиля нет ни у кого, кроме сервера. нагрузки — реального профиля нет ни у кого, кроме сервера.
- Завязка внешних потребителей (Jellyfin, закладки, чужие ссылки) на текущее - `architecture`: завязка внешних потребителей (Jellyfin, закладки, чужие
поведение. ссылки) на текущее поведение.
- Качество распознавания как таковое: правильно ли LLM определил фильм — вопрос - `architecture`: суждение «этой функциональности не должно существовать».
тюнинга модели и промпта, а не ревью кода. Размеченный корпус, по которому это - `requirements`: качество распознавания как таковое — правильно ли LLM
можно было бы судить числом, решено не собирать (`tasks/REJECTED.md`, определил фильм. Это вопрос тюнинга модели и промпта, а не ревью кода;
2026-08-06). размеченный корпус, по которому это можно было бы судить числом, решено не
- Суждение «этой функциональности не должно существовать». собирать (`tasks/REJECTED.md`, 2026-08-06).
**Перестали проверять сознательно** — пересматривается первым, как только **Перестали проверять сознательно** — пересматривается первым, как только
что-то проскочило. что-то проскочило.
- **Идиоматичность Go — с 2026-08-04.** Проектный проход `idiom` (поимённая - `conventions`: **идиоматичность Go — с 2026-08-04.** Проектный проход `idiom`
сверка с положениями Effective Go, Go Code Review Comments, стайлгайдов Uber (поимённая сверка с положениями Effective Go, Go Code Review Comments,
и Google) удалён вместе с проектными копиями агентов при переезде на плагин стайлгайдов Uber и Google) упразднён вместе с переездом конвейера в плагин
`av-dev-pipeline`, который этот проход упразднил. Способные части переселены: ([ADR-2026-08-04-review-pipeline-to-plugin](adr/ADR-2026-08-04-review-pipeline-to-plugin.md));
эксперимент против поведения библиотеки и драйвера — в `ops`, «не изобретаем способные части переселены — в тему `operations` (эксперимент против
ли то, что уже есть в библиотеке» — в `architecture`. **Различение поведения библиотеки и драйвера) и в `architecture` («не изобретаем ли то,
«идиоматично против распространено» теперь не спрашивает никто.** Класс что уже есть в библиотеке»). **Различение «идиоматично против
обратимый: портит форму кода, не данные. Пересмотр — задача распространено» теперь не спрашивает никто.** Класс обратимый: портит форму
`quality-review-agents`. кода, не данные. Пересмотр — задача `quality-review-agents`.
- `security`, `operations`, `architecture`: на метках `small` и `medium` не
проверяется ничто, требующее запуска, — построенных путей атаки, замеров и
эксплуатационного постмортема там нет по устройству конвейера. Их даёт только
`large`, а она приходится на 5–10% задач.
## Журнал дефектов ## Журнал дефектов
@@ -272,7 +288,7 @@ merge-раскладка при повторном добавлении разд
проход не спрашивал про **асимметрию признака владения** — признак снимается проход не спрашивал про **асимметрию признака владения** — признак снимается
с одной сущности (запись в qBittorrent), а действие применяется к другой с одной сущности (запись в qBittorrent), а действие применяется к другой
(байты на диске). Враждебный проход до этой задачи на данном коде не гонялся. (байты на диске). Враждебный проход до этой задачи на данном коде не гонялся.
- **Что меняем:** вопрос `adversary` в разделе выше дополнен пунктом про - **Что меняем:** в «Вопросы по темам» добавлен вопрос темы `security` про
асимметрию признака владения. Сам дефект — задачей в беклоге, кандидат асимметрию признака владения. Сам дефект — задачей в беклоге, кандидат
`critical`; спека `state-reconciliation` в том же изменении перестала `critical`; спека `state-reconciliation` в том же изменении перестала
утверждать, что уборка «данных пользователя не касается». утверждать, что уборка «данных пользователя не касается».
@@ -296,7 +312,7 @@ merge-раскладка при повторном добавлении разд
В дереве остался табличный `TestParseNoNameSentinelDropped`. В дереве остался табличный `TestParseNoNameSentinelDropped`.
- **Почему не поймали раньше:** ловить было нечему — дефект внесён этим же - **Почему не поймали раньше:** ловить было нечему — дефект внесён этим же
изменением и пойман тем же прогоном. Отмечено потому, что это **эвал-сет изменением и пойман тем же прогоном. Отмечено потому, что это **эвал-сет
наоборот**: случай, где верхняя ступень окупилась. Три прохода из семи наоборот**: случай, где старшая метка окупилась. Три прохода из семи
(`specs`, `adversary`, `reimpl`) нашли его независимо, и двое принесли оракул; (`specs`, `adversary`, `reimpl`) нашли его независимо, и двое принесли оракул;
проход `code` (конвенции) и гейт его не видели — порядок двух операций внутри проход `code` (конвенции) и гейт его не видели — порядок двух операций внутри
функции не выражается ни правилом линтера, ни конвенцией. функции не выражается ни правилом линтера, ни конвенцией.
+4 -4
View File
@@ -32,14 +32,14 @@ OpenSpec в сторону воспроизводимых автопроверо
Проектные копии агентов (`.claude/agents/jellybit-review-*`) и скиллов Проектные копии агентов (`.claude/agents/jellybit-review-*`) и скиллов
(`review-pipeline`, `task-pipeline`, `task-batch`) удалены в пользу плагина (`review-pipeline`, `task-pipeline`, `task-batch`) удалены в пользу плагина
`av-dev-pipeline`. Проектная специфика теперь приходит из документов канона — `av-dev-pipeline`. Проектная специфика теперь приходит из документов канона —
[docs/review.md](../../review.md): типовые узлы, ложноположительные, вопросы к [docs/review.md](../../review.md): типовые узлы, ложноположительные, вопросы по
проходам, триггеры профиля, недоступное проверке. темам, триггеры метки, недоступное проверке.
Два прохода плагин при этом **упразднил**, и это надо помнить: Два прохода плагин при этом **упразднил**, и это надо помнить:
- `idiom` — поимённая сверка со стайлгайдами языка не задаётся теперь ни одним - `idiom` — поимённая сверка со стайлгайдами языка не задаётся теперь ни одним
проходом; способные части переселены в `ops` и `architecture`. Класс проходом; куда переселены способные части и почему класс признан обратимым —
обратимый (портит форму кода, не данные) и признаётся в границах покрытия. [docs/review.md](../../review.md) → «Перестали проверять сознательно».
- `negative` — вопрос «что опытный человек отсюда удалил бы» вошёл в - `negative` — вопрос «что опытный человек отсюда удалил бы» вошёл в
`architecture` вторым обязательным. `architecture` вторым обязательным.
+3 -2
View File
@@ -17,5 +17,6 @@
это (magnet / ссылка на .torrent / .torrent-файл / сообщение бота). Сейчас текстовое это (magnet / ссылка на .torrent / .torrent-файл / сообщение бота). Сейчас текстовое
поле идёт только через `magnet.Parse`. поле идёт только через `magnet.Parse`.
Связано: specs/architecture.md → «Транспорты» (source_type = magnet|torrent|url уже в Связано: [architecture.md](../../architecture.md) → «Внешние границы и форматы»
схеме), пакет ingest, архив change `torrent-file-ingest`. (`source_type = magnet|torrent|url` уже в схеме), пакет `ingest`, архив change
`torrent-file-ingest`.
+30 -34
View File
@@ -9,50 +9,46 @@ context: |
- Технические термины (API, REST, JWT), пути и код — на английском - Технические термины (API, REST, JWT), пути и код — на английском
Имена capabilities: Имена capabilities:
- Capability — это ПОВЕДЕНИЕ/домен системы, а не пакет кода (совпадение с - Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с
именем пакета допустимо, но не критерий). именем пакета допустимо, но не критерий).
- Существительное, понятное без знания кода: ingest, recognition, - Существительное, понятное без знания кода: ingest, recognition, file-layout,
file-layout, review, notifications. НЕ qbt/worker (это реализация). review, notifications. НЕ qbt/worker это реализация.
- Гранулярность по принципу «требования меняются вместе». Дробить, когда в - Гранулярность по принципу «требования меняются вместе». Дробить, когда в
одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED
Requirements) — не дроби преждевременно в маленьком проекте. Requirements) — не дроби преждевременно в маленьком проекте.
RFC 2119 — это требование валидатора, не стиль: RFC 2119 — требование валидатора, не стиль:
- Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе - Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе
`openspec validate` падает (проверено). Поэтому эти слова и WHEN/THEN не `openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем.
русифицируем — они несут точную нормативную/структурную семантику.
Ревью (процесс, не артефакт): Что это за проект — читай перед предложением, а не отсюда:
- Нетривиальная/архитектурная задача — два чекпоинта: ревью дизайна (после - docs/passport.md — цель, её граница (чем jellybit НЕ является),
design/specs, ДО кода — дешевле чинить направление) и ревью кода (после потребители, типовые сценарии, референсы;
apply, до archive). - CLAUDE.md — инварианты с severity, семантика гейта, запреты с путями и то,
- Тривиальная задача — достаточно одного прохода (код). что считается необратимым;
- docs/architecture.md — устройство и единые точки; docs/security.md —
периметр; docs/database.md — схема и настройки с числами;
docs/adr/ — почему решено так; docs/research/ — что уже измерено.
Пересказа этих документов здесь нет намеренно: второй дом факта расходится с
первым молча, и заметно это становится в предложении, которое уже написано.
Конвенции кода (соблюдать при apply): Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом
- Механизируемое проверяет конвейер сборки (.golangci.yml + internal/archrules), скилл av-dev-pipeline:review-pipeline, проектная настройка — docs/review.md.
пересказывать его здесь не нужно: `task lint` и `task test` скажут точнее.
- Прозой остаётся то, что правилом не выражается, и это читаем в источнике:
docs/conventions/{logging,errors,config,database,web-ui}.md — уровень лога
по адресату, единственный логирующий чокпоинт на доменной границе,
трансляция доменной ошибки на внешней границе, самодокументируемый
config.example.toml, htmx-партиалы.
- Безопасность: никаких секретов в полях логов и в диагностике состояния
(пароли qBittorrent, API-ключи LLM/метабаз, auth-заголовки).
# Project context (optional) Конвенции кода: механизированное проверяет `task gate`, прозой остаётся
# This is shown to AI when creating artifacts. docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не
# Add your tech stack, conventions, style guides, domain knowledge, etc. пересказываем: и то и другое растёт по ходу задач.
# Example:
# context: | Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах
# Tech stack: TypeScript, React, Node.js паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого
# We use conventional commits же изменения.
# Domain: e-commerce platform
# Per-artifact rules (optional)
# Add custom rules for specific artifacts.
rules: rules:
proposal: proposal:
- Capabilities называй по поведению/домену системы, не по пакету кода - Capabilities называй по поведению или домену системы, не по пакету кода
specs: specs:
- Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает) # Кавычки обязательны: без них YAML обрежет строку на первом '#'.
- Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском - "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)"
- "Сценарий — ровно #### (четыре решётки); три или список молча теряются"
- "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его"
- "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"