370 lines
28 KiB
Markdown
Executable File
370 lines
28 KiB
Markdown
Executable File
---
|
||
title: Batch Data Edits with Human Review Loop (skill mirror)
|
||
created: '2026-09-26'
|
||
updated: '2026-09-27'
|
||
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**.
|
||
|
||
## 🔴 Уроки `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 записей».
|
||
```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 == имя подпапки`** —
|
||
значения, пришедшие из тегов (`Опиум для народа`), остаются.
|
||
|
||
### 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` и число строк
|
||
**не меняются** — агент отчитался «пересобрал», хотя файл остался старым.
|
||
|
||
**Правильно:** перенаправлять в файл, потом читать его.
|
||
```bash
|
||
python3 gen.py > /tmp/run.log 2>&1; echo "exit=$?"; grep -E "ВСЕГО|OK:" /tmp/run.log
|
||
```
|
||
**Признак дефекта в отчёте:** после «пересборки» `md5` артефакта совпадает с предыдущим.
|
||
|
||
### 21. 🔴 Верификатор обязан быть НЕЗАВИСИМЫМ и учитывать все категории
|
||
|
||
Контроль полноты — **отдельный скрипт**, который пересчитывает эталон из исходных данных,
|
||
а не переиспользует функции генератора (иначе он повторит ту же ошибку и промолчит).
|
||
Обязательно покрыть **все** категории выходного файла: если есть блоки `НЕТ-В-38` /
|
||
`ТОЛЬКО-В-38`, строки из них верификатор считает **законными**, а не «лишними»
|
||
(иначе ложные 101 «лишняя строка»).
|
||
|
||
```python
|
||
# инвариант: полное покрытие
|
||
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)` оказываются не представлены в диффе.
|
||
|
||
**Фикс:** схлопывать только при совпадении **всей пары**:
|
||
```python
|
||
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` до/после, счётчик).
|
||
|
||
## Связанное
|
||
|
||
- [[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-го прогона
|