Files
obsidian-vault/personal/tech/batch-data-edit-review-loop.md
T

32 KiB
Executable File
Raw Blame History

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
skill
data
csv
idempotency
music
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 "<старое>" <derived> → 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.

🔴 Уроки Various Artists поверх одного артиста (сессия 8, 2026-09-27)

5. Механический критерий «разные artist = сборник» даёт ложные Various Artists

Папка-АЛЬБОМ помечается Various Artists, хотя исполнитель один. Два ложных сигнала:

  1. Разнобой написания одного исполнителя — Иванушки / Иванушки Int. / Иванушки International → три «разных» артиста, на деле один.
  2. Несколько чужих файлов в корне — 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 дословно):

  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)

🔴 УСТАРЕЛО (сессия 7). Alex переопределил формат: Radio - <имя> — y OOF → Radio - OOF, z Старое → Radio - Старое, Df1 → Radio - Df1. Порядок «префикс», не «суффикс». Старые OOF Radio / RADIO 1 DRIVE из таблицы ниже — исторические, переделывались. См. personal/plans/music-artist-albumartist-repair § СЕССИЯ 7.

Форма Правило Пример
<буква> <остаток> <остаток> Radio → Radio - <остаток> 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 ложных расхождений.

Связанное