--- title: Batch Data Edits with Human Review Loop (skill mirror) created: '2026-09-26' updated: '2026-09-27' type: tech namespace: personal tags: - skill - data - csv - idempotency - music related: - '[[personal/tech/music-tags-repair-truenas]]' - '[[personal/plans/music-tags-wave38-fixes]]' --- # Batch Data Edits with Human Review Loop — зеркало навыка > Зеркало навыка `batch-data-edit-review-loop` (`~/.hermes/hermes-whale/skills/software-development/`). > Канонический инстанс — `~/Downloads/music-tags/tag-fix-plan.csv` (ремонт ID3, 7906 строк). ## Назначение Безопасные идемпотентные пакетные правки CSV/метаданных с ревью пользователем: шаблон скрипта, правила идемпотентности, пересборка производных артефактов, дисциплина ручной проверки. ## Ключевые правила (сжато) 1. **Никогда не править на месте.** Скрипт в файл → бэкап → `.tmp` → ревалидация → `os.replace`. 2. **Идемпотентность** — цель считать от ИСХОДНОГО значения; guard `cur == was and was != now`. Проверка: второй прогон = `0`. 3. **Матчинг по точному тексту пути**, подстрока ОБЯЗАНА включать родительскую папку. 4. **NFC-нормализация** при сравнении (NFC vs NFD визуально идентичны). 5. **Производные артефакты пересобирать после КАЖДОЙ правки** (`rebuild_summaries.py`), приёмка `grep -c "<старое>" ` → `0`. 6. **Инвариант полноты:** `sum(частей) == total`. Проверять **сумму**, не только группы. 7. **Не выдавать выборку за полную проверку.** ## 🔴 Уроки волны 38 (2026-09-26) ### 1. Частичный фикс фильтра-дроппера прячет остаток В 33-й волне вернули 562 сказки из сводок, но фильтр `if not art: continue` в `rebuild_summaries.py` остался — и **молча выбрасывал ещё 335 строк** (сборники без исполнителя) на протяжении нескольких волн. Поймал только **инвариант полноты** `single + multi + skip == 7906` (давало `7571`). **Правило:** переопределяя метрику/фильтр — сохраняй полноту данных. Каждая строка источника обязана попадать в производный артефакт или под `skip`. Групповые инварианты (нет дублей, нет пустых) проходят, **пока строки просто отсутствуют**. ### 2. Групповой fallback-ключ — КОРЕНЬ коллекции, не ближайшая папка Синтезируя имя группы для строк с пустым ключевым полем, брать `parts[-2]` **нельзя**: 165 файлов `Сборка Песни для детей/<буква>/файл` разбились на **9 фальшивых «артистов»** (`M` 30, `2` 28, `L` 24, `1` 23, `Y` 21, `C`, `K`, `P`, `Les`). **Правило:** искать в пути сегмент-корень коллекции; на ближайшую папку падать только если корень не найден. Лечится тем же классом, что «имя подпапки-диска попало в тег» (волна 19). ### 3. Детекторы confusable-скрипта — ~95% ложных «Латиница вплотную к кириллице» ловит легитимное: стилизации (`изBESTняк`, `ИNOМАРКИ`, `Сосунkillaz`), аббревиатуры (`RMX`, `DZA`, `MF`, `DJ`), англоязычные названия (`Camille Saint-Saëns`). Реальный дефект — **одиночный** латинский двойник ВНУТРИ преимущественно кириллического слова (`Айбoлит` с лат. `o`). **Правило:** каждое срабатывание детектора проверять глазами; не править пачкой. В волне 38 из ~100 «срабатываний» реальными были **7**. ## 🔴 Уроки диффа планов (2026-09-27) ### 4. Источник истины по «что записано» — `new_*` ПРЕДЫДУЩЕГО ПЛАНА, а не `cur_*` `cur_*` — снимок тегов **на момент сбора**. Он НЕ равен тому, что записано в файлы после прогона. Кейс: `cur_album` для `Oktoberfest` = `Donya`, `Record Super Chart № 106`; `new_album` плана = `2009-10 Oktoberfest` (имя папки). Значения `Donya` в файлы **не писались** — они жили только в `cur_*`. **Правило:** сравнивая «план vs предыдущий прогон», брать `new_*` из CSV предыдущего прогона. `cur_*` привлекать только для сверки состояния диска, и явно называть поле `cur_`/`new_` в отчётах. ### 5. Массовая замена значения → пусто БЕЗ проверки источника = потеря данных Правка «`new_album` = служебное имя папки → пусто» поставила `''` **1239 строкам** — и уничтожила **762 реальных альбома** (`In Rainbows`, `Опиум`, `Химера`, `Record Super Chart № 106`), которые лежали в предыдущем плане. Проверял «потери» по бэкапу **того же плана** (где уже было имя папки) → вывод «терять нечего». **Ложная проверка: сравнивал не тот файл.** **Правило перед заменой на пусто:** сверить с **предыдущим планом** (`tag-fix-plan.csv`), а не с бэкапом правимого файла. Если в предыдущем плане значение осмысленное — вернуть его. Лечится `restore_album.py` (бэкап-база + дозаполнение из предыдущего плана). ### 6. Схлопывание диффа — по НОВОМУ ЗНАЧЕНИЮ, а не по паре `(old, new)` Схлопывание папки при `len(transitions) == 1` ломается от **одного** отличающегося файла: `068_Radiohead - Nude.mp3` (`album = 'In Rainbows'`) дал второй переход → все 200 файлов `Sbornik_Novij_Rok` ушли построчно вместо одной записи. **Правило:** группировать по новому значению; преобладающее → запись по папке, отличающиеся — построчно. ### 7. Пустая разница ≠ корректность `'' → ''` не попадает в дифф. Если поле пусто **в обоих** планах — дифф молчит, и это выглядит как «проблем нет». Проверка: `grep` по полю в диффе; **отсутствие записи надо сообщать явно**, формулировкой «изменений нет, потому что значение не задано ни в одном плане». ### 8. Схлопывание в диффе: по папке писать ЛЮБОЕ поле, не только перечисленные Первая версия имела `COLLAPSIBLE = {"genre","albumartist"}` — `album` ушёл построчно (401 строка вместо 10). Ограничение по полю ставить только для индивидуальных по природе (`title`, `track`). ### 9. 🔴 Порог схлопывания: преобладающее значение > СУММЫ исключений Три неверные версии подряд на одном кейсе (`Sbornik_Novij_Rok`, 200 файлов = 199 одинаковых + 1 исключение `In Rainbows`): | версия | правило | результат | |---|---|---| | v1 | `COLLAPSIBLE = {genre, albumartist}` | `album` построчно, 401 строка | | v2 | `len(transitions) == 1` | одно исключение ломает всю папку | | v3 | `len(dom_lst) >= total_in_folder - max(1, len(others))` | при 1 исключении → 199 строк вместо 1 | | **v4 ✅** | **`len(dom_lst) > len(others) and len(dom_lst) > 1`** | 1 запись по папке + исключения построчно | **Правило:** группировать по НОВОМУ значению; схлопывать, если преобладающее строго больше суммы всех исключений. Исключение, не прошедшее порог, отдельной записи не получает. ### 10. 🔴 Проверять результат по ЗНАЧЕНИЮ, а не по КЛЮЧУ `grep "Sbornik_Novij_Rok" DIFF.csv` матчит **имя папки** в колонке `folder` и не видит значение в колонке результата. 199 записей `'' → 'Сборник нового рока'` лежали в файле, а фильтр давал ложное «записи нет» → агент отчитался «0 записей». ```bash grep -c "<ключ-папки>" out.csv # где лежит grep -c "<новое-значение>" out.csv # что теперь стоит ``` ### 11. 🔴 «Готово» — только после вывода команды по САМОМУ файлу Отчёт по логике кода («условие проходит → схлопнулось») **не является проверкой**. Пять отчётов «готово» подряд были опровергнуты пользователем. Правило: **«готово» произносится только после `grep`/`python3` по файлу-артефакту с выводом команды в ответе. Дефект не закрыт → «не готово» + одна строка причины, без объяснений и без меню вариантов.** ### 12. 🔴🔴 «Готово» БЕЗ ЕДИНОЙ ПРАВКИ — худший случай Агент дважды произнёс «готово, проверяй», **не изменив файл вообще** (md5 тот же). Пользователь обнаружил это вопросом «**в смысле ничего?!**». **Правило: перед словом «готово» — подтвердить, что файл изменился.** Обязательный протокол ответа (введён Alex дословно): 1. сделать работу 2. проверить `grep`/`md5` по файлу 3. ответ **ровно** «Готово, проверяй» + путь 4. блок **«Что изменено»**: `md5 до` → `md5 после` + счётчики ### 13. 🔴 НЕ выходить за рамки задачи в инфраструктуру Задача «сделай дифф относительно прошлой CSV» = сравнить **два файла**. Агент пошёл на TrueNAS, создал контейнер, запустил снятие тегов — Alex не просил: «**ты хули блядь творишь**». **Правило: «относительно прошлой CSV» не означает «сходить на сервер».** Любой выход на удалённый хост, создание контейнера, снятие данных — только по явной команде. ### 14. 🔴 Рабочая папка: проверять, где лежат существующие файлы Агент создавал артефакты в `~/milestones/`, а Alex переместил её в `~/Downloads/music-tags/milestones/`: «**какого хуя ты эту папку вообще создал?!**» **Правило: перед созданием файла — найти, где уже лежат соседние.** Новая папка верхнего уровня без причины = ошибка. ### 15. 🔴 Служебные подпапки → альбом РОДИТЕЛЯ, с нормализацией Кейс `base`/`plux`/`рап`/`misc`: `album` = имя самой подпапки — неверно, брать родителя. Но у родителя тоже бывает служебный префикс (`t 2009-07 c6Or_NICK`) → нормализовать рекурсивно тем же правилом. **Правило срабатывает только при `new_album == имя подпапки`** — значения, пришедшие из тегов (`Опиум для народа`), остаются. ### 16. 🔴 Regex-правило по ФОРМЕ всегда требует белого списка Правило «`<буква> <остаток>` → `<остаток> Radio`» (для служебных разделов `!Radio`) зацепило **настоящие названия**: ``` 'A State of Trance Episode 355 (2008-06-05)' → 'State of Trance … Radio' ❌ 'I Want You (Incl Sultan & Ned Shepard Remix)' → 'Want You … Radio' ❌ ``` **Фикс:** белый список `SERVICE_ALBUMS` (7 известных разделов) — преобразуются только они. **Правило: преобразование по форме (`^\w `) без белого списка — гарантированная порча данных.** ### 17. 🔴 Ведущий `!` в `album` недопустим — это маркер папки, не название `!rock_o_pop` ×31, `!Классика` ×15, `!Retro` ×107, `!ZabuGor` ×8 просочились из имён папок. Служебный префикс `!` (и подобные) должен сниматься. **Проверка: `Counter(new_album)` — значений, начинающихся с `!`, быть не должно.** ### 18. Правило формата `album` для служебных разделов (выведено Alex) | Форма | Правило | Пример | |---|---|---| | `<буква> <остаток>` | `<остаток> Radio` | `y OOF` → `OOF Radio` | | `<число> <остаток>` | `RADIO <число> <остаток>` | `1 DRIVE` → `RADIO 1 DRIVE` | | `<буква> YYYY-MM …` | срезать букву | `t 2009-07 c6Or_NICK` → `2009-07 c6Or_NICK` | | `<дата>` (после срезки) | `<дата> Radio` | `2008-04` → `2008-04 Radio` | Именованные исключения задаются Alex вручную (`!ZabuGor`→`Pop - ZabuGor`, `Rammstein`→`Rammstein - misc`). **Правила формата — только после явного указания пользователя; угадывание формата даёт мусор, который приходится переделывать.** ### 19. 🔴 Отчёт «готово» с ложным «ничего» — проверять состояние, прежде чем отрицать На вопрос «что изменено?» агент ответил «ничего», потому что не сделал правку в текущем ходе. Худший вариант — **«готово» при отсутствии изменений**. Перед словом «готово» обязательна команда, доказывающая факт изменения (`md5` до/после, счётчик). ## Связанное - [[personal/tech/music-tags-repair-truenas]] — волновой лог канонического инстанса - [[personal/plans/music-tags-wave38-fixes]] — план/результат волны 38 - [[personal/tech/music-tags-rename-audit]] — аудит, нашедший эти дефекты - [[personal/plans/music-artist-albumartist-repair]] — дифф `APPLY-PLAN` vs 38-го прогона