раскладка av-dev повышена с канона 12 до версии 5

- три плагина слились в один `av-dev`: служебные `docs/.docs.json` и
  `tasks/.tasks.json` заменены на `.av-dev.toml` в корне, в гейте переехали пути
  трёх скриптов, вызовы скиллов переименованы по всему репозиторию
- тип задачи `goal` и `ROADMAP.md` упразднены: семь целей закрыты с причинами,
  теги сняты, объявлена стадия `support`
- метка `small`/`medium`/`large` снята из процесса — вместо «Триггеров метки» в
  review.md подраздел «Когда звать глубокое ревью»; следом разобран урожай
  doc-consistency: девять фактов сведены к одному дому
This commit is contained in:
av
2026-09-02 09:55:28 +03:00
parent fc9a3b4066
commit 3bce73fc34
68 changed files with 178 additions and 327 deletions
-4
View File
@@ -1,4 +0,0 @@
{
"canon": 12,
"migrations": "internal/store/migrations"
}
+11 -4
View File
@@ -1,17 +1,24 @@
# Журнал решений
Одна запись — одно решение. **ADR это промоут поверх архивного `design.md`**,
а не второе сочинение: запись цитирует решение и ссылается на
`openspec/changes/archive/<id>/design.md`.
Одна запись — одно решение. **ADR продвигает уже написанное решение, а не
сочиняет его заново**: запись цитирует решение и ссылается на источник —
`openspec/changes/archive/<id>/design.md`, а у решения, принятого разведкой без
изменения, на её записку.
Главная ценность записи — сохранить **почему**: намерение и причинность. Это
важнее аккуратности оформления и полноты остальных секций.
Решение старше OpenSpec либо принятое вне этого репозитория архивного
`design.md` не имеет по построению. Тогда поле **Источник** называет то, что
есть, — историю git, черновик, записку соседнего репозитория — и прямо говорит,
что архивного `design.md` нет. Запись без поля «Источник» вовсе — дефект; запись,
честно назвавшая отсутствие источника, — нет.
## Когда заводить
Верно одно из трёх:
<!-- копия: adr-когда-заводить из av-dev-docs/skills/canon/references/canon.md -->
<!-- копия: adr-когда-заводить из av-dev/skills/canon/references/canon.md -->
- **дорогой откат** — переделка стоит дороже переписывания одного файла;
- **намеренный отказ** от очевидного подхода;
- **пересмотр прежнего решения** — тогда у старой записи обязателен статус
+2 -1
View File
@@ -1,7 +1,8 @@
# Краткий заголовок решения
- **Дата:** ГГГГ-ММ-ДД
- **Источник:** openspec/changes/archive/<id>/design.md
- **Источник:** openspec/changes/archive/<id>/design.md — либо записка разведки,
если решение принято без изменения
<!-- Статус ставится тем же полем и только при пересмотре:
- **Статус:** заменено на ADR-ГГГГ-ММ-ДД-slug
+3 -3
View File
@@ -108,7 +108,7 @@
| Что | Где |
| --- | --- |
| Время | `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` |
| Время | `store.Now()` — единственный источник меток времени в данных, всегда UTC; формат хранения — [database.md](database.md). Вторая санкционированная точка 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 и для реального применения |
| Чистка человекочитаемых значений | три санитайзера с разным предметом, сводить их в один нельзя: `recognize.SanitizeTitle` — значение (недоверенный вход: LLM и метабазы), `layout.sanitizeComponent` — компонент пути под требования ФС, `naming.sanitize` — отображаемый ярлык. Значение метабазы чистится **на каждой** точке входа в план: сборка матча, копия кандидата для ревью, набор закреплённых значений источника и его чтение — [ADR-2026-08-10-sanitize-at-every-entry](adr/ADR-2026-08-10-sanitize-at-every-entry.md) |
@@ -116,7 +116,7 @@
| Приём | use-case `ingest` — общий путь для HTTP, веб-UI, Telegram и CLI |
| Переходы состояний | `worker` под per-download блокировкой; легальность перехода задаётся декларативным графом |
| Хардлинки и удаление своих ссылок | `internal/layout` — единственное место, которое пишет в файловую систему библиотеки |
| Построение и проверка целевого пути | `layout.BuildLinks` — единственная сборка пути; там же обе проверки, и порядок значим: нахождение под корнем библиотеки, затем длина компонента. Отсюда же строятся оба предпросмотра ревью, поэтому показанное и применённое совпадают устройством, а не договорённостью |
| Построение и проверка целевого пути | `layout.BuildLinks` — единственная сборка пути; там же обе проверки, порядок нормирован [file-layout](../openspec/specs/file-layout/spec.md). Отсюда же строятся оба предпросмотра ревью, поэтому показанное и применённое совпадают устройством, а не договорённостью |
| Причина, по которой человек не видит плана | считается **на показе** (`worker.ReviewData.PreviewError`) и предпочитается записанной в состоянии: записанной может не быть вовсе, а после смены источника она уже про другой план — [ADR-2026-08-10-reason-computed-on-read](adr/ADR-2026-08-10-reason-computed-on-read.md) |
| Условие допуска полного удаления | `store.State.CanDelete()` — «из этого состояния удаление с файлами разрешено»; своего перечня состояний не заводит ни один транспорт (веб-UI, Telegram, страница группового удаления), а проверку в ядре предикат не заменяет: допуск держится без транспорта |
| Условие самообновления веб-UI | `store.State.IsObservable()` — «состояние ещё может измениться без человека»; транспорт своего перечня состояний не заводит, а поверхность (карточка списка, страница загрузки) держит **ровно один** поллер на обновляемый корень — [ADR-2026-08-10-observability-is-not-terminality](adr/ADR-2026-08-10-observability-is-not-terminality.md), правило разметки — [conventions/web-ui.md](conventions/web-ui.md) |
@@ -185,7 +185,7 @@ Jellyfin указывают на `movies`/`series`, а не на корень
| Область | Чего нет сегодня | Задача |
| --- | --- | --- |
| Масштаб | ориентир 100/1000 загрузок не зафиксирован, узкие места SQLite, воркера и поллинга не измерены | `scale-100-downloads` |
| Ретеншен | терминальные задачи и сырые ответы LLM копятся вечно, авточистки нет | `db-retention-cleanup` |
| Срок хранения | терминальные задачи и сырые ответы LLM копятся вечно, авточистки нет | `db-retention-cleanup` |
| Бекап | бекапить `/data` требуется, а стратегия и ротация не описаны | `sqlite-backup` |
| Метрики и алертинг | healthcheck проверяет только сам сервис; метрик и алертинга нет, отказ виден по застрявшей задаче | `deep-healthcheck-dependencies` |
| Идентичность раздачи | split v1/v2-хеши не связаны, паре `xt` из магнета доверяем | `infohash-identity-integrity` |
+1 -1
View File
@@ -9,7 +9,7 @@
перечень «Механизировано» ниже. Причина: файл на несколько сотен строк
размазывает внимание по тривиальному — и модель, и человек добросовестно
проверят именование и не дойдут до формы решения. Процедура промоута —
`references/promote.md` скилла `av-dev-pipeline:review-pipeline`.
`references/promote.md` скилла `av-dev:code-review`.
Обоснование «почему именно так» живёт в [../adr/](../adr/README.md); инварианты
с severity — в [CLAUDE.md](../../CLAUDE.md).
+9 -9
View File
@@ -39,16 +39,16 @@
## Прочее
- Enum-поля (`state`, `kind`, …) — обычный `TEXT` без `CHECK`; допустимые
значения держит код (`internal/store`).
- Временные метки — `TEXT` в **RFC 3339, UTC (суффикс `Z`)**, напр.
`2006-01-02T15:04:05Z` (секундная точность). Фиксированная ширина сохраняет
лексикографическую сортировку TEXT = хронологию (`ORDER BY created_at`).
Единая точка генерации — приложение: `store.Now()` + `store.FormatTime`/
- Enum-поля — обычный `TEXT`/`INTEGER` без `CHECK`; допустимые значения держит
код (`internal/store`). Какие именно поля — [../database.md](../database.md) →
«Индексы и ограничения».
- Временные метки пишет приложение: `store.Now()` + `store.FormatTime`/
`ParseTime` (аналогично `ident.NewID` для id), а не дефолт в схеме — так
забытая вставка падает громко. Измерение длительности — не метка времени: у
внешних вызовов его засекает `logging.StartCall`. Таймзона отображения в
UI — конфиг `[general].timezone`.
забытая вставка падает громко. Формат и точность хранения —
[../database.md](../database.md); фиксированная ширина там выбрана затем, чтобы
лексикографическая сортировка TEXT совпадала с хронологией. Измерение
длительности — не метка времени: у внешних вызовов его засекает
`logging.StartCall`. Таймзона отображения в UI — конфиг `[general].timezone`.
- Миграции — goose (`internal/store/migrations`): SQL-файлы для DDL;
Go-миграции (`goose.AddMigrationContext`) — когда нужен код (генерация
id, backfill). При изменении структуры обновляем ER-схему
+12 -7
View File
@@ -219,12 +219,16 @@ Go-ошибки логируем как атрибут, не как текст
## Безопасность: что не логируем
Никаких секретов в полях и сообщениях. Под запретом:
Никаких секретов в полях и сообщениях. Состав секретов —
[CLAUDE.md](../../CLAUDE.md) → «Инварианты», строка «Секреты не попадают в логи,
диагностику и ответы API»; здесь он не дублируется.
- учётные данные qBittorrent (логин/пароль, cookie сессии);
- API-ключ и токен LLM-провайдера, `Authorization`-заголовки;
- ключи TMDB/TVDB и прочих метабаз;
- содержимое аутентификационных параметров magnet/трекеров.
Специфичные для логов **носители** секрета, которых в инварианте нет:
- cookie сессии qBittorrent;
- заголовки `Authorization`;
- аутентификационные параметры magnet и трекеров;
- URL внутри `*url.Error` (см. ниже).
Дополнительно:
@@ -246,8 +250,9 @@ Go-ошибки логируем как атрибут, не как текст
- Пишем JSON в `stdout` одним потоком; сбор и ротацию делает окружение
(docker/journald). Не маршрутизируем по файлам.
- Базовый уровень в проде — `INFO`; `DEBUG` включается через конфиг/env при
необходимости. dev — `DEBUG`.
- Базовый уровень в проде — `INFO`; `DEBUG` включается полем `level` секции
`[log]` в `config.toml` — env для конфига не используется
([config.md](config.md)). dev — `DEBUG`.
## Анализ
+7 -5
View File
@@ -176,7 +176,7 @@ erDiagram
внутри JSON нет, SQL по содержимому этих полей не делается.
- **`recognition.raw_llm`** — сырой ответ модели как есть, **несжатый**. Это
самое крупное поле в базе и главный кандидат на рост: у каждой попытки
распознавания свой ответ, попытки не вытесняются, ретеншена нет
распознавания свой ответ, попытки не вытесняются, срока хранения нет
(задача в беклоге).
- **`download_torrent.data`** — единственный BLOB: исходные байты `.torrent`
(обычно десятки КБ, у больших раздач — сотни). Читается целиком при
@@ -224,8 +224,10 @@ erDiagram
| `httpapi.maxBulkDelete` | `20` загрузок | предел размера одной пачки группового удаления. Подтверждение, перечисляющее больше, человек не читает — то есть перестаёт быть подтверждением; плюс один синхронный запрос упирается в столько же последовательных вызовов qBittorrent. Предел называет сама страница выбора; отказ по пределу возвращает выбор с сохранёнными отметками |
| `httpapi.bulkFailThreshold` | `3` отказа подряд | сколько подряд идущих отказов внешнего сервиса прекращают проход группового удаления. Удаление снимает библиотечные ссылки раньше, чем сносит раздачу: при недоступном qBittorrent каждая единица успевает выполнить необратимый локальный шаг и упасть на внешнем. Счётчик сбрасывается на успехе; конфликт состояния системным отказом не считается |
| `httpapi.bulkBudget` | `2` минуты | потолок времени на один проход группового удаления. Удаление держит общий замок воркера на всё время обращения к qBittorrent, поэтому медленно, но успешно отвечающий сосед остановил бы фоновую работу целиком, а порог отказов такого не ловит. Проверяется между единицами: начатое удаление не обрывается, иначе оно встанет между снятием ссылок и сносом раздачи |
| `layout.maxComponentBytes` | `255` байт | предел длины компонента целевого пути (`NAME_MAX` у ext4/xfs/btrfs); меряется в байтах UTF-8, проверяется **до** первой операции с ФС, отказ уводит задачу в `review` с кодом `name_too_long`. У ядра не выясняется; на ФС с меньшим пределом остаётся отказ ядра — лечение правкой константы, а не настройкой |
| `layout.maxComponentBytes` | `255` байт | предел длины компонента целевого пути (`NAME_MAX` у ext4/xfs/btrfs); меряется в байтах UTF-8. Что происходит при превышении — [file-layout](../openspec/specs/file-layout/spec.md); величину у ядра не выясняем, и на ФС с меньшим пределом остаётся отказ ядра — чинится правкой константы, а не настройкой |
| `llm.maxResponseBody` | `8 MiB` | предел размера тела ответа LLM-эндпоинта: ответ читается через `io.LimitReader`, а не целиком |
| `metadata.maxBody` | `4 MiB` | то же для ответов TMDB, TVDB и TVMaze |
**Ретеншена нет ни у одной таблицы**, лимита на размер тела ответа LLM нет,
кэша метабаз нет; состояние по каждому пробелу и заведённые под них задачи —
[architecture.md](architecture.md) → «Открытые вопросы».
**Срока хранения нет ни у одной таблицы**, кэша метабаз нет; состояние по каждому
пробелу и заведённые под них задачи — [architecture.md](architecture.md) →
«Открытые вопросы».
+1 -1
View File
@@ -1,7 +1,7 @@
# Паспорт проекта
Зачем это и для кого. [architecture.md](architecture.md) отвечает «как
устроено», [tasks/ROADMAP.md](../tasks/ROADMAP.md) — «что уже умеет», паспорт —
устроено», [openspec/specs/](../openspec/specs/) — «что уже умеет», паспорт —
«зачем и для кого».
## Цель
+1 -1
View File
@@ -10,7 +10,7 @@
не от смены формата.
**Каждый вывод — с числами и командой или условиями, которыми получен**, чтобы
его можно было перепроверить. Число без провенанса проход обязан читать как
его можно было перепроверить. Число без происхождения проход обязан читать как
условие, а не как замер. Число, чей источник по ссылке не подтвердился, не
выбрасывается и не переписывается по догадке — остаётся с пометкой «расходится
с источником: там <что нашли>».
+1 -1
View File
@@ -5,7 +5,7 @@
сообщения берутся **источник** (magnet) и **контекст** для распознавания. Формат
чужой, ничем не документирован и может измениться без предупреждения.
**Провенанс.** Образец снят вручную из личного чата Telegram (сообщение
**Происхождение.** Образец снят вручную из личного чата Telegram (сообщение
датировано 2026-03-21) и зафиксирован в [BRIEF.md] проекта; второй образец,
меньшего размера, живёт константой `botMessage` в
`internal/tgbot/parse_test.go` и проверяется тестами разборщика. Статистики по
+1 -1
View File
@@ -4,7 +4,7 @@
из ответа `/search`. Здесь записано, откуда взята форма этого ответа и чего в ней
не подтверждено.
**Провенанс — и он слабый.** Всё ниже сверено **по публичной документации**
**Происхождение — и оно слабое.** Всё ниже сверено **по публичной документации**
TheTVDB API v4, файл `docs/swagger.yml` репозитория `thetvdb/v4-api`, поле
`info.version` = `4.7.10` (прочитано 2026-08-07). **Живым прогоном не
подтверждено ни одно наблюдение**: `CLAUDE.md` → «Запреты» запрещает ходить в
+56 -75
View File
@@ -1,8 +1,8 @@
# Ревью: настройка и журнал
Проектная часть конвейера ревью: чем jellybit отличается от абстрактного
Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (метки,
стадии, контракт находок) живёт в скилле, а не здесь.
Go-сервиса и что здесь уже проскакивало. Устройство самого конвейера (состав
проходов, стадии, контракт находок) живёт в скилле, а не здесь.
## Как настроен конвейер
@@ -135,8 +135,8 @@ Go-сервиса и что здесь уже проскакивало. Устр
### Вопросы по темам
Форма: `<тема>: <вопрос> (<провенанс>)`. Адресуется теме, а не имени прохода:
проход переезжает между метками и упраздняется, тема переезд переживает. Задаёт
Форма: `<тема>: <вопрос> (<откуда>)`. Адресуется теме, а не имени прохода:
проход переезжает между скиллами и упраздняется, тема переезд переживает. Задаёт
вопрос тот, кто закрывает тему на текущем прогоне.
- `security`: можно ли, управляя только именами файлов в раздаче и текстом
@@ -179,8 +179,8 @@ Go-сервиса и что здесь уже проскакивало. Устр
- `requirements`: не завелось ли поведение, которого спека не заказывала — тихий
дефолт, проглоченная ошибка, ретрай «на всякий случай», отброшенное поле?
- `security`: читается ли тело ответа внешнего сервиса целиком без предела —
у LLM (8 MiB) и метабаз (4 MiB) предел стоит, и новый исходящий вызов обязан
заводить свой ([security.md](security.md) → «Что вне модели»)
у LLM и метабаз предел стоит ([database.md](database.md) → «Пределы, зашитые
константой кода»), и новый исходящий вызов обязан заводить свой?
- `operations`: гарантия, которую вводит изменение, поставлена на запись или на
чтение — и что будет с данными, записанными до деплоя, которые обычный путь
не перезаписывает? (журнал, 2026-08-10: чистка названия стояла на записи, и
@@ -201,75 +201,50 @@ Go-сервиса и что здесь уже проскакивало. Устр
логика целевых имён мимо `naming`?
([architecture.md](architecture.md) → «Единые точки проекта»)
### Триггеры метки
### Когда звать глубокое ревью
Уточняет умолчания конвейера, не отменяет их. Рабочее умолчание — `medium`;
миграция схемы и публичный контракт метку **не** поднимают: их проверяют
проходы, которые в `medium` и так есть. Списка три: два поднимают до `large`,
по одному на ось, третий опускает до `small`.
Цикл задачи проверяет корректность и механику одним и тем же составом на всякой
задаче. Глубину даёт только отдельный прогон по области —
`av-dev:code-deep-review`, — и **зовёт его человек**. Списки ниже уточняют
признаки, по которым его здесь зовут, а не заводят расписание.
**Крупное здесь** — про объём, сколько узлов и слоёв трогает изменение:
**Области, которые смотрят целиком:**
- перенос ответственности между `worker`, `recognition`, `layout` и `store`;
- новое состояние в графе переходов загрузки: оно тянет за собой воркер, спеку,
отображение в веб-UI и боте и восстановление после рестарта;
- правка, идущая насквозь по цепочке приём → распознавание → раскладка;
- новый провайдер метабазы за существующим интерфейсом: клиент, поле конфига с
образцом, слияние полей кандидата, ветка «провайдера нет».
- **правила идентичности, слияния и разбора** — ключ владения раздачей и сверка
с qBittorrent (`ident`, `internal/store`, `state-reconciliation`); построение
целевых путей и санитизация имён (`internal/layout`, `naming`); разбор
недоверенного входа — bencode, magnet, текст контекста, ответ LLM
(`internal/torrent`, `internal/magnet`, `internal/tgbot/parse.go`); слияние
кандидата метабазы и победитель при конфликте (`internal/metadata`,
`metadata-match`). Сюда задачи возвращаются чаще всего: у одного правила здесь
несколько домов, и правка одного из них молча расходится с остальными.
- **удаление раздачи с файлами** — история инцидентов самая длинная: уборка
своего торрента после отмены сносила чужие файлы (журнал, 2026-08-06), три
дефекта поштучного удаления жили незамеченными до появления пачки и ещё
четыре нашлись на групповой поверхности (журнал, 2026-08-10).
- **граф состояний загрузки и восстановление после рестарта** — новое состояние
тянет за собой воркер, спеку, отображение в веб-UI и боте; расхождение видно
не на диффе, а на живом сервере.
- **код под дорогое решение** — то, на что обопрётся ADR или новая capability:
форму здесь нащупывают по ходу, и цена ошибки не в правке, а в переписывании
решения.
**Незнакомое здесь**про форму решения, которую предстоит нащупать по ходу:
**Необратимое здесь****места кода**, которые после мерджа не откатываются
обратной правкой. Находка в таком месте уходит человеку развилкой, а не чинится
молча. Перечень **операций**, которые спрашиваются у человека всегда, — другой
вопрос и другой дом: [CLAUDE.md](../CLAUDE.md) → «Работа» → «Необратимое».
- новый пакет `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-раскладка при повторном
добавлении раздачи.
- **новая поверхность поверх необратимой операции** — вторая точка входа в
команду, которая удаляет файлы или снимает последнюю копию. Форма решения
здесь нащупывается по ходу: подтверждение, порядок отказов, остаток,
наблюдаемость. Выведено по факту на `bulk-delete-page` (журнал, 2026-08-10):
метка `medium` не дала ни враждебного прохода, ни замера, а именно они нашли
четыре дефекта класса «необратимо».
- «Поведение, видимое снаружи» здесь включает **тексты и карточки Telegram**
для единственного пользователя это и есть интерфейс.
**Место из перечня метку не поднимает — поднимает правило.** Перечни выше
отвечают «здесь такие правила водятся», а не «любая правка здесь идёт в
`large`». Метку поднимает то, что даёт работу новому проходу: заводится ключ
сравнения или меняется его состав; у разбора появляется новый вид входа или
новая ветка неоднозначности; в слияние добавляется источник или меняется
победитель при конфликте.
**Мелкое здесь** — опускает до `small`. Перечень закрытый, каждый пункт
проверяется взглядом на дифф и дельта-спеку, любое сработавшее держит метку
внизу:
- **дельта-спека называет исход поимённо** — сценарий уже говорит, что даёт
вырожденный вход, и решать в коде нечего;
- **новых сценариев в дельта-спеке нет** — изменение уточняет уже описанное
поведение, а не заказывает новое;
- **правка сообщения, комментария, записи журнала, имени или теста** в узле из
перечней выше;
- **сужение уже существующей нормализации** без нового вида входа: вход остался
тот же, изменился исход на одном его значении.
Отрицательный тест поверх перечня: что после мерджа не откатывается обратной
правкой — миграция, формат на диске, публичный контракт, имя, — **не** `small`,
каким бы маленьким ни был дифф.
**Ориентир частоты.** `medium` закрывает большинство задач, `large` рассчитана
на 5–10% и приходится на крупную функциональность, а не на уборку: задача типа
`chore` или `fix`, собранная из нитей прошлого ревью, идёт в `small` или
`medium`, даже когда трогает файл из перечней выше. `large` чаще одной задачи из
десяти означает ошибку в критерии, а не полосу сложных задач подряд.
- **миграция схемы**, уже накатанная на боевую БД umbar;
- **операции, снимающие данные** — `torrents/delete` с `deleteFiles=true` и
снятие последней жёсткой ссылки: обе исключены из гарда сознательно, и второй
точки входа в них не бывает дешёвой (журнал, 2026-08-10);
- **раскладка в библиотеке Jellyfin** — переименование или перезапись файла,
на который уже смотрят клиенты и закладки;
- **публичный контракт** — форма ответа REST, имена полей конфига,
идентификаторы в ссылках: их держат чужие потребители, и переименование
расходится по базе;
- **формат на диске** — раскладка `paths.movies`/`paths.series` и имена, по
которым Jellyfin узнаёт тайтл.
### Недоступно проверке
@@ -283,7 +258,7 @@ Go-сервиса и что здесь уже проскакивало. Устр
ссылки) на текущее поведение.
- `architecture`: суждение «этой функциональности не должно существовать».
- `requirements`: качество распознавания как таковое — правильно ли LLM
определил фильм. Это вопрос тюнинга модели и промпта, а не ревью кода;
определил фильм. Это вопрос настройки модели и промпта, а не ревью кода;
размеченный корпус, по которому это можно было бы судить числом, решено не
собирать (`tasks/REJECTED.md`, 2026-08-06).
@@ -299,10 +274,10 @@ Go-сервиса и что здесь уже проскакивало. Устр
что уже есть в библиотеке»). **Различение «идиоматично против
распространено» теперь не спрашивает никто.** Класс обратимый: портит форму
кода, не данные. Пересмотр — задача `quality-review-agents`.
- `security`, `operations`, `architecture`: на метках `small` и `medium` не
проверяется ничто, требующее запуска, — построенных путей атаки, замеров и
- `security`, `operations`, `architecture`: в цикле задачи не проверяется
ничто, требующее запуска, — построенных путей атаки, замеров и
эксплуатационного постмортема там нет по устройству конвейера. Их даёт только
`large`, а она приходится на 5–10% задач.
отдельный прогон `av-dev:code-deep-review`, который зовёт человек.
## Журнал дефектов
@@ -312,7 +287,7 @@ Go-сервиса и что здесь уже проскакивало. Устр
Форма:
<!-- копия: журнал-дефектов-форма из av-dev-pipeline/skills/review-pipeline/references/review-journal.md -->
<!-- копия: журнал-дефектов-форма из av-dev/skills/code-review/references/review-journal.md -->
## ГГГГ-ММ-ДД — <краткое последствие> [проскочил|пойман]
- **Где:** путь:строка либо «конвейер, а не код»
@@ -351,6 +326,9 @@ Go-сервиса и что здесь уже проскакивало. Устр
удержания общего замка воркера (`280.450631ms` при задержке соседа `300ms`)
- **Что меняем:** в «Триггеры метки», ось «незнакомое», добавлен пункт про новую
поверхность поверх необратимой операции
- **Отменено 2026-09-02:** метка упразднена вместе с переходом на постоянный
состав прогона. Признак живёт в «Когда звать глубокое ревью» → «Области,
которые смотрят целиком»
## 2026-08-10 — три дефекта поштучного удаления жили незамеченными, пока рядом не появилась пачка [проскочил]
@@ -374,6 +352,9 @@ Go-сервиса и что здесь уже проскакивало. Устр
- **Что меняем:** три записи в беклоге со ссылкой на оракулы; триггер метки
дополнен (см. запись выше), чтобы следующая поверхность над необратимой
операцией шла сразу с доказательными проходами
- **Отменено 2026-09-02:** метки нет; вывод перенесён в «Когда звать глубокое
ревью» — удаление раздачи с файлами названо там областью, которую смотрят
целиком
## 2026-08-10 — тест остался зелёным навсегда, потому что проверял снятый атрибут [пойман]
+6 -6
View File
@@ -50,10 +50,10 @@ REST API работают **без авторизации** осознанно;
- **Правило:** компоненты санитизируются (убираются разделители пути, `..`,
управляющие символы), финальный `filepath.Clean`-путь обязан быть **строго
под** соответствующей библиотекой, иначе операция отклоняется. Проверка на
результате, а не на входе. Следом — длина: каждый компонент обязан помещаться в
255 байт UTF-8, иначе задача уходит в `review`. Порядок значим: путь, вышедший
за песочницу, отклоняется как выход за библиотеку, а не как длинное имя, иначе
находка безопасности спряталась бы за косметической причиной.
результате, а не на входе. Длина компонента проверяется следом и только после
проверки принадлежности — порядок и исход нормированы
[file-layout](../openspec/specs/file-layout/spec.md), величина предела —
[database.md](database.md).
- **Исходный путь** = `save_path` из qBittorrent + относительное имя файла из
`/torrents/files`. Тоже недоверенный, но по нему мы только **читаем и
линкуем**; писать в `paths.downloads` нельзя вообще.
@@ -104,8 +104,8 @@ REST API работают **без авторизации** осознанно;
- **Отказ в обслуживании изнутри контура.** Огромная раздача, тысяча файлов,
бесконечный ответ LLM — это вопросы устойчивости и ресурсов
([architecture.md](architecture.md) → «Эксплуатация»), а не безопасности.
Тело ответа внешнего сервиса при этом читается с пределом: LLM — 8 MiB
(`internal/llm`), метабазы — 4 MiB (`internal/metadata`).
Тело ответа внешнего сервиса при этом читается с пределом — величины в
[database.md](database.md) → «Пределы, зашитые константой кода».
- **Целостность содержимого медиафайлов.** Что в контейнере mkv — не наша забота.
- **Цепочка поставки** — модули Go, базовый образ distroless, плагины тулинга.
- **Приватность запросов к внешним сервисам.** Названия раздач уезжают в LLM и