From b879c049ea4d2e6256cbe7cfd0b7867fb0934f0b Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Fri, 7 Aug 2026 13:01:23 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D1=82=D1=8B=20=D0=BF=D0=BE=D0=B4=D0=BD=D1=8F=D1=82=D1=8B?= =?UTF-8?q?=20=D0=BD=D0=B0=20=D0=BA=D0=B0=D0=BD=D0=BE=D0=BD=207?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - review.md переведён на словарь меток: вопросы адресованы темам, триггеры профиля стали триггерами метки в три списка, quick/standard/wide → small/ medium/large, профиль deep упразднён - openspec/config.yaml переписан по канонической форме: адреса passport и CLAUDE.md вместо пересказа правил ревью и конвенций - разобраны находки doc-consistency и doc-code-drift: исключение инварианта сверено со спеками, единая точка времени и таблица classifyErr дополнены, MaxTorrentSize получил дом в database.md --- CLAUDE.md | 40 +++--- docs/.pm.json | 2 +- docs/architecture.md | 2 +- docs/conventions/config.md | 14 +- docs/conventions/errors.md | 3 + docs/database.md | 6 + docs/research/torrent-bencode-limits.md | 5 +- docs/review.md | 168 ++++++++++++---------- docs/tasks/items/quality-review-agents.md | 8 +- docs/tasks/items/torrent-url-fetch.md | 5 +- openspec/config.yaml | 64 ++++----- 11 files changed, 174 insertions(+), 143 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 7647741..378079a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,8 +40,12 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module последней копии там выключен сознательно ([state-reconciliation](openspec/specs/state-reconciliation/spec.md)); (2) уборка воркером **собственного** торрента, добавленного этим же `add` - секундами ранее, когда задачу отменили в окне после `add` - ([download-tracking](openspec/specs/download-tracking/spec.md)). Всё + секундами ранее, когда закрытие **любым** путём (`Cancel` или `Dismiss`) + увело задачу из `catched` в окне после `add` — уборка привязана к состоянию, + а не к команде; признак «своё» даёт подтверждённое отсутствие инфохэша + непосредственно перед `add` + ([download-tracking](openspec/specs/download-tracking/spec.md), + [state-reconciliation](openspec/specs/state-reconciliation/spec.md)). Всё остальное под `paths.downloads` — по-прежнему `critical`. - **Последняя копия не снимается** — `Undo` отклоняется целиком, если у цели не осталось других жёстких ссылок (`nlink <= 1`) или исходного файла уже нет. @@ -108,7 +112,8 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module - `govulncheck` даёт `WARN`, а не `FAIL`: находка тут — состояние зависимостей, а не диффа. Разбирает агент ревью по трассам вызовов. - `-race` без gcc уходит в `SKIP` с явным «гонки НЕ проверены» — тогда их - проверяет проход `ops` рассуждением, и это идёт в границы покрытия. + проверяет рассуждением тема `operations` ([docs/review.md](docs/review.md) + → «Вопросы по темам»), и это идёт в границы покрытия. - Ничего не гоняется против **живого** qBittorrent, LLM и метабаз: интеграционные тесты за env-гейтами, запускает человек вручную. - Качество распознавания гейтом не проверяется вовсе и проверяться не будет: @@ -140,7 +145,8 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module - **Необратимое** (спрашивается у человека всегда): всё, что пишет в `paths.downloads` или удаляет оттуда; удаление раздачи из qBittorrent вместе с файлами (`Delete`) — кроме уборки собственного, только что добавленного - торрента при отмене (см. исключения инварианта выше); снятие последней копии + торрента, когда закрытие любым путём увело задачу из `catched` (см. + исключения инварианта выше); снятие последней копии данных; правка уже применённой миграции; `git push --force`; удаление или перезапись файла в библиотеке Jellyfin, которого мы не создавали. - **Общий станок** — покрасневший `task gate` на `master` врывается в @@ -155,30 +161,30 @@ Go 1.26, один статический бинарь (`CGO_ENABLED=0`). Module (CLI `openspec`, v1.x). Сначала спецификация — потом код. - `openspec/specs//spec.md` — **нормативный дом поведения**: что - система делает сейчас. Capability — это поведение или домен (`ingest`, - `recognition`, `file-layout`, `review`, `notifications`), а не пакет кода. + система делает сейчас. Capability — это поведение или домен системы, а не + пакет кода. - `openspec/changes//` — предлагаемое изменение: `proposal.md`, `design.md` (для нетривиальных), дельта-спеки (`ADDED`/`MODIFIED`/`REMOVED Requirements`), `tasks.md`. После реализации change архивируется в `openspec/changes/archive/`, дельты вливаются в `openspec/specs/`. - `openspec/config.yaml` — только нужды генерации артефактов: язык, правила - именования capability, придирки валидатора. + именования capability, придирки валидатора — плюс адреса документов канона. + Пересказа этих документов там нет: второй дом факта расходится молча. Поток работы — через слэш-команды `opsx:*`: `opsx:explore` (продумать), `opsx:propose` (завести change), `opsx:apply` (реализовать tasks), `opsx:sync`/`opsx:archive` (влить и архивировать). -Правила спек: +Правила спек — язык, именование capability и придирки валидатора — живут в +[openspec/config.yaml](openspec/config.yaml) (`context` и `rules`), оттуда их +читает порождение артефактов; здесь не дублируются. Перед коммитом change — +`openspec validate --strict`. -- Каждое `### Requirement` ОБЯЗАНО содержать литерал `SHALL` или `MUST` — - иначе `openspec validate` падает. -- Структурные заголовки и ключевые слова — английские (`### Requirement:`, - `#### Scenario:`, `GIVEN/WHEN/THEN`, RFC 2119), остальной текст — русский. -- `openspec validate --strict` перед коммитом change. - -Ревью — два чекпоинта: профиль `design` на предложении (после design/specs, ДО -кода) и ревью изменения после apply, до archive. Настройка конвейера под проект -и журнал дефектов — [docs/review.md](docs/review.md). +Ревью — два чекпоинта: ревью дизайна на предложении (после design/specs, ДО +кода) и ревью изменения после apply, до archive. Состав обоих выбирается по +метке задачи (`small` / `medium` / `large`), которую разметка ставит один раз +после propose. Настройка конвейера под проект и журнал дефектов — +[docs/review.md](docs/review.md). ## Документация diff --git a/docs/.pm.json b/docs/.pm.json index d8bb38b..0fa3929 100644 --- a/docs/.pm.json +++ b/docs/.pm.json @@ -1,4 +1,4 @@ { - "canon": 4, + "canon": 7, "migrations": "internal/store/migrations" } diff --git a/docs/architecture.md b/docs/architecture.md index de42cc2..3a9bc6d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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/naming` — одна логика для превью в UI и для реального применения | | Разбор источника | `internal/magnet` и `internal/torrent`; инфохэш извлекается только здесь | diff --git a/docs/conventions/config.md b/docs/conventions/config.md index 3e8fceb..d5c5d14 100644 --- a/docs/conventions/config.md +++ b/docs/conventions/config.md @@ -37,18 +37,20 @@ ```toml [worker] -poll_interval = "5s" # как часто опрашивать qBittorrent; Go-duration (s/m/h) -magnet_timeout = "24h" # ждать метаданные magnet не дольше; Go-duration -source_missing_threshold = 3 # тиков сверки без раздачи, чтобы счесть источник удалённым +poll_interval = "" # как часто опрашивать qBittorrent; Go-duration (s/m/h) +magnet_timeout = "" # ждать метаданные magnet не дольше; Go-duration +source_missing_threshold = # тиков поллинга без раздачи, чтобы счесть источник удалённым [recognition] -auto_confidence_threshold = 0.85 # порог авто-раскладки без ревью; доля 0.0–1.0 +auto_confidence_threshold = <0.0–1.0> # порог авто-раскладки без ревью; доля [llm] -max_retries = 3 # попыток получить валидный ответ LLM; целое ≥ 0 +max_retries = # попыток получить валидный ответ LLM; целое ≥ 0 ``` -Значения в примере — иллюстрация формы комментария. Действующие умолчания и их +Значения намеренно заменены плейсхолдерами: предмет конвенции — форма +комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и +начинают врать молча при смене умолчания. Действующие умолчания и их смысл живут одним домом — таблица «Настройки с числовым значением» в [../database.md](../database.md); `config.example.toml` — источник истины по составу полей. diff --git a/docs/conventions/errors.md b/docs/conventions/errors.md index f6e6687..0ef079a 100644 --- a/docs/conventions/errors.md +++ b/docs/conventions/errors.md @@ -82,7 +82,10 @@ jellybit — **приложение, а не библиотека**: внешн |---|---|---| | `store.ErrNotFound` | 404 | «не найдено» | | `magnet.ErrNotMagnet` / `torrent.ErrNotTorrent` | 400 | «некорректный источник» | + | `ingest.ErrTorrentTooLarge` (файл больше лимита) | 400 | «файл .torrent слишком большой» | | `worker.ErrInvalidInput` (промах ввода команды) | 400 | «некорректный ввод» | + | `errManualSource` (ручной ввод источника, локальный sentinel `httpapi`) | 400 | текст самой ошибки | + | `errInvalidCandidate` (выбран несуществующий кандидат, локальный sentinel `httpapi`) | 400 | текст самой ошибки | | `worker.ErrNotReady` (источник ещё качается) | 409 | «торрент ещё качается…» | | `layout.ErrCollision` (цель занята, ушло в review) | 409 | «целевой файл уже существует…» | | `worker.ErrConflict` (операция недопустима сейчас) | 409 | «действие недоступно в текущем состоянии» | diff --git a/docs/database.md b/docs/database.md index b5b2726..f9e2043 100644 --- a/docs/database.md +++ b/docs/database.md @@ -207,5 +207,11 @@ erDiagram | `[llm].timeout` / `max_retries` | `120s` / `3` | каждая попытка порождает строку `recognition` с сырым ответом | | `[metadata.*].timeout` | `10s` | таймаут запроса к метабазе | +Пределы, зашитые константой кода, а не полем конфига: + +| Константа | Значение | Что означает | +| --- | --- | --- | +| `ingest.MaxTorrentSize` | `8 MiB` | предел размера принимаемого `.torrent`; проверяется **до** разбора, поэтому bencode-аллокации на эту величину не масштабируются (см. [research/torrent-bencode-limits.md](research/torrent-bencode-limits.md)) | + **Ретеншена нет ни у одной таблицы**, лимита на размер тела ответа LLM нет, кэша метабаз нет — всё три пункта в беклоге. diff --git a/docs/research/torrent-bencode-limits.md b/docs/research/torrent-bencode-limits.md index fe5fb5a..3456203 100644 --- a/docs/research/torrent-bencode-limits.md +++ b/docs/research/torrent-bencode-limits.md @@ -76,8 +76,9 @@ func main() { - **Усиление огромное, и наш лимит размера от него не защищает.** 36 байт входа дают 128 MiB транзиентной аллокации — это ×3.7 млн, а не «крафт-8 MiB даёт 128 MiB», как предполагала исходная нить ревью. Предел приёма - `ingest.MaxTorrentSize` (8 MiB) стоит **до** разбора и на эту величину не - влияет вовсе: атакующему хватает трёх десятков байт. + `ingest.MaxTorrentSize` ([database.md](../database.md) → «Настройки с + числовым значением») стоит **до** разбора и на эту величину не влияет вовсе: + атакующему хватает трёх десятков байт. - **Но аллокация ограничена сверху и одна на попытку разбора.** Выше потолка библиотека отказывает, не аллоцировав ничего; ниже — аллоцирует ровно объявленное и падает на чтении, обрывая разбор целиком. Дочитать несколько diff --git a/docs/review.md b/docs/review.md index 7d382dc..b141aa3 100644 --- a/docs/review.md +++ b/docs/review.md @@ -1,7 +1,7 @@ # Ревью: настройка и журнал Проектная часть конвейера ревью: чем jellybit отличается от абстрактного -Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (профили, +Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (метки, стадии, контракт находок) живёт в скилле, а не здесь. ## Как настроен конвейер @@ -93,126 +93,142 @@ Go-сервиса и что здесь уже проскакивало. Устр `original_title` заполняется всегда и при неуверенности дублирует `title` — это контракт capability `recognition`, а не недосмотр. -### Вопросы к проходам +### Вопросы по темам -Форма: `<имя прохода>: <вопрос> (<провенанс>)`. Журнал дефектов пока пуст, -поэтому провенанс у всех пунктов — инвариант или ADR, а не пойманный случай; -по мере накопления журнала список должен смещаться в сторону реальных промахов. +Форма: `<тема>: <вопрос> (<провенанс>)`. Адресуется теме, а не имени прохода: +проход переезжает между метками и упраздняется, тема переезд переживает. Задаёт +вопрос тот, кто закрывает тему на текущем прогоне. -- `adversary`: можно ли, управляя только именами файлов в раздаче и текстом +- `security`: можно ли, управляя только именами файлов в раздаче и текстом контекста, добиться целевого пути вне `paths.movies`/`series` — включая путь через юникод, длину сверх лимита ФС и коллизию после нормализации? (инвариант «целевой путь строго под библиотекой», [security.md](security.md)) -- `adversary`: есть ли последовательность команд, после которой снимается +- `security`: есть ли последовательность команд, после которой снимается **последняя** копия данных — с учётом `superseded`-ссылок и гонки со сверкой? (инвариант «источник неприкосновенен») -- `adversary`: что даёт крафт-магнет с чужим или подставным инфохэшем — +- `security`: что даёт крафт-магнет с чужим или подставным инфохэшем — присоединение к чужой активной загрузке, отравление владения? (открытая задача про идентичность инфохэшей) -- `adversary`: где признак «это наше» снимается с одной сущности, а действие +- `security`: где признак «это наше» снимается с одной сущности, а действие применяется к другой — присутствие раздачи в qBittorrent против байтов на диске, запись в БД против файла, инфохэш против содержимого? (журнал, 2026-08-06: уборка своего торрента сносила чужие файлы) -- `ops`: что делает эта ветка, когда qBittorrent недоступен несколько минут - подряд — сколько ERROR-строк в секунду и меняется ли состояние задач? +- `operations`: что делает эта ветка, когда qBittorrent недоступен несколько + минут подряд — сколько ERROR-строк в секунду и меняется ли состояние задач? (задача про ERROR-шторм фоновых циклов) -- `ops`: как это ведёт себя при сотне загрузок в базе и десятках тысяч +- `operations`: как это ведёт себя при сотне загрузок в базе и десятках тысяч `file_link` — есть ли запрос без индекса и полный проход по таблице? (задача про масштаб 100/1000, [database.md](database.md) → «Настройки») -- `ops`: что остаётся на диске и в базе, если процесс убит посреди раскладки - батча? (состояние `linking` и его восстановление) -- `code`: логирующий чекпоинт один на операцию — или ошибка залогирована и - возвращена вверх, где залогирована снова? +- `operations`: что остаётся на диске и в базе, если процесс убит посреди + раскладки батча? (состояние `linking` и его восстановление) +- `conventions`: логирующий чекпоинт один на операцию — или ошибка залогирована + и возвращена вверх, где залогирована снова? ([conventions/logging.md](conventions/logging.md)) -- `code`: новое поле конфига появилось в `config.example.toml` с описанием - назначения, диапазона и единиц? ([conventions/config.md](conventions/config.md)) -- `specs`: не завелось ли поведение, которого спека не заказывала — тихий +- `conventions`: новое поле конфига появилось в `config.example.toml` с + описанием назначения, диапазона и единиц? + ([conventions/config.md](conventions/config.md)) +- `requirements`: не завелось ли поведение, которого спека не заказывала — тихий дефолт, проглоченная ошибка, ретрай «на всякий случай», отброшенное поле? - `architecture`: не появился ли второй способ делать то, что уже делается — второе место, где генерится время или id, второй парсер источника, вторая логика целевых имён мимо `naming`? ([architecture.md](architecture.md) → «Единые точки проекта») -### Триггеры профиля +### Триггеры метки -Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `standard`; -миграция схемы и публичный контракт ступень **не** поднимают: их проверяют -проходы, которые в `standard` и так есть. +Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `medium`; +миграция схемы и публичный контракт метку **не** поднимают: их проверяют +проходы, которые в `medium` и так есть. Списка три: два поднимают до `large`, +по одному на ось, третий опускает до `small`. -**Новым понятием или структурной единицей здесь считается** (→ `wide`): новый -пакет `internal/*`; новая capability в `openspec/specs/`; новый транспорт приёма -или уведомлений рядом с REST, веб-UI, ботом и CLI; новый провайдер метабазы за -существующим интерфейсом; новое состояние в графе переходов загрузки; перенос -ответственности между `worker`, `recognition`, `layout` и `store`. +**Крупное здесь** — про объём, сколько узлов и слоёв трогает изменение: -**Правила идентичности, слияния и разбора живут здесь** (→ `deep`): владение -раздачей по инфохэшу и сверка с qBittorrent (`ident`, `internal/store`, -`state-reconciliation`); построение целевых путей и санитизация имён -(`internal/layout`, `naming`); разбор недоверенного входа — bencode, magnet, -текст контекста, ответ LLM (`internal/torrent`, `internal/magnet`, -`internal/tgbot/parse.go`, разбор ответа модели); выбор кандидата метабазы и -слияние его полей с догадкой LLM (`internal/metadata`, `metadata-match`); -merge-раскладка при повторном добавлении раздачи. +- перенос ответственности между `worker`, `recognition`, `layout` и `store`; +- новое состояние в графе переходов загрузки: оно тянет за собой воркер, спеку, + отображение в веб-UI и боте и восстановление после рестарта; +- правка, идущая насквозь по цепочке приём → распознавание → раскладка; +- новый провайдер метабазы за существующим интерфейсом: клиент, поле конфига с + образцом, слияние полей кандидата, ветка «провайдера нет». + +**Незнакомое здесь** — про форму решения, которую предстоит нащупать по ходу: + +- новый пакет `internal/*` или новая capability в `openspec/specs/`; +- новый транспорт приёма или уведомлений рядом с REST, веб-UI, ботом и CLI; +- заводится или меняется **правило идентичности, слияния или разбора**: ключ + владения раздачей и сверка с qBittorrent (`ident`, `internal/store`, + `state-reconciliation`); построение целевых путей и санитизация имён + (`internal/layout`, `naming`); новый вид входа или новая ветка неоднозначности + у разбора недоверенного — bencode, magnet, текст контекста, ответ LLM + (`internal/torrent`, `internal/magnet`, `internal/tgbot/parse.go`); новый + источник или новый победитель при конфликте в слиянии кандидата метабазы + (`internal/metadata`, `metadata-match`); merge-раскладка при повторном + добавлении раздачи. - «Поведение, видимое снаружи» здесь включает **тексты и карточки Telegram** — для единственного пользователя это и есть интерфейс. -**Место из списка ступень не поднимает — поднимает правило.** Перечни выше -отвечают «здесь такие правила водятся», а не «любая правка здесь идёт в `deep`». -Ступень поднимает то, что даёт работу новому проходу: заводится ключ сравнения -или меняется его состав; у разбора появляется новый вид входа или новая ветка -неоднозначности; в слияние добавляется источник или меняется победитель при -конфликте. +**Место из перечня метку не поднимает — поднимает правило.** Перечни выше +отвечают «здесь такие правила водятся», а не «любая правка здесь идёт в +`large`». Метку поднимает то, что даёт работу новому проходу: заводится ключ +сравнения или меняется его состав; у разбора появляется новый вид входа или +новая ветка неоднозначности; в слияние добавляется источник или меняется +победитель при конфликте. -**Отсекающие условия — проверяются до выбора профиля, любое сработавшее держит -ступень внизу.** Перечень закрытый, каждый пункт проверяется взглядом на дифф и -дельта-спеку: +**Мелкое здесь** — опускает до `small`. Перечень закрытый, каждый пункт +проверяется взглядом на дифф и дельта-спеку, любое сработавшее держит метку +внизу: -- **дельта-спека называет исход поимённо.** Независимая реализация окупается - выбором, которого спека не сделала. Если сценарий уже говорит, что даёт - вырожденный вход, реализация повторит спеку, и дифф покажет расхождение в - форме, а не в решении; +- **дельта-спека называет исход поимённо** — сценарий уже говорит, что даёт + вырожденный вход, и решать в коде нечего; - **новых сценариев в дельта-спеке нет** — изменение уточняет уже описанное поведение, а не заказывает новое; - **правка сообщения, комментария, записи журнала, имени или теста** в узле из - перечня; + перечней выше; - **сужение уже существующей нормализации** без нового вида входа: вход остался тот же, изменился исход на одном его значении. -**Ориентир частоты.** `standard` закрывает большинство задач, `wide` — редкий -случай, `deep` — исключение на крупной функциональности, а не на уборке. Задача -типа `chore` или `bugfix`, собранная из нитей прошлого ревью, идёт в `quick` или -`standard`, даже когда трогает файл из перечня `deep`. Верхняя ступень чаще одной -задачи на спринт означает ошибку в критерии, а не спринт из сложных задач. +Отрицательный тест поверх перечня: что после мерджа не откатывается обратной +правкой — миграция, формат на диске, публичный контракт, имя, — **не** `small`, +каким бы маленьким ни был дифф. + +**Ориентир частоты.** `medium` закрывает большинство задач, `large` рассчитана +на 5–10% и приходится на крупную функциональность, а не на уборку: задача типа +`chore` или `fix`, собранная из нитей прошлого ревью, идёт в `small` или +`medium`, даже когда трогает файл из перечней выше. `large` чаще одной задачи на +спринт означает ошибку в критерии, а не спринт из сложных задач. ### Недоступно проверке **Не проверит ни один проход** — принципиальная граница, по факту промаха не пересматривается. -- История инцидентов на umbar и то, что уже ломалось в проде. -- Поведение таблицы SQLite под реальным объёмом и профилем нагрузки: реального - профиля нет ни у кого, кроме сервера. -- Завязка внешних потребителей (Jellyfin, закладки, чужие ссылки) на текущее - поведение. -- Качество распознавания как таковое: правильно ли LLM определил фильм — вопрос - тюнинга модели и промпта, а не ревью кода. Размеченный корпус, по которому это - можно было бы судить числом, решено не собирать (`tasks/REJECTED.md`, - 2026-08-06). -- Суждение «этой функциональности не должно существовать». +- `operations`: история инцидентов на umbar и то, что уже ломалось в проде. +- `operations`: поведение таблицы SQLite под реальным объёмом и профилем + нагрузки — реального профиля нет ни у кого, кроме сервера. +- `architecture`: завязка внешних потребителей (Jellyfin, закладки, чужие + ссылки) на текущее поведение. +- `architecture`: суждение «этой функциональности не должно существовать». +- `requirements`: качество распознавания как таковое — правильно ли LLM + определил фильм. Это вопрос тюнинга модели и промпта, а не ревью кода; + размеченный корпус, по которому это можно было бы судить числом, решено не + собирать (`tasks/REJECTED.md`, 2026-08-06). **Перестали проверять сознательно** — пересматривается первым, как только что-то проскочило. -- **Идиоматичность Go — с 2026-08-04.** Проектный проход `idiom` (поимённая - сверка с положениями Effective Go, Go Code Review Comments, стайлгайдов Uber - и Google) удалён вместе с проектными копиями агентов при переезде на плагин - `av-dev-pipeline`, который этот проход упразднил. Способные части переселены: - эксперимент против поведения библиотеки и драйвера — в `ops`, «не изобретаем - ли то, что уже есть в библиотеке» — в `architecture`. **Различение - «идиоматично против распространено» теперь не спрашивает никто.** Класс - обратимый: портит форму кода, не данные. Пересмотр — задача - `quality-review-agents`. +- `conventions`: **идиоматичность Go — с 2026-08-04.** Проектный проход `idiom` + (поимённая сверка с положениями Effective Go, Go Code Review Comments, + стайлгайдов Uber и Google) упразднён вместе с переездом конвейера в плагин + ([ADR-2026-08-04-review-pipeline-to-plugin](adr/ADR-2026-08-04-review-pipeline-to-plugin.md)); + способные части переселены — в тему `operations` (эксперимент против + поведения библиотеки и драйвера) и в `architecture` («не изобретаем ли то, + что уже есть в библиотеке»). **Различение «идиоматично против + распространено» теперь не спрашивает никто.** Класс обратимый: портит форму + кода, не данные. Пересмотр — задача `quality-review-agents`. +- `security`, `operations`, `architecture`: на метках `small` и `medium` не + проверяется ничто, требующее запуска, — построенных путей атаки, замеров и + эксплуатационного постмортема там нет по устройству конвейера. Их даёт только + `large`, а она приходится на 5–10% задач. ## Журнал дефектов @@ -272,7 +288,7 @@ merge-раскладка при повторном добавлении разд проход не спрашивал про **асимметрию признака владения** — признак снимается с одной сущности (запись в qBittorrent), а действие применяется к другой (байты на диске). Враждебный проход до этой задачи на данном коде не гонялся. -- **Что меняем:** вопрос `adversary` в разделе выше дополнен пунктом про +- **Что меняем:** в «Вопросы по темам» добавлен вопрос темы `security` про асимметрию признака владения. Сам дефект — задачей в беклоге, кандидат `critical`; спека `state-reconciliation` в том же изменении перестала утверждать, что уборка «данных пользователя не касается». @@ -296,7 +312,7 @@ merge-раскладка при повторном добавлении разд В дереве остался табличный `TestParseNoNameSentinelDropped`. - **Почему не поймали раньше:** ловить было нечему — дефект внесён этим же изменением и пойман тем же прогоном. Отмечено потому, что это **эвал-сет - наоборот**: случай, где верхняя ступень окупилась. Три прохода из семи + наоборот**: случай, где старшая метка окупилась. Три прохода из семи (`specs`, `adversary`, `reimpl`) нашли его независимо, и двое принесли оракул; проход `code` (конвенции) и гейт его не видели — порядок двух операций внутри функции не выражается ни правилом линтера, ни конвенцией. diff --git a/docs/tasks/items/quality-review-agents.md b/docs/tasks/items/quality-review-agents.md index 94faeeb..87d3550 100644 --- a/docs/tasks/items/quality-review-agents.md +++ b/docs/tasks/items/quality-review-agents.md @@ -32,14 +32,14 @@ OpenSpec в сторону воспроизводимых автопроверо Проектные копии агентов (`.claude/agents/jellybit-review-*`) и скиллов (`review-pipeline`, `task-pipeline`, `task-batch`) удалены в пользу плагина `av-dev-pipeline`. Проектная специфика теперь приходит из документов канона — -[docs/review.md](../../review.md): типовые узлы, ложноположительные, вопросы к -проходам, триггеры профиля, недоступное проверке. +[docs/review.md](../../review.md): типовые узлы, ложноположительные, вопросы по +темам, триггеры метки, недоступное проверке. Два прохода плагин при этом **упразднил**, и это надо помнить: - `idiom` — поимённая сверка со стайлгайдами языка не задаётся теперь ни одним - проходом; способные части переселены в `ops` и `architecture`. Класс - обратимый (портит форму кода, не данные) и признаётся в границах покрытия. + проходом; куда переселены способные части и почему класс признан обратимым — + [docs/review.md](../../review.md) → «Перестали проверять сознательно». - `negative` — вопрос «что опытный человек отсюда удалил бы» вошёл в `architecture` вторым обязательным. diff --git a/docs/tasks/items/torrent-url-fetch.md b/docs/tasks/items/torrent-url-fetch.md index 729c846..e8df799 100644 --- a/docs/tasks/items/torrent-url-fetch.md +++ b/docs/tasks/items/torrent-url-fetch.md @@ -17,5 +17,6 @@ это (magnet / ссылка на .torrent / .torrent-файл / сообщение бота). Сейчас текстовое поле идёт только через `magnet.Parse`. -Связано: specs/architecture.md → «Транспорты» (source_type = magnet|torrent|url уже в -схеме), пакет ingest, архив change `torrent-file-ingest`. +Связано: [architecture.md](../../architecture.md) → «Внешние границы и форматы» +(`source_type = magnet|torrent|url` уже в схеме), пакет `ingest`, архив change +`torrent-file-ingest`. diff --git a/openspec/config.yaml b/openspec/config.yaml index 73d6c73..742a14f 100644 --- a/openspec/config.yaml +++ b/openspec/config.yaml @@ -9,50 +9,46 @@ context: | - Технические термины (API, REST, JWT), пути и код — на английском Имена capabilities: - - Capability — это ПОВЕДЕНИЕ/домен системы, а не пакет кода (совпадение с + - Capability — это ПОВЕДЕНИЕ или домен системы, а не пакет кода (совпадение с именем пакета допустимо, но не критерий). - - Существительное, понятное без знания кода: ingest, recognition, - file-layout, review, notifications. НЕ qbt/worker (это реализация). + - Существительное, понятное без знания кода: ingest, recognition, file-layout, + review, notifications. НЕ qbt/worker — это реализация. - Гранулярность по принципу «требования меняются вместе». Дробить, когда в одной спеке смешиваются разные заботы. Переименовать дёшево (RENAMED Requirements) — не дроби преждевременно в маленьком проекте. - RFC 2119 — это требование валидатора, не стиль: + RFC 2119 — требование валидатора, не стиль: - Каждое ### Requirement ОБЯЗАНО содержать литерал SHALL или MUST, иначе - `openspec validate` падает (проверено). Поэтому эти слова и WHEN/THEN не - русифицируем — они несут точную нормативную/структурную семантику. + `openspec validate` падает. Поэтому эти слова и WHEN/THEN не русифицируем. - Ревью (процесс, не артефакт): - - Нетривиальная/архитектурная задача — два чекпоинта: ревью дизайна (после - design/specs, ДО кода — дешевле чинить направление) и ревью кода (после - apply, до archive). - - Тривиальная задача — достаточно одного прохода (код). + Что это за проект — читай перед предложением, а не отсюда: + - docs/passport.md — цель, её граница (чем jellybit НЕ является), + потребители, типовые сценарии, референсы; + - CLAUDE.md — инварианты с severity, семантика гейта, запреты с путями и то, + что считается необратимым; + - docs/architecture.md — устройство и единые точки; docs/security.md — + периметр; docs/database.md — схема и настройки с числами; + docs/adr/ — почему решено так; docs/research/ — что уже измерено. + Пересказа этих документов здесь нет намеренно: второй дом факта расходится с + первым молча, и заметно это становится в предложении, которое уже написано. - Конвенции кода (соблюдать при apply): - - Механизируемое проверяет конвейер сборки (.golangci.yml + internal/archrules), - пересказывать его здесь не нужно: `task lint` и `task test` скажут точнее. - - Прозой остаётся то, что правилом не выражается, и это читаем в источнике: - docs/conventions/{logging,errors,config,database,web-ui}.md — уровень лога - по адресату, единственный логирующий чокпоинт на доменной границе, - трансляция доменной ошибки на внешней границе, самодокументируемый - config.example.toml, htmx-партиалы. - - Безопасность: никаких секретов в полях логов и в диагностике состояния - (пароли qBittorrent, API-ключи LLM/метабаз, auth-заголовки). + Ревью: правило выбора метки и состав проходов здесь не пересказываем — их дом + скилл av-dev-pipeline:review-pipeline, проектная настройка — docs/review.md. -# Project context (optional) -# This is shown to AI when creating artifacts. -# Add your tech stack, conventions, style guides, domain knowledge, etc. -# Example: -# context: | -# Tech stack: TypeScript, React, Node.js -# We use conventional commits -# Domain: e-commerce platform + Конвенции кода: механизированное проверяет `task gate`, прозой остаётся + docs/conventions/. Ни состав шагов гейта, ни перечень конвенций здесь не + пересказываем: и то и другое растёт по ходу задач. + + Развилка или блокер — сперва prior art. Готовые решения смотрим в референсах + паспорта, отвергаем — с названной причиной, и причина идёт в design.md этого + же изменения. -# Per-artifact rules (optional) -# Add custom rules for specific artifacts. rules: proposal: - - Capabilities называй по поведению/домену системы, не по пакету кода + - Capabilities называй по поведению или домену системы, не по пакету кода specs: - - Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает) - - Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском + # Кавычки обязательны: без них YAML обрежет строку на первом '#'. + - "Каждое ### Requirement обязано содержать SHALL или MUST (иначе валидация падает)" + - "Сценарий — ровно #### (четыре решётки); три или список молча теряются" + - "SHALL/MUST должно стоять в ПЕРВОМ абзаце требования: валидатор смотрит только его" + - "Заголовки и WHEN/THEN/GIVEN — на английском, остальной текст на русском"