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

118 lines
8.7 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`).
## Связанное
- [[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-го прогона