Files
jellybit/openspec/changes/archive/2026-08-10-metadata-title-sanitize/review/report.md
T
av 9aecf757e0 recognize: название из метабазы санитизируется перед попаданием в план
- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
2026-08-10 10:41:16 +03:00

163 lines
14 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Отчёт ревью — `metadata-title-sanitize`
Стадия: ревью кода после apply, до archive. Отчёт триажа записан оркестратором
(проход `review-triage` пишет только во временный каталог).
## Сводка
| | |
|---|---|
| Размер / сложность / метка | среднее / незнакомое / `large` (максимум по осям) |
| Обоснование метки | триггер «заводится или меняется правило идентичности, слияния или разбора — построение целевых путей и санитизация имён» (`docs/review.md` → «Триггеры метки») |
| Режим | по графу |
| База диффа | `HEAD~1`, рабочее дерево |
| Гейт | зелёный, прогнан триажом самостоятельно: 14 шагов `OK`, ни одного `SKIP`, `-race` реально выполнился, diff-coverage 46/46 строк (100%) |
| Находок на входе | 16 (+1 замер): specs 4, code 3, architecture 3, adversary 3, ops 3, autotests 0 |
| После дедупликации по причине | 11 причин + 1 найденная самим триажом |
| Итог | 0 блокирующих, 1 «стоит исправить сейчас», 3 гипотезы, 2 promote |
Сигнал о заниженной метке: `review-code` возражений не подал. Второго,
независимого от него корректора на этом прогоне не было — `basics` не
запускался, своих тем проекта не размечено.
## План разметки и исход по каждой теме
| тема | дом | глубина | закрывает | исход |
|---|---|---|---|---|
| requirements | дельты change + `openspec/specs/{metadata-match,recognition}` | разбор | specs | закрыта, 4 находки (minor) |
| autotests | `CLAUDE.md` → «Гейт» | — | autotests | закрыта, 0 находок |
| conventions | `docs/conventions/*` | разбор | code | закрыта, 3 находки (minor), нарушений конвенций 0 |
| architecture | `docs/architecture.md` + источник `docs/passport.md` | доказательство | architecture | закрыта, 3 находки (minor) |
| security | `docs/security.md` | доказательство | adversary | закрыта, 2 построенных пути (major) + 1 свойство |
| operations | `docs/architecture.md` «Эксплуатация» + источник `docs/database.md` | доказательство | ops | закрыта, 3 постмортема (2 major) + 1 замер |
**Тем без отчёта нет.** Тем без дома план не содержал.
## Что стало с каждой причиной
| # | причина | кто нашёл | статус |
|---|---|---|---|
| 1 | превью источника расходится с применением | architecture + adversary | закрыта: общий дом пинов `sourcePins`; `TestBuildSources_PreviewMatchesApply` |
| 2 | кандидат, лежащий в БД грязным, пиннит несанитизированное название | specs + code + adversary | закрыта: чистка в `sourcePins`; `TestChooseCandidate_DirtyLegacyTitleSanitized` |
| 3 | пин, закреплённый до деплоя, читается дословно | ops | закрыта: чистка на чтении в `applyOverrides`; `TestApplyOverrides_LegacyDirtyPinSanitized` |
| 4 | предикат пригодности считается дважды из сырого значения | specs + code | закрыта: `decide` читает уже чистое `Match.Title` |
| 5 | отказ закрепить название молчит | specs + code | закрыта: атрибут `title_pinned` |
| 6 | `Match.Title` остаётся сырым, чистка в двух местах | architecture | закрыта: единственная точка сборки `buildMatch` |
| 7 | `specs/review` обещает закрепление безусловно | specs | закрыта: заведена дельта `review` |
| 8 | грязный каталог-якорь наследуется дальше | ops | не чинится сознательно, см. H1 |
| 9 | название от LLM гейта пригодности не получает | adversary | вне объёма, см. P1 |
| 10 | набор пинов пишется пятью транзакциями | ops | пре-существующее, см. H2 |
| 11 | `resolveFolderBase` даёт `SCAN` по `recognition` | ops (замер) | пре-существующее, см. H3 |
| 12 | режиссёр и название доезжают до карточки с RLO и zero-width | **триаж** | закрыта: `naming.sanitize` снимает Cf |
## Блокирует мердж
Пусто. Обе `major`-находки враждебного прохода и обе `major` эксплуатационного
отработаны, каждая с падающим-без-правки тестом в дереве. Потолок не срабатывал.
## Стоит исправить сейчас — 1 находка (отработана)
### Название и режиссёр из метабазы доезжают до карточки Telegram, шапки веб-UI и имени раздачи с RLO и zero-width
- Файл: `internal/naming/naming.go`, `sanitize`
- Severity: `minor` · Confidence: `high`
- Оракул (прогон триажа): `director = "DenisVilleneuve"`
`label = "Dune (DenisVilleneuve, 2021)"`; `naming.sanitize` снимал только
C0 (`r < 0x20`) и не трогал категорию Cf.
- Последствие: дельта `recognition` этого изменения обосновывает отказ чистить
`director` в плане тем, что «его очистка применяется при рендере отображаемого
имени». Рендер этой гарантии для Cf не давал — то есть изменение записывало в
спеку свойство, которого в коде нет. RLO из credits разворачивает отображение
имени в карточке Telegram, которая для единственного пользователя и есть
интерфейс.
- Найдено проходом: триаж, при добыче оракула на утверждение дельты. Ни один
проход этого не заявлял: тема `requirements` сверяла дельту с кодом изменения,
а обоснование дельты уводило в пакет, которого дифф не касался.
- **Отработано инлайн:** `naming.sanitize` снимает `unicode.Cf`, тест
`TestSanitize_StripsFormatChars`.
## Гипотезы без доказательства
**H1. Грязный каталог-якорь наследует имя всем последующим раздачам того же
матча.** Оракула нет (постмортем-рассуждение), severity снижена до `minor`.
Предложение прохода — чистить базу, извлечённую из якоря, — дало бы вторую папку
рядом с существующей, то есть исход, против которого затевалась задача.
`planBase` и так гоняет `sanitizeComponent` по `FolderBase`, на уровне ФС база
безопасна; речь только о невидимке внутри имени существующей папки. Задача не
заводится: лечение — переименовать папку руками, записано риском в `design.md`.
**H2. Набор пинов пишется пятью отдельными транзакциями.** Пре-существующее,
диффом не заведено (дифф добавил только присваивание переменной ради лог-атрибута).
Окно — миллисекунды между пятью `UPSERT` в локальный SQLite, пути отказа никто не
строил. Дом — цель `state-integrity`.
**H3. `resolveFolderBase` даёт `SCAN` по `recognition` без индекса на
`(provider, provider_id)`.** Замер прохода `ops`: 12.9 мс при 100 загрузках и
20000 `file_link`. Команды замера в выводе прохода нет, триаж его не
воспроизводил — число несётся как заявленное, не как проверенное.
Пре-существующее; дом есть — `tasks/items/scale-100-downloads.md`.
## Promote candidates
**P1. Гейт пригодности названия стоит у источников, а не на границе `layout`.**
Путь построен триажом: `title="-"` → каталог `- (2021)`; `title="…"` → каталог
`… (2021)`; `title="..."``layout: empty title after sanitization`;
`title="etcpasswd"` → каталог `etcpasswd (2021)`. Инвариант «целевой путь
строго под библиотекой» **не нарушен** (`underRoot` держит, полноширинный `` не
разделитель), «существующее не перезаписываем» тоже (коллизия → review). Отсюда
`minor`. Претензия к правилу: `UsableTitle` зовут три вызывающих со стороны
метабазы, а настоящая граница — вход в `layout`. Класс тот же, что у задачи
`long-title-to-review`; расширить её, а не заводить новую.
**P2. `ident.Parse` на входных границах остаётся правилом без механизации.**
Стоячий разрыв из `CLAUDE.md`, кандидат в `internal/archrules`.
## Отсев вкусовщины
По проектному списку «Типовые ложноположительные» не выброшено ничего — ни одна
входящая находка под его пункты не подошла. По общим критериям выброшены две:
«правило разложено по трём пакетам» (после переноса чистки последствия за
пределами чтения не осталось, а отдельный дом правила у `layout` дельта
`recognition` легитимизирует дословно) и «атрибут `title_pinned` конфлатит два
случая» (исход в обоих один, последствия нет).
## Границы покрытия
**Запускалось:** `autotests`, `specs`, `code`, `architecture`, `adversary`, `ops`,
`triage` — метка `large`, режим по графу. Стадия ревью дизайна прогонялась
отдельно составом `specs`, `rubric`, `architecture`; её находки отработаны до
реализации.
**Не запускалось:** `basics` — своих тем проекта не размечено, принимать нечего;
следствие — второго, независимого от `code`, сигнала о заниженной метке не было.
Проход независимой реализации в конвейере отсутствует (снят по стоимости),
`idiom` упразднён 2026-08-04.
**Ни один из шести проходов не сообщил свой потолок и величину среза.** По
молчанию «показал всё» неотличимо от «показал первые N» — это находка о прогоне.
**Правки оркестратора не видел ни один проход.** Все шесть отчётов сняты с
состояния кода до исправлений; семь закрытых причин — правки, по которым ревью не
проводилось. Их проверка сводится к зелёному гейту со 100% diff-coverage, трём
новым падающим-без-правки тестам и чтению кода в точках единственности
(`buildMatch`, `sourcePins`, `applyOverrides`). Находка №12 — прямое следствие:
она живёт в пакете, которого дифф не касался.
**Не проверит ни один проход** (`docs/review.md` → «Недоступно проверке»):
история инцидентов на umbar; поведение SQLite под реальным объёмом и профилем;
завязка внешних потребителей (Jellyfin, закладки) на текущее поведение; суждение
«этой функциональности не должно существовать»; качество распознавания как
такового.
**Перестали проверять сознательно:** идиоматичность Go — с 2026-08-04, различение
«идиоматично против распространено» не спрашивает никто; класс обратимый,
пересмотр — задача `quality-review-agents`.
**Решения и наблюдения проекта прогон не читал:** `docs/adr/` и `docs/research/`
процессные документы. Расхождение изменения с записанным решением ловит сверка
документации (`av-dev-docs:healthcheck`), а не ревью.
**Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.**
На этом изменении — правило разбора и санитизации — именно такой проход на прошлом
прогоне приносил независимый оракул.