118 lines
8.7 KiB
Markdown
Executable File
118 lines
8.7 KiB
Markdown
Executable File
---
|
||
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-го прогона
|