recognize: название из метабазы санитизируется перед попаданием в план

- чистка стоит на каждой точке входа значения метабазы в план — сборка матча,
  копия кандидата для ревью, набор закреплённых значений источника и его
  чтение: гарантия, поставленная только на запись, обходится данными,
  сохранёнными прежними версиями
- название, непригодное как имя каталога (пустое или без единой буквы и
  цифры), не подставляется — раздача уходит в review с названной причиной
- гейт подтверждения матча не сдвинут: сравнение с планом идёт по значениям
  провайдера, чистится только копия, уходящая дальше
This commit is contained in:
av
2026-08-10 10:41:16 +03:00
parent bb278e8744
commit 9aecf757e0
26 changed files with 1653 additions and 53 deletions
@@ -0,0 +1,162 @@
# Отчёт ревью — `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`), а не ревью.
**Альтернативной реализации, с которой можно сдиффить решения, у конвейера нет.**
На этом изменении — правило разбора и санитизации — именно такой проход на прошлом
прогоне приносил независимый оракул.