32 KiB
Executable File
title, created, updated, type, namespace, tags, related
| title | created | updated | type | namespace | tags | related | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Batch Data Edits with Human Review Loop (skill mirror) | 2026-09-26 | 2026-09-27 | tech | personal |
|
|
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/метаданных с ревью пользователем: шаблон скрипта, правила идемпотентности, пересборка производных артефактов, дисциплина ручной проверки.
Ключевые правила (сжато)
- Никогда не править на месте. Скрипт в файл → бэкап →
.tmp→ ревалидация →os.replace. - Идемпотентность — цель считать от ИСХОДНОГО значения; guard
cur == was and was != now. Проверка: второй прогон =0. - Матчинг по точному тексту пути, подстрока ОБЯЗАНА включать родительскую папку.
- NFC-нормализация при сравнении (NFC vs NFD визуально идентичны).
- Производные артефакты пересобирать после КАЖДОЙ правки (
rebuild_summaries.py), приёмкаgrep -c "<старое>" <derived>→0. - Инвариант полноты:
sum(частей) == total. Проверять сумму, не только группы. - Не выдавать выборку за полную проверку.
🔴 Уроки волны 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.
🔴 Уроки Various Artists поверх одного артиста (сессия 8, 2026-09-27)
5. Механический критерий «разные artist = сборник» даёт ложные Various Artists
Папка-АЛЬБОМ помечается Various Artists, хотя исполнитель один. Два ложных сигнала:
- Разнобой написания одного исполнителя —
Иванушки/Иванушки Int./Иванушки International→ три «разных» артиста, на деле один. - Несколько чужих файлов в корне — 7 треков сторонних групп среди 52 треков
Агата Кристи→ «сборник».
Правило Alex: «Папка с 1 уникальным артистом → АЛЬБОМ, albumartist = artist».
Детектор: personal/tech/scripts/find_va_misuse.py (>50% файлов на одном артисте).
6. Класс вычищается ИТЕРАТИВНО, а не одним прогоном
После правки 4 папок детектор нашёл ещё 5. Гонять детектор после каждой пачки правок, пока он не вернёт пусто или только подтверждённые пользователем спорные случаи.
7. Подпапка может требовать ДРУГОГО значения, чем корень
В корне Агата Кристи чужие исполнители сохраняют свои имена; в 9999 - Разное/Проекты
(12 файлов, все чужие) — все получают Агата Кристи. Не экстраполировать правило
с корня на подпапку — спрашивать.
8. «Почему тут X?» — сигнал дефекта, не запрос на объяснение
Четыре вопроса подряд «почему тут Various?» (Иванушки, Магомаев, Агата Кристи,
Dj Tiesto). Реакция: показать факт (aa, список artist с частотой) и спросить целевое
значение. Не выводить его из «правила» и не решать за пользователя.
🔴 Уроки диффа планов (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 записей».
grep -c "<ключ-папки>" out.csv # где лежит
grep -c "<новое-значение>" out.csv # что теперь стоит
11. 🔴 «Готово» — только после вывода команды по САМОМУ файлу
Отчёт по логике кода («условие проходит → схлопнулось») не является проверкой. Пять
отчётов «готово» подряд были опровергнуты пользователем. Правило: «готово» произносится
только после grep/python3 по файлу-артефакту с выводом команды в ответе. Дефект не закрыт
→ «не готово» + одна строка причины, без объяснений и без меню вариантов.
12. 🔴🔴 «Готово» БЕЗ ЕДИНОЙ ПРАВКИ — худший случай
Агент дважды произнёс «готово, проверяй», не изменив файл вообще (md5 тот же). Пользователь обнаружил это вопросом «в смысле ничего?!».
Правило: перед словом «готово» — подтвердить, что файл изменился. Обязательный протокол ответа (введён Alex дословно):
- сделать работу
- проверить
grep/md5по файлу - ответ ровно «Готово, проверяй» + путь
- блок «Что изменено»:
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)
🔴 УСТАРЕЛО (сессия 7). Alex переопределил формат:
Radio - <имя>—y OOF→Radio - OOF,z Старое→Radio - Старое,Df1→Radio - Df1. Порядок «префикс», не «суффикс». СтарыеOOF Radio/RADIO 1 DRIVEиз таблицы ниже — исторические, переделывались. См. personal/plans/music-artist-albumartist-repair § СЕССИЯ 7.
| Форма | Правило | Пример |
|---|---|---|
<буква> <остаток> |
<остаток> RadioRadio - <остаток> |
y OOF → Radio - OOF |
<число> <остаток> |
RADIO <число> <остаток> |
1 DRIVE → RADIO 1 DRIVE |
<буква> YYYY-MM … |
срезать букву | t 2009-07 c6Or_NICK → 2009-07 c6Or_NICK |
<дата> (после срезки) |
<дата> Radio |
2008-04 → 2008-04 Radio |
Именованные исключения задаются Alex вручную. Правила формата — только после явного указания пользователя; угадывание формата даёт мусор, который приходится переделывать. 🔴 Формат может быть переопределён повторно — не считать его застывшим.
19. 🔴 Правило схлопывания ОДНО для всех полей — включая title/track
Список NEVER_COLLAPSE = {title, track} («эти поля всегда индивидуальны, не схлопывать»)
создавал второй, несогласованный набор правил. Alex: «одинаковое значение по папке
должно быть на уровне APPLY-PLAN организовано — а дифф должен лишь объединить если значение
одинаковое в new_ для всей папки».
Правило: схлопывание — механическое объединение по факту одинакового значения, без исключений по типу поля. Особые правила для отдельных полей запрещены.
20. 🔴 python3 script.py | head = SIGPIPE → выходной файл НЕ ПЕРЕЗАПИСЫВАЕТСЯ
Запуск генератора через пайп в head/tail убивает Python сигналом SIGPIPE
до записи артефакта. Вывод в консоль выглядит корректным, md5 и число строк
не меняются — агент отчитался «пересобрал», хотя файл остался старым.
Правильно: перенаправлять в файл, потом читать его.
python3 gen.py > /tmp/run.log 2>&1; echo "exit=$?"; grep -E "ВСЕГО|OK:" /tmp/run.log
Признак дефекта в отчёте: после «пересборки» md5 артефакта совпадает с предыдущим.
21. 🔴 Верификатор обязан быть НЕЗАВИСИМЫМ и учитывать все категории
Контроль полноты — отдельный скрипт, который пересчитывает эталон из исходных данных,
а не переиспользует функции генератора (иначе он повторит ту же ошибку и промолчит).
Обязательно покрыть все категории выходного файла: если есть блоки НЕТ-В-38 /
ТОЛЬКО-В-38, строки из них верификатор считает законными, а не «лишними»
(иначе ложные 101 «лишняя строка»).
# инвариант: полное покрытие
assert sum(len(v) for v in groups.values()) + len(missing) == total_expected
Запускать верификатор после каждой правки исходника, а не один раз в конце.
22. 🔴 Схлопывание по НОВОМУ значению без учёта СТАРОГО теряет записи
Схлопывание в запись папки по set(new) ломается, когда у файлов одинаковый new,
но разный old ('', Дискотека Арлекина 2009, Boom Boom Pow Remix → все
2009-07 c6Or_NICK). Запись папки берёт old от первого файла и врёт про
остальные; 43 пары (old, new) оказываются не представлены в диффе.
Фикс: схлопывать только при совпадении всей пары:
combos = set((ov, nv) for _, ov, nv in lst)
if len(combos) == 1 and len(lst) > 1: # ПАПКА
else: # все файлы построчно
Поймал только независимый верификатор — в генераторе всё выглядело верно.
23. 🔴 Счётчик «N затираний» — ШУМ, а не список дефектов
cur_albumartist непуст и new_albumartist != cur_albumartist → 765 строк.
Почти все — законная нормализация: CP1251-мойибаке (Èëüÿ ×åðò→Илья Чёрт),
транслит (Vladimir Vysockij→Владимир Высоцкий), регистр (ДИСКОТЕКА АВАРИЯ→Дискотека Авария).
Реальные дефекты — только там, где значение взято от чужого файла ТОЙ ЖЕ папки. Не рапортовать число 765 как «проблему» — сначала отфильтровать детектором.
24. 🔴 Класс «albumartist размножен от чужого файла в корне папки»
Правило «у корневых файлов albumartist = имя артиста папки» брало значение от первого
попавшегося файла. Один затесавшийся чужой трек (из сборника) заражал всю папку:
The Prodigy → DJ Feel (из трека DJ Feel на Trancemission Vol.9) 6 файлов испорчено
Банд Эрос → VIP77 (из Russian R'n'B vol.1) 6 файлов
Жуки → Виктор Жуков 11 файлов
Дискотека Авария → ДА и Меньшиков · Потап & Каменских → Мика Ньютон
Король и Шут → КняZz · Ленинград → Бумер
Признак порчи — видно в cur_albumartist: у Prodigy/Hotride [EP].mp3 было
cur_albumartist='The Prodigy', а план ставил DJ Feel — прямое затирание верного значения.
Детектор: personal/tech/scripts/find_aa_hijack.py — albumartist папки один на всех,
не совпадает с топ-артистом и не совпадает с именем папки, но совпадает с миноритарным
артистом → подозреваемый.
Правило: albumartist трека = его собственный артист, если альбом не сборник.
Значение albumartist НИКОГДА не размножается на файлы других исполнителей.
У миноритарных артистов в корне — их собственное имя (КняZz ×1, Король и Шут ×3).
25. 🔴 Имя подпапки идёт в album, НЕ в albumartist
Попытка закодировать имя подпапки как <Родитель> - <Подпапка> в album (Rammstein - misc) —
ошибка. Alex: «оно должно попадать в album блядь! не в albumartist!»
albumartist = 'Rammstein' (имя исполнителя)
album = 'misc' (имя подпапки — как есть)
Детектор: искать new_album in (f'{grand} - {last}', f'{last} - {grand}') — склейка
родитель+подпапка недопустима ни в album, ни в albumartist.
26. 🔴🔴 НЕ ОТВЕЧАТЬ ЗА ПОЛЬЗОВАТЕЛЯ
Пользователю задан вопрос → ответа нет → агент пишет «правлю по выработанному правилу»
и применяет правку. Затем Alex: «что за ответ по ленинград?»
Второй раз: агент сам придумал значение (Rammstein - misc) и предъявил его как
«твоё решение», хотя пользователь его не называл.
Правило: если вопрос задан и ответа нет — НЕ решать за него. Ждать. Формулировки «правлю по выработанному правилу», «по аналогии», «предположу» при отсутствии ответа — запрещены. Придуманное значение, предъявленное как чужое решение, — хуже отсутствия ответа: пользователь теряет возможность заметить подмену.
27. 🔴 Порядок отчётности: дождаться → править → проверить → отчитаться
Агент отчитался «Нет, 7 папок ждут правки» после того как начал править. Состояние в отчёте обязано соответствовать моменту произнесения.
28. 🔴 Отчёт «готово» с ложным «ничего» — проверять состояние, прежде чем отрицать
На вопрос «что изменено?» агент ответил «ничего», потому что не сделал правку в текущем ходе.
Худший вариант — «готово» при отсутствии изменений. Перед словом «готово» обязательна
команда, доказывающая факт изменения (md5 до/после, счётчик).
29. 🔴🔴 ИСКАТЬ ГОТОВЫЙ СКРИПТ ПРЕЖДЕ ЧЕМ ПИСАТЬ НОВЫЙ — grep -l обязателен
Скрипты копятся в /tmp/*.py и рабочей папке проекта, и нужный почти всегда уже есть —
вместе с выстраданными правилами (списки исключений, нормализация, схлопывание).
Кейс сессии 29-бис: написаны diff_new_vs_38_v5.py / _v6.py вместо существующих
/tmp/make_diff_rule.py + /tmp/rule_check_v2.py, которые делали ровно эту задачу и уже
содержали исключение сборников (Sbornik_* / Сборник*). Переписывание с нуля:
потеряно схлопывание (16417 строк вместо 1674) и повторно сломан Sbornik_Novij_Rok
(имя папки ушло в album на 200 файлов). Alex: «какого хуя ты по кругу мне предлагаешь
обратно то это менять?!».
Правило: перед написанием любого скрипта —
grep -l "<ключевое понятие>" /tmp/*.py ~/Downloads/<проект>/*.py
30. 🔴 ё и е — РАЗНЫЕ СИМВОЛЫ: grep с е НЕ НАХОДИТ значение с ё
grep -ci "черный кот" вернул 0, хотя в датасете лежали три файла Чёрный кот.mp3.
Из-за поиска сделан вывод «строки нет в плане вообще» — ответ пользователю был неверным
не по данным, а по поиску.
Протокол поиска кириллического значения: сначала с ё, затем с е, затем по фрагменту
без спорной гласной ("рный кот"), затем grep -i по всему полю.
Относится к любому поиску имён/текста — артисты, альбомы, заголовки, папки.
31. 🔴🔑 СНАЧАЛА УСТАНОВИТЬ, ЧТО ЕСТЬ КАНОН — диффать не тот набор колонок
Диф между неверной парой колонок даёт уверенный, правдоподобный и полностью неверный отчёт. Канонная рамка этого проекта (слова Alex, подтверждать при любом сомнении):
APPLY-PLAN.csv = RC. Канон. Источник истины. Прогон на 7496 файлов.
| Поле | Смысл |
|---|---|
new_* |
канон — что ДОЛЖНО быть в файлах |
cur_* |
снимок ДО прогона; после прогона УСТАРЕЛ |
Отсюда верная проверка — new_* плана vs живая БД, ожидание 0 различий.
Диффать cur_* против БД бессмысленно: различия гарантированы и ничего не доказывают.
В каждом отчёте называть пару полей, которые сравниваешь. Факт этой сессии:
RC-сверка (/tmp/rc_verify.py → RC-VERIFY.csv) дала 7861 расхождение из 7861 —
title/artist сошлись ✅, но genre разошёлся на ВСЕХ 7861 и track на 1699.
32. 🔴 Колонка track в БД Navidrome называется track_number
SELECT track FROM media_file → OperationalError: no such column: track.
Верные имена: title, artist, album_artist, album, genre, track_number.
Возвращается int → str(v).strip(), иначе AttributeError.
track_number = 0 = «номер не проставлен» — сравнивать с пустым значением из плана
с учётом этого, иначе 1699 ложных расхождений.
Связанное
- 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-PLANvs 38-го прогона