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

186 lines
14 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Batch Data Edits with Human Review Loop (skill mirror)
created: '2026-09-26'
updated: '2026-09-26'
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 "<старое>" <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**.
## 🔴 Уроки диффа планов (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 == имя подпапки`** —
значения, пришедшие из тегов (`Опиум для народа`), остаются.
## Связанное
- [[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-го прогона