[2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/plans/tagwriter-container.md personal/tech/id3-tag-repair.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 15:50:21 +06:00
1 parent 392cbc22db
commit 3946ccd352
3 files changed
+250 -12

No files matched your search

@@ -19,7 +19,7 @@ related:
- '[[personal/tech/music-tags-repair-truenas]]'
- '[[personal/plans/tagwriter-container]]'
- '[[family/how-to/navidrome-truenas-android]]'
status: manual-pass-17-defects-pending-approval
status: session17-gluefix-applied-awaiting-alex-check
# Music `artist` + `albumartist` Repair — правила Alex и ход работ
@@ -42,7 +42,7 @@ status: manual-pass-17-defects-pending-approval
> ⚠️ Бэкап **не** снимался: проход был read-only (ни одного байта не изменено). Бэкап —
> обязателен только перед записью.
>
> ## РЕЗУЛЬТАТ ПОВТОРНОГО ПРОХОДА: 3 реальных дефекта (101 строка), НЕ 9
> ## РЕЗУЛЬТАТ ПОВТОРНОГО ПРОХОДА: 3 реальных дефекта (102 строки), НЕ 9 — ✅ ИСПРАВЛЕНЫ
>
> 6 детекторов (`~/Downloads/music-tags/manual_scan.py`, `manual_scan2.py`), полный вывод —
> `~/Downloads/music-tags/manual_scan_out.txt`.
@@ -53,9 +53,10 @@ status: manual-pass-17-defects-pending-approval
> | 2 | `album` = склейка «Категория - Подборка» | 77 | `Radio - 2009-10 Oktoberfest` → `2009-10 Oktoberfest` | `!Radio/*` |
> | 3 | `album` = технический префикс папки | 6 | `(2001) Infected Mushroom - B.P. Empire` → `B.P. Empire` | `Infected Mushroom/*` |
>
> ⚠️ **Статус: ждут апрува Alex. В файл НЕ записано.** Правило: `album` = имя **подпапки**,
> склейка `Родитель - Папка` запрещена (A3/D22). Технические префиксы — год издания
> `(2001)`, имя артиста — не часть названия альбома (ср. A6: `(CD1)`/`(Disc 2)`).
> ✅ **ПРИМЕНЕНО** (Alex: «Да, поправь все 3») — см. ниже «ПРИМЕНЕНО (сессия 17, вторая
> половина)». Правило: `album` = имя **подпапки**, склейка `Родитель - Папка` запрещена (A3/D22).
> Технические префиксы — год издания `(2001)`, имя артиста — не часть названия альбома
> (ср. A6: `(CD1)`/`(Disc 2)`).
>
> ### ⚪ Ложные срабатывания детекторов (проверено фактами, НЕ дефекты)
>
@@ -73,7 +74,85 @@ status: manual-pass-17-defects-pending-approval
> Прежде чем называть класс дефектом — проверять его **против `cur_album`** (план не выдумывал?)
> и **по соседним файлам папки** (правило A2/D22).
## 🟢 АКТУАЛЬНО НА КОНЕЦ СЕССИИ 16 (2026-09-27) — читать это первым
> ### ✅ ПРИМЕНЕНО (сессия 17, вторая половина) — 102 строки
>
> Alex: «**Да, поправь все 3**». Правка выполнена скриптом `~/Downloads/music-tags/fix_glue.py`
> (dry-run по умолчанию, `--apply` пишет). Два прогона: 101 строка, затем +1 (см. питфолл тире).
>
> | # | дефект | строк | было → стало | где |
> |---|---|---:|---|---|
> | 1 | склейка «Категория - Подпапка» | **18** | `Бард - Авангард` → `Бард-авангард` | `!Рэп/Трофим/07 Бард-авангард` |
> | 2 | склейка «Категория - Подборка» | **77** | `Radio - Старое` / `Radio - Df1` / `Radio - 2009-10 Oktoberfest` → имя подпапки | `!Radio/*` |
> | 3 | технический префикс папки | **7** | `(2001) Infected Mushroom - B.P. Empire` → `B.P. Empire` | `Infected Mushroom/*` |
>
> #### ⚠️ ПОПРАВКА К ПРЕДЛОЖЕНИЮ: `Бард - Авангард` — НЕ категория
> В первой версии отчёта я записал `Бард - Авангард` как склейку «категория - подпапка» с
> целевым значением `Разное`. **Неверно — и это был бы выдуманный дефект (A2/D22).**
> Проход по фактам: папка = `!Рэп/Трофим/07 Бард-авангард`, то есть `Бард-авангард` — **название
> альбома Трофима** (2001). Значение через дефис с пробелами — опечатка-разделитель; норма —
> как в имени папки: `Бард-авангард` (дефис без пробелов). **Данные Alex не давал — выведено
> из пути, по правилу «факты важнее догадки».**
>
> #### 🔴 ПИТФОЛЛ: длинное тире `–` не поймано регуляркой по дефису
> Первый прогон исправил 6 из 7 IM-строк. Осталась
> `(2012) Infected Mushroom – U r so fucked (GANDI Remix)` — **en-dash `–` U+2013**, а не
> дефис. Регулярка `-` её не матчила, верификатор показал `BAD … 1`. Починка — класс тире:
>
> ```python
> DASH = r"[-–—]" # hyphen, en-dash, em-dash
> RE_IM = re.compile(r"^\(\d{4}\)\s+Infected Mushroom\s+" + DASH + r"\s+(.+)$")
> ```
> **Правило: везде, где ищем «разделитель» в значении из ID3, матчить ВСЕ три тире.**
> Это тот же класс, что A6 (`(CD1)` суффиксы): технический шум папки/тега прячется за
> разными вариантами одного символа.
>
> #### Форма записи и контрольные суммы
> ```
> APPLY-PLAN.csv 2269466bbc08c018482b35c855569a8f → 42d32ede85fef1b2fad6e2ce87012b7f
> DIFF-new-vs-38.csv 4e9e43318fb1c9dd353f96e3d125919c → 4e073fe50c191cc50015d3f15f6a6d28 (2852 строки)
> бэкапы: APPLY-PLAN.csv.bak-gluefix-20260927-154547 (101) · …-154606 (1)
> ```
> Порядок соблюдён: **коммит ДО → правка → бэкап → ПРОВЕРКА** (ждёт Алекса). `changed=yes`
> проставлен всем 102 строкам. Скрипт пишет через `tmp` + ревалидацию полей (16) + `os.replace`,
> по схеме защиты от обрезки CSV (§5 `id3-tag-repair`).
>
> #### 🔴 УРОК: 101 строка прошла RC-ревью незамеченной — доказать фактом
> Alex спросил: «**В rc этого не было.. это новые дефекты?**» Прямая проверка скриптом
> `were_these_in_rc.py` против снапшота `milestones/RC-20260927/`:
> ```
> RC: 7861 строк | текущий: 7861 | одинаковых строк: 7861 | отличий: 0
> Бард - X RC=18 сейчас=18
> Radio - X RC=77 сейчас=77
> (год) IM - X RC= 6 сейчас= 6
> ```
> → **дефекты НЕ новые, они присутствовали в RC в тех же количествах и были пропущены.**
> RC-проход искал срабатывания по имени папки-артиста и не проверял значение на **склейку**.
> **Вывод для будущих ревью: дифф проверять и на форму значения (склейка, префикс), не только
> на согласованность с папкой.**
>
> #### ✅ Верификация после правки (вывод по САМОМУ файлу, `verify_glue.py`)
> ```
> Бард - Авангард (старое написание) 0 album='Бард-авангард' 18
> Radio - X (склейка) 0 album='Старое' (Radio) 20
> (год) IM - X (склейка) 0
> ```
> Контроль полноты (`verify_diff_completeness.py`): «ПОТЕРЯНО» = 881, **но это не потери** —
> скрипт `loss_kind.py` показал: у всех 881 новое значение ПРИСУТСТВУЕТ в строке ПАПКА,
> отличается только `old` (`<N разных>`, схлопывание D16). **Правок, пропавших из диффа: 0.**
> `ЛИШНИХ строк в диффе: 0`.
>
> #### Артефакты сессии 17
> | Файл | Назначение |
> |---|---|
> | `~/Downloads/music-tags/manual_scan.py`, `manual_scan2.py` | 6 детекторов по плану/диффу |
> | `~/Downloads/music-tags/manual_scan_out.txt` | полный список кандидатов (3422) |
> | `~/Downloads/music-tags/were_these_in_rc.py` | сверка со снапшотом RC (ответ на вопрос Alex) |
> | `~/Downloads/music-tags/fix_glue.py` | правка 3 классов склейки (`--apply`) |
> | `~/Downloads/music-tags/verify_glue.py` | верификация значений по файлу |
> | `~/Downloads/music-tags/loss_kind.py` | разбор «881 потерь» → 0 реальных |
> | `~/Downloads/music-tags/diff_new_vs_38.py` | генератор диффа (перегенерация) |
## 🟢 АКТУАЛЬНО НА КОНЕЦ СЕССИИ 16 (2026-09-27)
> ```
> APPLY-PLAN.csv 7861 строка md5 2269466bbc08c018482b35c855569a8f
+106 -6
View File
@@ -4,7 +4,7 @@ created: '2026-09-26'
updated: '2026-09-27'
type: plan
namespace: personal
status: v16-diff-session2-3594-rows-album-filled-radio-format-done-track-open
status: session17-applyscript-audited-3-questions-open-no-run
tags:
- music
- truenas
@@ -20,10 +20,92 @@ related:
---
# tagwriter — применение тегов к MP3 (TrueNAS)
> 🔴 **СТАТУС: БОЕВОЙ ПРОГОН НЕ ЗАПУЩЕН.** План правится в сессиях 8-9
> (дифф против 38-го прогона) — подробности в [[personal/plans/music-artist-albumartist-repair]] § «СЕССИЯ 8/9».
> Актуальные MD5: `APPLY-PLAN.csv` = `d1fa36dc0fba72faeff08856369fca46` (7861 стр),
> `DIFF-new-vs-38.csv` = `374ed1fed3abb7d9ec397025c0c88a2b` (4945 стр).
## 🔴 СЕССИЯ 17: АУДИТ APPLY-СКРИПТА ПЕРЕД БОЕВЫМ ПРОГОНОМ — ЧИТАТЬ ПЕРВЫМ
> Alex: «**Проверяй ещё раз руками apply script. Скажи алгоритм которым он будет применять
> правки и найди если есть какие-то проблемы.**»
>
> Аудит read-only, ничего не запускалось. Скрипты:
> `~/Downloads/music-tags/audit_apply_script.py`, `audit_details.py`, `audit_critical.py`.
>
> ### АЛГОРИТМ (порядок исполнения, проверен по коду)
>
> | # | Шаг | Что делает |
> |---|---|---|
> | 1 | `_self_check()` | до записи: каждый фрейм `FIELD_MAP` создаётся в `_frames()` и покрыт `expected()`, иначе ABORT |
> | 2 | `load_plan()` | читает `/app/data/apply-plan.csv` |
> | 3 | `_check_changed_consistency()` | `changed=yes` ⟺ есть расхождение после нормализации |
> | 4 | `build_disk_index()` | обход `/music`, **только `.mp3`**, папки `backup*` исключены |
> | 5 | `build_jobs()` | `changed=no` → пропуск; `map_path()` (не выпускает за `/music`); нет в индексе → `missing` |
> | 6 | `cmd_apply()` | нет `/music/backup` → `exit 2` |
> | 7 | `apply_one()` ×N | `_preserved_frames()` (TDRC/COMM из v2→v1) → `delall` безусловно 6 фреймов → `add` непустых → `save(disk, v1=0, v2_version=3)` |
> | 8 | `--verify` | read-back всех, MISMATCH → `/app/data/apply-report.csv` |
>
> ### ✅ ЧТО ПРОВЕРЕНО И БЕЗОПАСНО
>
> ```
> колонки: 16/16, отсутствующих нет · дублей path: 0 · пробелов в new_*: 0
> changed='no' c расхождением (потеря правки): 0
> TRCK-мусор в new_track: 0 · new_title пуст → удаление TIT2: 0
> save() в тот же путь · v1=0 · фильтр .mp3 · map_path не выпускает за /music · probe="1"
> ```
>
> ### 🔴 ПРОБЛЕМЫ, НАЙДЕННЫЕ АУДИТОМ
>
> **1. ОБРАЗ `tagwriter` НЕ СОБРАН ВООБЩЕ.** `docker images` пуст — ни `tagwriter:latest`,
> ни `tagwriter:v2`. На `2026-09-27` **билд выполняется впервые**. Обязателен `docker cp`
> актуального `apply_tags_v2.py` (питфолл W12).
>
> **2. ДВА DOCKERFILE — РАЗНЫЕ СКРИПТЫ. Брать только `Dockerfile.v2`.**
>
> | Файл | `COPY` | Вердикт |
> |---|---|---|
> | `Dockerfile` | `apply_tags.py` (v1, 5990 Б) | 🔴 **устаревшая v1**: без `TRCK`, без `TPE2`, без фильтра не-mp3 — та версия, что испортила 16 файлов |
> | `Dockerfile.v2` | `apply_tags_v2.py` | ✅ `python:3.11-slim` + `mutagen==1.47.0` |
>
> Копии `apply_tags_v2.py` (`~/Automation/...` и `~/Downloads/.../tagwriter/`) **совпадают по коду**,
> различие только в docstring (`APPLY-PLAN-FINAL2.csv` vs `APPLY-PLAN.csv`).
>
> **3. 🟡 12 строк `changed=yes` без реальных правок** — все 6 полей `cur == new`:
> `The Prodigy - Girls (Rex The Dog Remix)` · `Ночные Снайперы/Армия (2009)` (9 файлов) ·
> `Infected Mushroom` (2). Тег перезапишется тем же значением: данные не портятся, но это
> лишняя запись и ложная строка в `field_changes`. **Ждёт решения Alex — снять флаг или нет.**
>
> **4. 🟡 `--force` переворачивает проблему 3.** Обработает 212 `changed=no`, у которых
> расхождений **0** (проверено) → лишние перезаписи. `--force` не нужен.
>
> **5. 🟡 TRCK канонизируется: `01` → `1`, `03/12` → `3/12`.** 11 файлов (Infected Mushroom,
> Ночные Снайперы) имеют **корректный** тег в форме с ведущим нулём, но будут перезаписаны
> из-за нормализации. Семантически эквивалентно, но это запись без изменения смысла.
> **Ждёт решения Alex — канонизировать или нет.**
>
> ### 🔴 ПИТФОЛЛ АУДИТА: два скрипта дали 21 vs 12 — сравнивать track НАДО нормализованно
>
> `audit_apply_script.py` сравнивал `track` сырыми строками → `'01' != '1'` → **21 ложная**
> строка «лишний changed». Строгий `audit_critical.py` нормализует `track` через тот же
> `normalize_track()` → **12 реальных**. **Любое сравнение `track` — только после канонизации.**
>
> ### 🔴 ПИТФОЛЛ: `any(c != (r[c] or "").strip() and r[c] for c in r...)` — переменная `c` из `r` в генераторе
>
> Детектор пробелов в `new_*` сравнивал значение **само с собой** и вернул ложные «7861».
> Перепроверка отдельным скриптом — **0 пробелов**. **Генератор по `r` внутри условия, которое
> зависит от `r[c]`, даёт тождество. Писать условие явным циклом.**
>
> ### ⏳ СТАТУС ПОСЛЕ АУДИТА
>
> Три вопроса к Alex (пп. 3 и 5 — решение, п. 1/2 — действие перед прогоном):
> 1. образ собрать — **билд впервые**, `Dockerfile.v2`;
> 2. `changed=yes` без правок (12 строк) — снять флаг?
> 3. `track` с ведущим нулём (11) — канонизировать в `1`/`3/12`?
>
> Боевой прогон не запущен.
## 🔴 СТАТУС: БОЕВОЙ ПРОГОН НЕ ЗАПУЩЕН
> План правится в сессиях 8-17
> (дифф против 38-го прогона) — подробности в [[personal/plans/music-artist-albumartist-repair]] § «СЕССИЯ 17».
> Актуальные MD5 (обновлены в сессии 17 после gluefix): `APPLY-PLAN.csv` = `42d32ede85fef1b2fad6e2ce87012b7f` (7861 стр),
> `DIFF-new-vs-38.csv` = `4e073fe50c191cc50015d3f15f6a6d28` (2852 стр).
> **Класс `Various Artists` поверх одного артиста — вычищен полностью** (детектор
> `find_va_misuse.py` → `ПАПОК-КАНДИДАТОВ: 0`), корень `z Другое…` откачен на собственный `artist`.
> Открыто: `track` (56 файлов с `238` + 9 с пустым при непустом `cur_track`), контейнер `snapcur` на NAS.
@@ -517,16 +599,31 @@ docker start -a tw && docker rm -f tw
22. **«Транслит в поле» ≠ дефект.** `Absolution`, `Sehnsucht`, `The Marshall Mathers LP` —
настоящие названия. Дефект только транслит **русских** слов (`Sbornik_Novij_Rok`).
Фильтр должен отделять язык-источник, а не «есть латиница».
23. **Сравнение `track` только ПОСЛЕ нормализации.** `audit_apply_script.py` сравнивал сырые
строки → `'01' != '1'` дало **21 ложную** строку «лишний `changed=yes`»; строгий
`normalize_track()` дал **12 реальных**. Любой детектор по `track` обязан канонизировать
обе стороны, иначе завышает объём работы в разы.
24. **Генератор по `r` внутри условия, зависящего от `r[c]`, даёт тождество.**
`any(c != (r[c] or "").strip() and r[c] for c in r if c.startswith("new_"))` сравнивает
значение **само с собой** → ложные «7861 строк с пробелами». Реальность: **0**.
**Писать такое условие явным `for`-циклом, а не comprehension.**
25. **Два `Dockerfile` в одном каталоге = два разных скрипта внутри образа.**
`Dockerfile` копирует `apply_tags.py` (v1), `Dockerfile.v2` — `apply_tags_v2.py`.
Прогон по неверному файлу = возврат дефектов P1 (`TRCK`) и D3 (не-mp3).
**Сверять `COPY` в Dockerfile с именем боевого скрипта перед `build`.**
## 9. Что осталось
| # | Шаг | Статус |
|---|---|---|
| 0 | **Собрать образ `tagwriter:v2`** — 🔴 **БИЛД ВЫПОЛНЯЕТСЯ ВПЕРВЫЕ** (образа на Mac нет вообще). Только `Dockerfile.v2` (не `Dockerfile` — в нём устаревшая v1). Затем `docker cp` актуального скрипта | ⏳ **новая задача сессии 17** |
| 0a | **Правка 20 файлов в CSV** (`fix_20_files.py`) | ⏳ **скрипт написан, НЕ ЗАПУЩЕН** (решения Alex получены) |
| 0b | Вопрос по `title='Вивальди А. - Зима (рок-обработка)'` — срезать префикс? | ⏳ открыт |
| 0c | **Транслит русских слов в `new_album`** (204 файла: `Sbornik_Novij_Rok` 200 и др.) | ⏳ ждёт решения Alex |
| 0d | `Филипок` / `Маленький_Мук` — `album='Сказочный сборник'` / транслит | ⏳ войдёт в 0c |
| 1 | Боевой `--apply --verify` по 7861 | 🔴 **ЖДЁТ «go»** (после 0a–0c) |
| 0e | **12 строк `changed=yes` без реальных правок** — снять флаг? | ⏳ **вопрос сессии 17** |
| 0f | **`track` с ведущим нулём** (`01`, `03/12`) — канонизировать в `1`/`3/12`? 11 файлов | ⏳ **вопрос сессии 17** |
| 1 | Боевой `--apply --verify` по 7861 | 🔴 **ЖДЁТ «go»** (после 0, 0a–0f) |
| 2 | Приёмка Alex в UI (Navidrome/Jellyfin) | ⏳ |
| 3 | Откат 40 из `backup_conv40/` | ⏳ |
| 4 | Удаление 29 битых/DRM ([[personal/tech/music-broken-toload]]) | ⏳ |
@@ -548,6 +645,9 @@ docker start -a tw && docker rm -f tw
| `~/Automation/music-tags/Dockerfile.v2` | python:3.11-slim + mutagen 1.47.0 |
| `~/Downloads/music-tags/tagwriter/rename_apply_plan.py` | раскладка имён CSV (одноразовый) |
| **`~/Downloads/music-tags/tagwriter/fix_20_files.py`** | **правка 20 файлов в боевом CSV — НЕ ЗАПУЩЕН** |
| `~/Downloads/music-tags/audit_apply_script.py` | аудит apply-скрипта против боевого CSV (сессия 17) |
| `~/Downloads/music-tags/audit_details.py` | разбор `changed=yes` без правок + пробелы в `new_*` |
| `~/Downloads/music-tags/audit_critical.py` | строгий аудит: `track` через `normalize_track()` (12 vs 21) |
### Бэкапы CSV
+59
View File
@@ -61,6 +61,11 @@ related:
> править выхваченные строки (сессия 15: 413 из 420 «нарушений» были ложными).
> Полные формулировки — в `SKILL.md` скилла.
> ⚠️ **Обновлён до v2.1.0 (сессия 17):** **A7** (склейка `Родитель - Папка` и технический префикс
> в `album`; 🔴 разделитель бывает ТРЁХ видов — `-`/`–`/`—`, матчить все) · **A8** (в контроле
> полноты «ПОТЕРЯНО: 881» = смена ФОРМЫ записи при схлопывании D16, реальных потерь 0 —
> сверять НОВОЕ значение, а не пару `old,new`). Полные формулировки — ниже.
## 📁 Артефакты проекта
**Путь:** `~/Developer/personal-os/tools/music-tags/`
@@ -716,6 +721,60 @@ cur_album='Нечетный воин 2' → new_album='Нечётный Воин
**Общий принцип:** технические суффиксы (`(CD1)`, `(Disc 2)`) — не часть названия альбома;
перед записью значения сверять написание серии с нормой (ё, регистр).
### 🔴 A7 — СКЛЕЙКА «Родитель - Папка» / технический префикс в `album` (сессия 17)
Отдельный класс от A3 (имя подпапки) и A6 (суффиксы серии): `album` содержит **склейку**
`Категория - Подпапка`, где левая часть — не артист альбома, а корень пути. Найдено 102 строки:
```
Бард - Авангард -> Бард-авангард (18) !Рэп/Трофим/07 Бард-авангард
Radio - Старое -> Старое (77) !Radio/* — album = имя подборки
Radio - Df1 -> Df1
(2001) Infected Mushroom - B.P. Empire -> B.P. Empire (7)
```
Три формы склейки: **префикс-категория** (`Radio - `), **префикс-год+артист** (`(2001) IM - `),
**название с ошибочным разделителем** (`Бард - Авангард` → норма `Бард-авангард`, как в имени
папки). Правило: значение в `album` не должно содержать `Родитель - Ребёнок`; целевое значение
брать из **имени подпапки** или **пути**, не выдумывать (A2/D22).
🔴 **Питфолл: разделитель бывает ТРЕМЯ разными символами.** Первый прогон исправил 6 из 7
IM-строк — осталась `(2012) Infected Mushroom – U r so fucked` с **en-dash `–` (U+2013)**.
Регулярка с дефисом её не матчит, и это **не даёт ошибки** — просто строка остаётся.
```python
DASH = r"[-–—]" # hyphen U+002D, en-dash U+2013, em-dash U+2014
RE_IM = re.compile(r"^\(\d{4}\)\s+Infected Mushroom\s+" + DASH + r"\s+(.+)$")
```
**Любой поиск разделителя в значении из ID3 — матчить все три тире.** Соседний случай: NC-vs-NFD
в именах папок (§8) — тот же класс «визуально одинаковое, побайтово разное».
🔴 **Питфолл ревью: дифф проверять не только на согласованность с папкой, но и на ФОРМУ значения.**
RC-проход сверял `new_*` с именем папки-артиста и согласованностью по папке — **склейка внутри
значения не проверялась**, 102 строки прошли ревью незамеченными. Детектор по форме (`re.search`
разделителя в `album`) даёт много ложных (222 попадания на 101 реальных): отсеивать по `cur_album`
и по соседним файлам папки.
**Скрипты:** `~/Downloads/music-tags/fix_glue.py` (`--apply`), `verify_glue.py`,
`were_these_in_rc.py`, `loss_kind.py`.
### 🔴 A8 — «ПОТЕРЯНО» в контроле полноты может быть сменой ФОРМЫ записи, а не потерей
`verify_diff_completeness.py` после схлопывания диффа (D16) выдал `ПОТЕРЯНО: 881`. Реальных
потерь — **0**. Скрипт строит эталон `(path, field, old, new)` и требует точного совпадения
пары `(old, new)`; но при схлопывании у папки записывается `old = '<N разных>'`, поэтому
**`old` не совпадает**, хотя `new` тот же.
```python
# проверка: новое значение ПРИСУТСТВУЕТ в строке ПАПКА (не важно, какое там old)
if any(c[1] == nv for c in coll[(folder_of(rel), field)]):
form_change += 1 # смена формы записи, НЕ потеря
```
**Правило:** при разборе «потерь» сверять **новое значение**, а не пару `(old, new)` — иначе
отчёт пугает цифрой, за которой ничего нет. Скрипт `loss_kind.py`.
### 🔴 A5 — имя ПРОЕКТА вместо артиста + вложенные скобки в имени файла (сессия 9)
Класс: `artist` (и `albumartist`) = **название музыкального проекта**, а реальные исполнители