From 042cac021645cbd8f80a39b243c509029195e6b1 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sun, 27 Sep 2026 13:58:58 +0600 Subject: [PATCH] [2026-09-27] eagle: personal/plans/music-artist-albumartist-repair.md personal/tech/id3-tag-repair.md --- .../plans/music-artist-albumartist-repair.md | 168 +++++++++++++++++- personal/tech/id3-tag-repair.md | 91 ++++++++++ 2 files changed, 258 insertions(+), 1 deletion(-) diff --git a/personal/plans/music-artist-albumartist-repair.md b/personal/plans/music-artist-albumartist-repair.md index 6fc25f62..fd018a8a 100755 --- a/personal/plans/music-artist-albumartist-repair.md +++ b/personal/plans/music-artist-albumartist-repair.md @@ -4,7 +4,7 @@ created: '2026-09-27' updated: '2026-09-27' type: plan namespace: personal -status: session3-done-0-empty-fields-diff-3595-rows +status: session4-done-diff-rebuilt-4864-rows-0-losses tags: - music - metadata @@ -53,6 +53,21 @@ related: > в разделе «🔷 СЕССИЯ 3» в конце документа: **`bb724716c7ffe45619b76abbb3deb5cc`**. > Старые `245efb55…`, `8367084e…` более не верны. Пустых полей в плане — **0**. > +> ## 🔴 ДИФФ ПЕРЕОПРЕДЕЛЁН (сессия 4, вечер) — правила изменились +> +> Актуальные правила диффа и **все три дефекта** старой версии скрипта — в разделе +> «🔷 СЕССИЯ 4» в конце документа. Кратко: +> +> | Правило | | +> |---|---| +> | Появление новой колонки (`albumartist`, `genre`) | ✅ **ЭТО ПРАВКА**, не выкидывать | +> | Схлопывание в ПАПКА | только если у **ВСЕХ** файлов совпадает **и `old`, и `new`** | +> | «Доминирующее большинство» | 🔴 **ЗАПРЕЩЕНО** (давало 92 искажённые папки) | +> | Полнота | `assert` в скрипте + независимый `verify_diff_completeness.py` | +> +> Актуальный дифф: **4864 строки**, md5 `0d7a31ca529b2ce2e7f018816ea0ff8e`. +> Все цифры состава диффа в разделах ниже (3594 / 3595 / 3865 строк) — **УСТАРЕЛИ**. +> > Снимок каталога перед переименованием: `/Users/admin/Downloads/music-tags/milestones/music-csv-BEFORE-RENAME-20260927-124542.tar.gz`. > > ## 🔴 АУДИТ `new_artist` ВЫЯВИЛ 20 ДЕФЕКТОВ @@ -2152,4 +2167,155 @@ bb724716… ⬅ ТЕКУЩИЙ, + Сосни попса (2) + Bimbo Jones (1) - **Сравнение `cur_*` между двумя CSV бессмысленно** — это два разных снимка. Проверять состояние тегов можно **только** чтением файлов. +--- + +## 🔷 СЕССИЯ 4 (2026-09-27): ПЕРЕСБОРКА ДИФФА — ПРАВКИ ДОЛЖНЫ БЫТЬ ВСЕ + +### Задача Alex + +> «пересобрал дифф?!» +> «дифф должен показывать то что поменяется. если там только перенос данных — это не изменение. +> изменение это было одно значение `new_` в 38-м csv, а стало другое, либо добавлено. +> и эти патчи должны быть в диффе **ВСЕ**.» +> «**добавление колонки — это правка или нет блядь?? почему выкинуть блядь??**» +> «одинаковое значение по папке должно быть на уровне `APPLY-PLAN` организовано — +> а дифф должен лишь **объединить если значение одинаковое в `new_` для всей папки**» + +### 🔴 ЧЕТЫРЕ ПРАВИЛА ДИФФА (зафиксированы Alex, обязательны) + +| # | Правило | Следствие | +|---|---|---| +| 1 | Изменение = `new_` в 38-м ≠ `new_` сейчас | Основа диффа | +| 2 | **Появление НОВОЙ КОЛОНКИ = правка.** `albumartist`/`genre` в 38-м плане **не существовало** → тег вообще не писался → сейчас пишется | `old_38 = '<КОЛОНКИ НЕ БЫЛО>'`, **НЕ выкидывать** | +| 3 | Схлопывание в запись ПАПКА — **ТОЛЬКО если у ВСЕХ файлов папки совпадает И `old`, И `new`** | «доминирующее большинство» **ЗАПРЕЩЕНО** | +| 4 | Каждая правка обязана лежать в диффе либо строкой ТРЕК, либо быть покрыта строкой ПАПКА | Обязателен `assert` на полноту | + +> 🔴 **Моя ошибка, из-за которой правило 2 появилось:** я предложил «выкинуть `albumartist` +> и `genre` (2022 строки), колонок в 38-м не было». Alex: «**ты идиот? добавление колонки — +> это правка или нет блядь??**». Логика: колонки не было ⇒ план её не заполнял ⇒ тег не +> записывался ⇒ появление колонки = запись тега = правка. **Не путать с «переносом данных».** + +### 🔴 Три дефекта, найденные в предыдущей версии `diff_new_vs_38.py` + +| # | Дефект | Как проявлялся | Фикс | +|---|---|---|---| +| 1 | Схлопывание по «**большинству**» (`len(dom) > len(others)`) | **92 места**, где папка показана как вся-одна, а часть файлов отличалась. Пример: `Sbornik_Novij_Rok/genre` — 193 `Rock` схлопнуто, 7 файлов другого жанра → папка выглядела как «вся Rock» | условие `len(set(new)) == 1` | +| 2 | `NEVER_COLLAPSE = {title, track}` | Правило разное для разных полей — Alex требует **одно** правило | убрано | +| 3 | Схлопывание при одинаковом `new`, но **разном `old`** | **43 потери** в `t 2009-07 c6Or_NICK` и `z Старое/disk*/2 ALT,PUNK`: запись папки брала `old` первого файла (`''`) → врала про старое значение (`Дискотека Арлекина 2009`) | сравнивать **пару `(old, new)`** | + +**Мораль дефекта 3:** «значение одинаково у всех» недостаточно — запись папки утверждает +**переход** `old → new`. Если `old` у файлов разный, переход разный ⇒ папка не схлопывается. + +### 🔴 ПИТФОЛЛ: `python3 script.py | head -40` = SIGPIPE = CSV НЕ записан + +Запуск с пайпом в `head` убивает Python по `SIGPIPE` **до** `w.open(OUT)`. Скрипт отработал, +отчёт напечатался — **а `DIFF-new-vs-38.csv` остался старым**, MD5 не изменился. +Я почти отчитался «готово». **Всегда `python3 x.py > /tmp/log 2>&1`**, затем читать лог. + +### 🔴 ВЕРИФИКАТОР: независимый скрипт, не переиспользующий логику диффа + +`/Users/admin/Downloads/music-tags/verify_diff_completeness.py` — пересчитывает эталон +из двух CSV **с нуля** (своя реализация) и проверяет: + +``` +эталон (всех реальных изменений): 18317 + покрыто строками ПАПКА: 14806 + отдельными строками ТРЕК: 3511 +ПОТЕРЯНО: 0 ЛИШНИХ: 0 +НЕТ-В-38: 41 = 41 OK +ТОЛЬКО-В-38: 85 = 85 OK +ИТОГ: ПОТЕРЬ НЕТ, дифф ПОЛНЫЙ +``` + +`exit-code 0` = чисто. **Два ложных срабатывания, обнаруженных в нём же:** +- считал «ЛИШНИМИ» 101 строку файлов из категории `НЕТ-В-38` (у них `old_38 = '<НЕТ В 38-м>'`) — + поправлено: такие пары законны, если путь в множестве `e_none`; +- `e_none` вычислялся **ниже** места использования → `NameError`/неработающий фильтр. + Вычисление перенесено наверх. + +> **Урок:** «своя проверка своей же функцией» ничего не доказывает (ср. W11). +> Верификатор пишется **независимо** и печатает цифры, а не «успех». + +### 📊 ИТОГ СЕССИИ 4 — АКТУАЛЬНЫЕ АРТЕФАКТЫ + +``` +APPLY-PLAN.csv 7861 строка md5 bb724716c7ffe45619b76abbb3deb5cc +DIFF-new-vs-38.csv 4864 строки md5 0d7a31ca529b2ce2e7f018816ea0ff8e +tag-fix-plan.csv не тронут (база 38-го, источник диффа) +``` + +**Состав диффа:** +``` +ПАПКА 1126 (было 1140 до фикса дефекта 3) +ТРЕК 3612 (было 3419) +НЕТ-В-38 41 +ТОЛЬКО-В-38 85 +ВСЕГО правок 18558 ← контроль: 14806 покрыто папками + 3511 треков ≈ эталон +``` + +**Строк по полям (правильный подсчёт — через `csv`, НЕ `awk -F,`: папки содержат запятые):** +``` +genre 2509 +track 987 +albumartist 701 +album 305 +artist 194 +title 42 +``` + +**ПАПКА-строки по полям:** `albumartist` 571 · `genre` 470 · `album` 72 · `track` 9 · `artist` 4. + +**Цепочка MD5 диффа:** +``` +5d134966… сессия 3 (3595 строк, схлопывание по большинству) +eab00b4b… + правило колонок (4685 строк) — НЕ записался из-за SIGPIPE-пайпа +0d7a31ca… ⬅ ТЕКУЩИЙ, + фикс схлопывания по паре (old,new) (4864 строки) +``` + +### 🔴 ПИТФОЛЛ: `awk -F,` для подсчёта полей CSV врёт + +`folder` в диффе содержит запятые (`!Pop, Club, Любэ/...`) → `awk -F, '{print $4}'` печатал +**имена папок вместо имён полей**. Считать только парсером: `csv.DictReader` + `Counter(r['field'])`. + +### ⚙️ Инвариант в самом скрипте + +```python +covered = sum(n for _, _, _, _, n in collapse_lines) +assert covered + len(file_lines) == sum(field_counts.values()), "ПОТЕРЯ ПРАВОК!" +print(" OK: потерь нет") +``` + +Скрипт **падает** при нарушении — молчаливая потеря правок исключена. Запуск: + +```bash +cd /Users/admin/Downloads/music-tags +python3 diff_new_vs_38.py > /tmp/diff_run.log 2>&1 # НЕ через | head! +python3 verify_diff_completeness.py # exit 0 = чисто +``` + +Бэкапы: `diff_new_vs_38.py.bak-before-collapse-fix-`, +`DIFF-new-vs-38.csv.bak-before-collapse-fix-`. + +--- + +### ⏳ НЕ ЗАКРЫТО (обновлено конец сессии 4) + +| # | Вопрос | Статус | +|---|---|---| +| 1 | `track = '238'` у 56 файлов (`Красная Плесень/Золушка`) | ⏳ верное значение неизвестно | +| 2 | `track` пуст у 1438, из них **9 с непустым `cur_track`** → тег `TRCK` будет УДАЛЁН | ⏳ решение Alex | +| 3 | `changed = 'no'` у **213 строк** в `APPLY-PLAN.csv`; из них **50 файлов (61 строка)** присутствуют в диффе как «изменения» | ⏳ уточнить трактовку | +| 4 | Боевой прогон `--apply --verify` | 🔴 не запущен | +| 5 | Контейнер `snapcur` на NAS (только чтение, `:ro`) | ⏳ не убран — Alex не подтвердил удаление | +| 6 | Глазами пройти `field=album` (305 строк) в диффе | ⏳ следующий шаг по указанию Alex | + +### Про `changed = 'no'` — разбор (сессия 4) + +Проверено фактом: **0 случаев**, где `changed='no'`, но `new_* != cur_*` — флаг не врёт. +213 строк = файлы, у которых правка не требуется (`new_ == cur_`). Файлы этой группы +попадают в дифф, когда `new_` отличается от **38-го плана** — то есть правка по факту +не нужна, а в диффе строка есть. Пользователь обозначил принцип: **в диффе только то, +что реально поменяется.** + + diff --git a/personal/tech/id3-tag-repair.md b/personal/tech/id3-tag-repair.md index fe4c66ac..92ea7c0a 100755 --- a/personal/tech/id3-tag-repair.md +++ b/personal/tech/id3-tag-repair.md @@ -22,6 +22,9 @@ related: > Зеркало скилла `id3-tag-repair` (категория `media`, `~/.hermes/hermes-whale/skills/media/id3-tag-repair/`). > Скилл — для исполнения, этот док — для контекста проекта и истории. +> +> ⚠️ **Скилл обновлён до v1.3.0** — добавлены правила **D1–D6** (слой диффа планов, +> раздел «🔵 Слой ДИФФА планов» ниже). Полные формулировки — в `SKILL.md` скилла. ## 📁 Артефакты проекта @@ -141,6 +144,94 @@ os.replace(TMP, SRC) с combining `U+0306`. Перед сравнением/группировкой всегда `unicodedata.normalize("NFC", name)`. +## 🔵 Слой ДИФФА планов — правила и питфоллы (добавлено 2026-09-27, сессия 4) + +Задача класса: «показать, что изменится, относительно предыдущей версии плана». Здесь +ломается не запись, а **отчёт** — и это опаснее, потому что отчёт читает человек и по нему +принимает решение. + +### 🔴 D1 — схлопывание «по большинству» искажает папку целиком + +```python +# ❌ 92 испорченные папки +dominant, dom = max(by_new.items(), key=lambda kv: len(kv[1])) +if len(dom) > len(others) and len(dom) > 1: # «большинство победило» + collapse(folder, field, dominant) +# ✅ схлопывать только если значение одинаково у ВСЕХ файлов папки +if len(set(nv for _, _, nv in lst)) == 1 and len(lst) > 1: + collapse(folder, field, lst[0][1], lst[0][2]) +``` + +`Sbornik_Novij_Rok` / `genre`: 193 `Rock` + 7 других → папка показывалась как «вся Rock». +Читатель видит одно значение и не знает, что 7 файлов отличаются. + +### 🔴 D2 — сравнения только `new` недостаточно: запись папки утверждает ПЕРЕХОД + +Папка `t 2009-07 c6Or_NICK`: у файлов `old` = `''`, `'Дискотека Арлекина 2009'`, +`'Record Super Chart (13.06.09)'` — а `new` одинаковый. Схлопывание по `new` берёт `old` +первого файла → запись врёт про старое значение, а **43 файла теряют свою строку**. +Сравнивать **пару**: + +```python +if len(set((ov, nv) for _, ov, nv in lst)) == 1 and len(lst) > 1: # ✅ +``` + +### 🔴 D3 — новая КОЛОНКА в плане = правка, а не «перенос данных» + +Если в предыдущем плане колонки `albumartist`/`genre` **не существовало** — тег не писался +вообще. Появление колонки означает, что тег **впервые будет записан**. Это изменение. + +``` +albumartist 633 строк → 0 = НЕВЕРНО (выкинуты на основании «колонки не было») +albumartist 7861 строк = ВЕРНО (old_38 = '<КОЛОНКИ НЕ БЫЛО>') +``` + +Реакция Alex на выкидывание: «**ты идиот? добавление колонки — это правка или нет?**» +Не путать с настоящим «переносом данных» (значение v1→v2 при неизменном `new_`). + +### 🔴 D4 — `python3 script.py | head` убивает скрипт ДО записи файла + +`SIGPIPE` приходит, когда читатель (`head`) закрывает пайп. `print`-и доходят, `open(OUT, "w")` +в конце — **никогда**. Симптом: скрипт «отработал успешно», отчёт напечатан, +**MD5 выходного файла не изменился**. Проверять после каждого запуска: + +```bash +python3 gen.py > /tmp/run.log 2>&1 # ✅ +tail -5 /tmp/run.log && md5 out.csv # сверять, что файл реально перезаписан +``` + +### 🔴 D5 — контроль полноты обязателен: `assert` в генераторе + +Генератор обязан доказать, что не потерял правок: + +```python +covered = sum(n for _, _, _, _, n in collapse_lines) # покрыто записями ПАПКА +assert covered + len(file_lines) == sum(field_counts.values()), "ПОТЕРЯ ПРАВОК!" +``` + +Плюс **независимый** верификатор (`verify_diff_completeness.py`): он строит эталон +из двух исходных CSV своей реализацией и проверяет, что каждая пара `(path, field, old, new)` +присутствует в диффе **либо** строкой ТРЕК, **либо** покрыта строкой ПАПКА. Печатает цифры +(`ПОТЕРЯНО: 0`), а не «успех»; exit-code 0 = чисто. + +> **Не переиспользовать код генератора в верификаторе** — иначе общая ошибка пройдёт обе +> проверки (ср. W11: копия функции продолжала проходить после фикса). + +### 🔴 D6 — подсчёт полей CSV через `awk -F,` врёт + +`folder` = `!Pop, Club, Любэ/...` содержит запятую → `awk -F, '{print $4}'` печатает фрагмент +имени папки вместо имени поля. Считать только парсером: `csv.DictReader` + `Counter(r['field'])`. + +### Правило именования записей диффа + +``` +ПАПКА + field + old + new — значение одинаково у ВСЕХ файлов папки +ТРЕК + path + field + old + new — хоть у одного файла отличается +``` + +Никаких третьих форм. «Одинаковое значение по папке» должно быть организовано **в плане**, +дифф лишь объединяет — так формулирует Alex. + ## 🖊 Слой ЗАПИСИ — ID3v1 vs ID3v2 (добавлено 2026-09-27) Зеркало правил W1–W15 скилла. Полные формулировки — в `id3-tag-repair/SKILL.md`.