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

This commit is contained in:
Alexey Martemyanov committed 2026-09-27 13:58:58 +06:00
1 parent 751e63014c
commit 042cac0216
2 files changed
+258 -1

No files matched your search

+91
View File
@@ -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`.