diff --git a/personal/plans/balda-mm-phase4-agents.md b/personal/plans/balda-mm-phase4-agents.md new file mode 100644 index 00000000..b0b6bc23 --- /dev/null +++ b/personal/plans/balda-mm-phase4-agents.md @@ -0,0 +1,247 @@ +# Фаза 4 — исполнитель на Mac + `~/agents` + секция «Agents» в Eagle Dashboard + +> Создан 2026-09-26. Статус: **ПЛАН — ожидает апрува Alex, работа не начата.** +> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 4 + Фаза 5). +> Предыдущее: [[personal/projects/balda/balda-kraken-mm-deploy]] (Фаза 3 ✅ — бот отвечает Alex). + +--- + +## 0. Решения Alex (2026-09-26, этот заход) + +| # | Вопрос | Решение | +|---|---|---| +| 1 | Папка для проектов исполнителя | **`~/agents`** | +| 2 | Подтверждения | **Full access** → `permissions.mode: allow_all` | +| 3 | Управление hardlink/symlink в `~/agents` | **Новая секция в Eagle Dashboard** | + +--- + +## 1. 🔴 ПРОВЕРЕНО ЖИВЫМ ТЕСТОМ: симлинк в Docker НЕ работает + +Alex спросил: «Если в папку симлинк кинуть вовне — докер это прочтет?» +**Ответ: НЕТ.** Проверено на живом Docker (alpine:3.20, linux/arm64), не по памяти. + +``` +host: /tmp/symtest/outside/file.txt = "REAL CONTENT ON HOST" +bind: -v /tmp/symtest/bind:/mnt (внутри /mnt/target -> /tmp/symtest/outside) + +контейнер: cat /mnt/target/file.txt +→ cat: can't open '/mnt/target/file.txt': No such file or directory +``` + +Сам симлинк внутри виден (`lrwxrwxrwx link -> /outside`), но **цель резолвится +относительно корня контейнера**, где этого пути нет. Docker монтирует только реальный +путь — симлинки не разворачивает и не «протаскивает». + +### Следствие для архитектуры + +``` +Mac Контейнер-исполнитель +~/agents/ /agents (bind-mount, rw) + project-x/ ← РЕАЛЬНАЯ папка → видна + src -> ~/Developer/proj-x ← СИМЛИНК → ✗ БИТЫЙ +``` + +**Правило:** внутрь `~/agents` — только **реальные каталоги** или **hardlink**. +Симлинки допустимы **только если цель лежит внутри того же смонтированного корня**. + +--- + +## 2. Symlink / hardlink / bind-mount — что выбрать + +| Способ | В Docker | Может пересечь ФС | Может указать на файл вне mount | Итог | +|---|---|---|---|---| +| **symlink** | ✗ битый | да | ✗ нет | ❌ не использовать для внешних целей | +| **hardlink** | ✓ работает | **✗ нет** | ✗ нет | ✓ для файлов внутри `~/agents` | +| **bind-mount** | ✓ работает | да | да | ✓ для внешних каталогов | +| **APFS clonefile** | ✓ (реальная копия) | да | да | ✓ для «копии проекта» без расхода диска | + +### Ключевое ограничение hardlink + +Hardlink **не может пересечь границу файловой системы** и **не может ссылаться на +каталог** (только на файл, и только на macOS — с `-F`). То есть замена симлинка +«проект целиком» через hardlink технически невозможна, если проект лежит +на другом томе или это директория. + +**Вывод:** «линкование проекта извне» = **bind-mount**, а не линк. Линки — для +внутренней организации одного тома. + +📌 **Прямо отвечая на исходный замысел Alex:** hardlink'ом **нельзя** подключить +проект-каталог извне — физически невозможно (каталоги не линкуются, ФС не пересекается). +Поэтому в целевой архитектуре внешние проекты висят на **bind-mount'ах** +(`~/Developer/:/agents/`), а секция Dashboard управляет именно манифестом +`agents-mounts.yaml`. Hardlink-операции остаются как вспомогательный инструмент +для файлов внутри одного тома. + +--- + +## 3. Целевая архитектура + +``` +Mattermost (Alex) + │ «почини баг X в project-y» + ▼ +Балда-оркестратор (Kraken, DeepSeek, mode=allow_all) + │ 1. читает Obsidian, формирует ТЗ + │ 2. /goalkeeper → work → validate + ▼ +Мост (Mac, FastAPI, :8790) ← новый сервис, регистрируется в Eagle Dashboard + │ HTTP + shared secret (WireGuard-хаб уже есть) + │ идемпотентность + таймаут + лимит 1-2 параллельных задач + ▼ +Контейнер-исполнитель (Mac, Docker) + │ -v ~/agents:/agents:rw + │ -v ~/Developer/:/agents/ ← для внешних проектов (bind, не линк) + │ Node 24 + codex + claude + git + │ CLAUDE_CODE_OAUTH_TOKEN из env + ▼ +возврат: diff + вывод тестов + коммит → Балда → MM +``` + +--- + +## 4. Секция «Agents» в Eagle Dashboard + +### Где живёт код (проверено 2026-09-26) + +| Файл | Строк | Что править | +|---|---|---| +| `~/Developer/eagle-dash/main.py` | 889 | новые `@app.get/post/delete` роуты | +| `~/Developer/eagle-dash/templates/index.html` | 1248 | новая вкладка + Alpine.js стейт | +| `~/Developer/eagle-dash/health.py` | 181 | health-чек моста (`type: http`) | +| `~/Developer/eagle-dash/services.yaml` | 6.5 КБ | регистрация сервиса `agents-bridge` | + +**Существующие вкладки:** `services` · `crons` · `pages` · `files` +(роутинг через `?tab=`, `setUrlTab()`, `activeTab` в Alpine). + +### Что делает новая секция + +1. **Список содержимого `~/agents`** — по каждому проекту: имя, тип (реальная папка / + symlink / hardlink), размер, дата изменения, **статус линка** (валиден / битый). +2. **Создать проект** — `mkdir ~/agents/`. +3. **Подключить внешний проект** — **bind-mount-запись**: сохраняется в + `~/Developer/agents-mounts.yaml`, мост перезапускает контейнер с новым `-v`. + ⚠️ **Не symlink** — см. §1. +4. **Hardlink-операции** — `ln -F ~/agents//` (только файлы, + только внутри тома; в UI показывать `st_dev` и блокировать при разных ФС). +5. **Удалить из контейнера** — убрать из `agents-mounts.yaml` + `docker compose up -d` + ⚠️ **Файлы на диске не трогать** — только отвязка от контейнера (см. Core Rules). +6. **Открыть терминал** — ссылка на существующую вкладку `files`. + +### API-контракт (черновик) + +``` +GET /api/agents → [{name, kind, size, mtime, link_valid, target}] +POST /api/agents {name} → mkdir ~/agents/ +POST /api/agents/{name}/mount {host_path} → добавить bind-mount + recreate +DELETE /api/agents/{name}/mount → убрать bind-mount + recreate (файлы целы) +POST /api/agents/{name}/hardlink {file, name} → ln -F (с проверкой st_dev) +GET /api/agents/{name}/usage → размер, место на томе +``` + +### Чего секция НЕ делает + +- **Не удаляет** содержимое `~/agents` — только отвязывает от контейнера. +- **Не убивает** процессы — lifecycle контейнера через существующий supervisor. +- **Не создаёт symlink** на внешние цели — вместо них bind-mount. + +--- + +## 5. Система подтверждений — итог разведки + +### Как устроено в Balda (апстрим, `docs/architecture/interactive-questions.md`) + +Три режима (`permissioncmd.Mode`): + +| Режим | Поведение | +|---|---| +| `allow_all` | никого не спрашивает — **выбран Alex** | +| `ask` | ADK-callback **блокирует турн**, вопрос уходит в чат, ждёт ответа, таймаут fail-closed, продолжает через `ResumeTarget` | +| `deny_all` | всё запрещено | + +🔴 **`ask` сейчас работает только для Telegram и Slackagent** — прямая цитата +`interactive-questions.md`: *«`ask` is supported for Telegram and Slackagent and +fails closed elsewhere»*. Для Mattermost `ask` **фейлится закрыто**. + +### Что это значит для сценария «MCP просит линк → Alex подтверждает из чата» + +Прямой путь **сейчас невозможен** поверх MM. Варианты: + +| Вариант | Объём | Ложится куда | +|---|---|---| +| A. Дописать `ask` в MM | ~1-2 дня | ветка `feat/mattermost-channel` → **расширение PR #8** | +| B. Апрув в мосте (свой мини-эндпоинт + очередь) | ~0.5 дня | FastAPI-мост Фазы 4 | + +**С Alex выбран `allow_all`** — этим заходом подтверждения не нужны. Пункт +оставлен как задел: когда `ask` понадобится, начинать с варианта **B** (дешевле, +не зависит от ревью апстрима). + +--- + +## 6. План работ (по шагам) + +### Шаг 1 — `~/agents` + контейнер-исполнитель (~2 ч) +1. `mkdir ~/agents` + `.gitkeep`, права `admin:staff`. +2. `~/Docker/balda-executor/Dockerfile` — Node 24 + `codex` + `claude` + `git`. +3. `docker-compose.yml` — `-v ~/agents:/agents:rw`, `CLAUDE_CODE_OAUTH_TOKEN` из `.env`. +4. Авторизация: `claude setup-token` на Mac → токен в `~/Docker/balda-executor/.env` (0600). +5. Проверка: `docker exec ... claude --version` + `codex --version`. + +### Шаг 2 — Мост на Mac (~3-4 ч) +1. `~/Developer/agents-bridge/` — FastAPI, порт **8790**. +2. Эндпоинты: `POST /task` (idempotency key), `GET /task/{id}`, `GET /healthz`. +3. Запуск CLI headless в контейнере, возврат diff + тестов. +4. Лимит параллельных задач: **1-2** (риск «unusual traffic» — см. §Риски плана Фаз). +5. Shared secret в заголовке + проверка source IP (WireGuard-подсеть). + +### Шаг 3 — Сеть Kraken → Mac (~1 ч) +1. WireGuard-туннель (хаб на VPS уже есть — см. скилл `wireguard-vps-hub`). +2. Проверка: `curl` с Kraken на Mac `:8790/healthz`. + +### Шаг 4 — Секция «Agents» в Eagle Dashboard (~3-4 ч) +1. `main.py` — 6 роутов из §4. +2. `templates/index.html` — вкладка `agents` + `?tab=agents`. +3. `services.yaml` — запись `agents-bridge` (`health.type: http`, url `:8790/healthz`). +4. `__pycache__` + `launchctl kickstart -k gui/$(id -u)/com.eagle.dashboard`. + +### Шаг 5 — Связка Балда → мост (~2 ч) +1. MCP-инструмент в Балде (или webhook) → `POST /task`. +2. Проверка на задаче-пустышке: `echo ok > /agents/test/probe.txt`. + +### Шаг 6 — Приёмка Фазы 5 (~1 день) +1. Сквозной тест **от аккаунта Alex**: `@balda почини X` → diff в MM. +2. Расходы: DeepSeek-токены + подписочные лимиты. +3. Задокументировать: как работает, где логи, как откатить. + +--- + +## 7. Риски + +| Риск | Оценка | Митигация | +|---|---|---| +| Симлинк в `~/agents` на внешний путь → битый | **подтверждён** | bind-mount вместо линка; UI помечает битые линки | +| Hardlink через границу ФС → `EXDEV` | высокий | проверять `st_dev` перед `ln`; UI блокирует | +| Бан подписочного аккаунта за «unusual traffic» | средний | 1-2 параллельных задачи, официальный бинарь, без прокси токена | +| Полный доступ (`allow_all`) + MCP → агент может снести `~/agents` | **средний** | бэкапы + `git rm`-семантика вместо `rm`; правки только через bind-mount-манифест | +| Мост открыт наружу | высокий | только WireGuard-подсеть + shared secret; не публиковать порт в LAN | + +--- + +## 8. Открыто (требует ответа Alex при старте) + +1. Порт моста — **8790** (взят свободный; проверить `lsof -i :8790` перед стартом). +2. Провайдер исполнителя — **claude** (подписка уже залогинена) или **codex**? +3. Нужен ли `ask`-апрув в будущем, или `allow_all` достаточно постоянно? + +## История +- **2026-09-26**: создан. Решения Alex: папка `~/agents`, full access, управление + линками — секция в Eagle Dashboard. Живым тестом доказано, что симлинк в Docker + не читается → внешние проекты подключать bind-mount'ом, не линком. + +## Связанные заметки +- [[personal/projects/balda/balda-mm-delegation-plan]] — общий план (Фазы 4-5) +- [[personal/projects/balda/balda-kraken-mm-deploy]] — Фаза 3 (✅ бот отвечает) +- [[personal/plans/balda-mm-pr-plan]] — Фаза 2 (PR #8, 8/8 CI, ждёт ревью) +- [[personal/tech/balda-go-fx-pitfalls]] — fx-питфоллы, дельта-апплаер +- скилл `eagle-dashboard` — архитектура дашборда, health-типы, launchd-нюансы +- скилл `wireguard-vps-hub` — туннель Kraken → Mac diff --git a/personal/plans/music-tags-review/music-tags-review.md b/personal/plans/music-tags-review/music-tags-review.md index afa9a756..b5b3f578 100755 --- a/personal/plans/music-tags-review/music-tags-review.md +++ b/personal/plans/music-tags-review/music-tags-review.md @@ -1,7 +1,14 @@ +--- +title: Music Tags — Review +type: task +namespace: personal +tags: [music, jellyfin, tags, review, truenas] +status: awaiting-approval +--- # Music Tags — проверка тегов -**К правке:** 4777 файлов · **без изменений:** 3129 +**К правке:** 6773 файлов · **без изменений:** 1133 > [!warning] Ничего не изменено > Это план. Ставь галочки, что одобряешь. После «go» — бэкап в `Music/backup/` и применение. @@ -11,8 +18,37 @@ | Блок | Файлов | Что это значит | |---|---:|---| | [[1-needs-decision\|Требует решения]] | 28 | Нет ни исполнителя, ни альбома. Взять неоткуда — только имя файла. | -| [[2-medium-confidence\|Средняя уверенность]] | 2144 | Восстановлено из имён файлов и структуры папок. | -| [[3-high-confidence\|Высокая уверенность]] | 2605 | Точная перекодировка CP1251 → UTF-8, обратимая без потерь. | +| **Средняя уверенность** (разбита по категориям) | 5787 | Восстановлено из имён файлов и структуры папок. | +| [[3-high-confidence\|Высокая уверенность]] | 958 | Точная перекодировка CP1251 → UTF-8, обратимая без потерь. | + +### Средняя уверенность — файлы по категориям + +| Категория | Файлов | +|---|---:| +| [[2-medium-confidence-Pop_Club_Любэ\|Pop, Club, Любэ]] | 1141 | +| [[2-medium-confidence-RockFolk_Пилот\|Rock&Folk / Пилот]] | 281 | +| [[2-medium-confidence-RockFolk_Ногу_Свело\|Rock&Folk / Ногу Свело!]] | 153 | +| [[2-medium-confidence-RockFolk_Ночные_Снайперы\|Rock&Folk / Ночные Снайперы]] | 95 | +| [[2-medium-confidence-z_Другое_Enigma_Ленинград\|z Другое, Enigma, Ленинград]] | 903 | +| [[2-medium-confidence-Детские\|!Детские]] | 510 | +| [[2-medium-confidence-Рэп\|!Рэп]] | 469 | +| [[2-medium-confidence-RockFolk_Муммий_Тролль\|Rock&Folk / Муммий Тролль]] | 80 | +| [[2-medium-confidence-RockFolk_Пикник\|Rock&Folk / Пикник]] | 83 | +| _(остальные — в списке файлов блока)_ | | + +> 📂 Раньше это был один файл на 1.1 МБ — Obsidian тормозил. Теперь **56 нот**, +> разбитых по категориям; Rock&Folk разложен по группам (глубина 2). + +## Откуда взялось поле `track` + +В v5 добавлена правка **номера трека**. Раньше номер оставался в названии: + +``` +БЫЛО: title = "01 Hit The Road Jack (Kub vs Ray Charles)" +СТАЛО: title = "Hit The Road Jack (Kub vs Ray Charles)" track = 1 +``` + +Затронуто 5158 файлов. В нотах строка `track: → **N**`. ## Важное про блок 1 @@ -23,6 +59,13 @@ Если подставить туда имя папки, получится мусор вида `artist = Сказки` (238 треков одним «исполнителем») — от этого отказались. +## Что НЕ трогаем (осознанно) + +- **Рукописный транслит** — `EMINЭM`, `КняZz`, `Flёur`, `изBESTняк`, `Скаzки` (126 файлов). + Это оригинальное написание / реальные псевдонимы, а не порча кодировки. +- **`album == artist`** (101) — одноимённые альбомы: `Рубль/Рубль`, `Би-2/2001 Би-2`. +- **Числовые названия** (15) — `007` у Ленинграда, `48` у Пилота — это реальные треки. + ## Формат Каждый файл — чекбокс `- [ ]`. Под ним: поле, старое значение → **новое**, и источник diff --git a/personal/projects/balda/balda-mm-delegation-plan.md b/personal/projects/balda/balda-mm-delegation-plan.md index a05bf7db..68c03d41 100644 --- a/personal/projects/balda/balda-mm-delegation-plan.md +++ b/personal/projects/balda/balda-mm-delegation-plan.md @@ -302,6 +302,12 @@ PR: `gh pr create --repo baldaworks/balda --base main --head mallexxx:feat/matte Безлицензионная альтернатива — SMTP-почта, решение за Alex. ### Фаза 4 — исполнитель в VM/Docker на Mac (~2-3 дня) +0. ✅ **План зафиксирован: [[personal/plans/balda-mm-phase4-agents]].** + Решения Alex: папка **`~/agents`**, режим **`allow_all`** (full access), + управление линками — **новая секция «Agents» в Eagle Dashboard**. + 🔴 **Доказано живым тестом: симлинк в Docker НЕ читается** → внешние проекты + подключать **bind-mount'ом**, не линком. Hardlink — только для файлов внутри + одного тома (не пересекает ФС, каталоги не линкует). 1. Docker-контейнер (или UTM VM) с Node 24 + `codex` + `claude` + `git`. 2. Авторизация: `claude setup-token` на Mac → `CLAUDE_CODE_OAUTH_TOKEN` в контейнер; для codex — свой flow. 3. Bind-mount проектной директории (не весь `~`). @@ -420,6 +426,19 @@ PR: `gh pr create --repo baldaworks/balda --base main --head mallexxx:feat/matte [[personal/tech/hermes-secret-redaction-bypass]]. - ⏳ Дальше: сборка образа на Mac → деплой → приёмка в UI MM. Детальный план: [[personal/projects/balda/balda-kraken-mm-deploy]]. +- **2026-09-26 (после приёмки)**: **Разведка под Фазу 4.** Фаза 3 подтверждена закрытой + (бот отвечает аккаунту Alex, `settlement_stage=accepted`). + - Alex решил три развилки: папка исполнителя — **`~/agents`**; подтверждения — + **full access** (`permissions.mode: allow_all`); управление линками — **новая секция + «Agents» в Eagle Dashboard** (`~/Developer/eagle-dash/`). + - 🔴 **Живой тест Docker (alpine:3.20 arm64): симлинк в bind-mount НЕ читается** — + цель резолвится от корня контейнера. Следствие: внешние проекты подключать + **bind-mount'ом** через манифест `~/Developer/agents-mounts.yaml`, не линком. + Hardlink каталоги не линкует и ФС не пересекает (`EXDEV`) — только файлы внутри тома. + - Уточнена система подтверждений: `ask` в Balda поддержан **только** для Telegram и + Slackagent, для Mattermost фейлится закрыто (`docs/architecture/interactive-questions.md`). + - План зафиксирован: [[personal/plans/balda-mm-phase4-agents]]. Работа **не начата** — + ждёт выбора Alex: Шаг 1 (`~/agents` + контейнер) или Шаг 4 (секция в Dashboard). ## Связанные заметки - [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1: MM на Kraken (arm64-блокер, деплой) diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index 45ca6915..86912911 100755 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -16,13 +16,119 @@ related: - '[[family/how-to/jellyfin-config]]' - '[[family/projects/media-toolbox-taiga]]' --- + # Music Tags Repair — TrueNAS (7906 файлов) -> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV v4 + ноты ревью отданы Alex. Правки файлов НЕ начаты.** +> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV v5 + ноты ревью отданы Alex. Правки файлов НЕ начаты.** > Следующий шаг — апрув Alex по нотам ревью, затем бэкап + применение. -> Прогонялись три волны проверок: полный аудит всех строк (`audit_plan.py` + `final_gate.py`) → 6 багов, -> затем третья волна по замечаниям Alex → 3 бага (en-dash + подстроковый поиск стоп-слов). -> Итог v4: 4777 `fix`, mojibake = 0, 80 тестов PASS, блок «требует решения» 296 → 28. +> Прогонялись **четыре волны** проверок: полный аудит всех строк (`audit_plan.py` + `final_gate.py`) → 6 багов, +> третья волна по замечаниям Alex → 3 бага (en-dash + подстроковый поиск стоп-слов), +> **четвёртая волна (ручной проход) → 10 багов (track-номера, disc-track, восклицательные знаки, регистр)**. +> Итог v5: 6773 `fix`, mojibake = 0, 121 тест PASS, блок «требует решения» 296 → 28. + +## 🔴 ЧЕТВЁРТАЯ ВОЛНА: РУЧНОЙ ПРОХОД И TRACK-НОМЕРА (2026-09-26) + +Alex: **«Ты блядь руками пройди и пофикси все что мне предлагал кроме того что неочевидно! +Чё за автообработка то!!»** + **«01 Hit The Road Jack (Kub vs Ray Charles), 06 Benassi I am a Dj +и другие подобные — в них тебе что не понятно? Почему ты вписал все включая трек намбер в тайтл?»** + +Оба замечания верны. Автоматика выжата — дальше только глазами по всему файлу (8051 строка ревью). + +### 🔴 ГЛАВНОЕ: ведущий номер трека ехал в title + +Было: `title = "01 Hit The Road Jack (Kub vs Ray Charles)"`. Стало: `title = "Hit The Road Jack…"`, `track = 1`. +**Затронуто 5158 файлов** — это массовый случай, не единичный. + +**Причина:** `PREFIX_RE = r"^\s*\d{1,3}\s*[-._)\]]\s*"` требовал **пунктуацию** после номера. +`01 Hit…` (пробел после номера) не матчился. + +**Новое поле `new_track`** добавлено в CSV-план и в ноты ревью. Функция `split_track_number(stem) -> (track, title)`. + +**Условия извлечения номера** (все проверены тестами `test_track_numbers.py`, 25 кейсов): + +| Случай | Пример | Результат | +|---|---|---| +| Обычный ведущий номер | `01 Hit The Road Jack` | track=1, title=чисто | +| Номер в теге, не в имени | `24 Моцарт Турецкий марш` | track=24 | +| **Disc-track** | `1-01. Albibeno` | track=`01`, НЕ `1` (иначе весь диск = один номер) | +| **Ведущие нули** | `001 БУРАТИНО` | track=1 (3 цифры с нулём = позиция, не id плейлиста) | +| **Трёхзначный без нуля** | `159_Css`, `149_Thriving` | **не трогать** — это номер архива/плейлиста | +| Остаток — число | `05 48`, `11 140` | **не трогать** — это реальное название | +| 4 цифры = год | `1999 Жывой Концерррт` | **не трогать** | +| **Строчная после номера** | `50 cent - p.i.m.p` | **не трогать** — «50» часть имени артиста | +| `00` | `00-intro-family-affair` | **не трогать** — track 0 не существует в ID3 | + +> 🔴 **ПИТФОЛЛ: `PREFIX_RE` НЕ должен срезать номер.** Сначала номер убрали из `PREFIX_RE` +> (чтобы `50 cent` не резался) — это **сломало** путь «отвергнутый `TrackNN` → `<Папка> `», +> `numeric_title` подскочил 22 → 67. Решение: `title_from_name()` вызывает `split_track_number()` +> как единый источник истины по номерам, а не регекс. + +### Остальные баги, найденные ручным проходом + +| # | Баг | Пример | Фикс | +|---|---|---|---| +| 1 | Номер в **artist**, не title | `10 Armin Van Buuren feat. Jan Vayne - Serenity` → artist=`10 Armin…` | `split_track_number` перед `split_artist_title` в обеих ветках (`artist<-filename`, `artist<-filename(replaced-release)`) | +| 2 | `Track NN` оставался title | `11. Josh Wink - Higher State…` → title=`Track 11` | после срезания номера остаток `TrackNN` → брать имя файла | +| 3 | Disc-track схлопывался | `1-01..1-10` все → track=1 | `md = re.match(r"^\s*(\d{1,2})\s*[-–—]\s*(\d{1,2})…")` → track=MM | +| 4 | Восклицательные знаки в artist | `!!Dj_hottab_-_test…` → artist=`!!Dj hottab` | `clean_value`: `v.strip("!")`, `PREFIX_RE = r"^\s*!+\s*"` | +| 5 | Заглавные союзы из CP1251 | `Êîðîëü È Øóò` → `Король И Шут` | `normalise_case()` + `FUNCTION_WORDS` (29 файлов) | +| 6 | Ложный сплит в сборнике | `ДВАЖДЫ ДВА - ЧЕТЫРЕ` → artist=`ДВАЖДЫ ДВА` | не сплитить, если родитель в `NON_ARTIST` | +| 7 | `track = 0` | `00-intro`, `00 Geo…` | `if not num.strip("0"): return "", rest` | + +**`normalise_case()` — узкое правило:** понижать русский служебный союз/предлог (`и`, `в`, `на`, `с`, `за`…) +только если (а) не первое слово, (б) значение **не в ALL-CAPS** (капс — стиль, `НЕ ХОДИТЕ В АФРИКУ` не трогаем), +(в) предыдущее слово не `&`/`+`/`and` (`Чиж & Со` — «Со» часть названия). 16 тестов. + +### ⚪ Что ручной проход НЕ стал чинить (осознанно) + +**Рукописный транслит — 126 файлов.** `EMINЭM`, `КняZz`, `Flёur`, `изBESTняк`, `Скаzки`, `ИNOМАРКИ`, +`Глюкоzа`, `Cо`, `WWWаленки`, `Закoлебал`. Это **оригинальное написание** (мода 2000-х) / реальные +псевдонимы (`КняZz`, `Flёur`), а **не порча байтов**. Заменять `Э`→`E` в произвольном имени = выдумывать. +Оставлено как есть, задокументировано как законное. + +**`numeric_title` — 15 файлов, все законные:** `007` у Ленинграда, `48` у Пилота, `11 - 11`, +`Високосный год/06.mp3` (имени нет, взять негде). + +**`album_equals_artist` — 101 файл, законные:** одноимённые альбомы (`Рубль/Рубль`, `Би-2/2001 Би-2`). + +### 📊 Цифры четвёртой волны + +| Метрика | v4 | **v5 (финал)** | +|---|---:|---:| +| `fix` | 4777 | **6773** | +| `no_change` | 3129 | 1133 | +| Ноты: «требует решения» | 28 | **28** | +| mojibake в выводе | 0 | **0** | +| Тесты | 80 | **80 + 41 новых = 121** | +| `bad_track` (track вне 1–99) | — | **0** | +| `no_visible_change` | 2301 | **0** (аудит теперь считает track) | + +**Новые скрипты четвёртой волны:** + +| Файл | Что делает | +|---|---| +| `manual_review.py` | Печать всех правок по папкам в читаемом виде (`T:`/`A:`/`L:`/`#:`). Аргументы `--dir`, `--limit`. Именно им пройден весь файл. | +| `test_track_numbers.py` | 25 тестов извлечения ведущего номера | +| `test_titlecase.py` | 16 тестов нормализации служебных слов | +| `test_separators.py` | 15 тестов разделителей artist/title (третья волна) | + +**Обновлены:** `audit_plan.py` (класс `bad_track`, `track_not_canonical`; `no_visible_change` учитывает track), +`build_fix_plan2.py` (колонка `new_track`, `split_track_number`, `normalise_case`, `has_word`, `find_separator`). + +### 📂 НОТЫ РЕВЬЮ РАЗБИТЫ ПО КАТЕГОРИЯМ + +`2-medium-confidence.md` разросся до **1.1 МБ / 3932 чекбокса** — Obsidian тормозит. +`render_grouped_doc.py` теперь **рекурсивно** делит блок >1200 файлов по категориям +(глубина 1 → `!Rock&Folk`, глубина 2 → `!Rock&Folk/Пилот`), лимит `max_items=1200`. + +Итог: **56 файлов**, крупнейший 317 КБ. `Rock&Folk` разложен по группам: +`Пилот` (281), `Ногу Свело` (153), `Номумий Тролль` (80), `Ночные Снайперы` (95), `Пикник` (83). +Функции: `note_body()`, `split_by_category(depth, max_items)`. Индекс `music-tags-review.md` +ссылается на все созданные ноты. + +> ⚠️ **Собрано в `/tmp/rg3/` (56 файлов), в vault ЕЩЁ НЕ СКОПИРОВАНО.** Старая версия +> (296 «требует решения») лежит в `personal/plans/music-tags-review/`. Нужно: скопировать +> `/tmp/rg3/*.md` → vault, удалить старые `2-medium-confidence*.md`, синк. ## 📌 ПЛАН ТРЕТЬЕЙ ВОЛНЫ (2026-09-26, апрув Alex) — «сам проставь, потом на апрув» @@ -503,11 +609,12 @@ if cyr < 2: # одна буква — совпадение | # | Шаг | Статус | |---|---|---| | 1 | **Инвентаризация** read-only: `ffprobe` по 7906 файлам → CSV | ✅ **ГОТОВО (v2)** — `tag_inventory2.py`, `probe_failed`=0 | -| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v4)** — `~/Downloads/music-tags/tag-fix-plan.csv`, 4777 `fix` / 3129 `no_change` | -| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex) | ✅ **ГОТОВО (v4)** — `personal/plans/music-tags-review/`, пересинкнуто | +| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v5)** — `~/Downloads/music-tags/tag-fix-plan.csv`, 6773 `fix` / 1133 `no_change`, +колонка `new_track` | +| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex) | ✅ **ГОТОВО (v5)** — 56 файлов, разбиты по категориям; ⚠️ собрано в `/tmp/rg3/`, в vault НЕ скопировано | | 2b | **Полный аудит всех строк** (`audit_plan.py` + `final_gate.py`) → 6 багов исправлено | ✅ **ГОТОВО** (запрос Alex «Сам проверил весь файл?») | | 2c | **Третья волна** по замечаниям Alex: чекбоксы, en-dash, подстроковый поиск стоп-слов | ✅ **ГОТОВО** — 3 бага, блок «требует решения» 296→28 | -| 3 | **Апрув Alex по нотам ревью** → только после этого правки | ⏳ **ЖДЁМ** (v1–v3 отклонены, v4 + ноты отданы) | +| 2d | **Четвёртая волна (ручной проход)**: track-номера, disc-track, `!`-префиксы, регистр, ложный сплит | ✅ **ГОТОВО** — 10 багов, 5158 файлов получили track, 121 тест PASS | +| 3 | **Апрув Alex по нотам ревью** → только после этого правки | ⏳ **ЖДЁМ** (v1–v4 отклонены, v5 + ноты отданы) | | 4 | **Бэкап** — копировать **только перезаписываемые** файлы в `Music/backup/` | ⏳ | | 5 | **Ремонт** — перекодировка mojibake + восстановление из имён путей + чистка мусора | ⏳ | | 6 | **Приёмка** — Alex слушает выборку | ⏳ | @@ -568,20 +675,26 @@ if cyr < 2: # одна буква — совпадение | `diagnose_probe_failures.py` | Анализ заголовков файлов, упавших в `probe_failed`: ID3-версия, `hdr_ok`, размеры | | `test_encoding_guard.py` | **32 регресс-теста (v1).** Защита от порчи корректного UTF-8 (`Björk`, `Saint-Saëns`, `Noël`) + `split_artist_title` | | `test_plan2.py` | **65 регресс-тестов (v3, актуальный).** Пословная конверсия смешанных строк, короткие токены (`7Á`→`7Б`), контейнеры не становятся артистом, release-папки, отказ сплита на event-именах | -| `render_grouped_doc.py` | **Генератор нот для Obsidian.** CSV → 3 блока по уверенности + обзор, группировка по папкам, чекбоксы, оглавление. **v4:** чекбоксы без префикса `> ` (иначе Obsidian парсит их как цитату и они не кликаются); `ROOT = /storage/Music/` (пути в CSV относительные от `/storage`, не от `/mnt/...`); блок «требует решения» = `нет artist И нет album` | -| `test_separators.py` | **15 регресс-тестов (v4).** Разделители `–`/`—`/приклеенный дефис; отказ сплита на `Рок-н-ролл`, `БИ-2`; event-имена (`НАШЕСТВИЕ 2009 - …`) не расщепляются; `VA - Title` и `Сказка - Теремок` возвращаются целиком. Зовётся `python3 test_separators.py` в `tools/music-tags/` | +| `test_separators.py` | **15 тестов (третья волна).** Разделители `-`/`–`/`—` в artist/title, `Doray- Apocalypse`, отказ на event-именах | +| `test_track_numbers.py` | **25 тестов (четвёртая волна).** `split_track_number`: disc-track `1-01`, ведущие нули `001`, трёхзначные `159_`, `05 48`, `50 cent`, год `1999` | +| `test_titlecase.py` | **16 тестов (четвёртая волна).** `normalise_case`: `Король И Шут`→`Король и Шут`, ALL-CAPS не трогается, `Чиж & Со` защищён | +| `manual_review.py` | **Ручной проход (четвёртая волна).** Печать всех правок по папкам: `T:`/`A:`/`L:`/`#:` префиксы, `--dir`, `--limit`. Именно им пройден весь файл глазами | +| `render_grouped_doc.py` | **Генератор нот для Obsidian.** CSV → блоки по уверенности + обзор. Рекурсивное деление блоков >1200 файлов по категориям (`note_body`, `split_by_category(depth, max_items)`). Чекбоксы БЕЗ `> ` | +| `final_gate.py` | **Гейт полноты.** Проверяет совпадение числа строк с инвентарём, отсутствие заглушек/пустых title/U+FFFD/правок без `source` | | Артефакт | Где | Что | |---|---|---| | CSV-инвентарь v2 | `/mnt/RED_2TB/storage/tmp/tag-inventory2.csv` (NAS) → `~/Downloads/music-tags/tag-inventory.csv` (Mac) | 7906 строк, колонка `source`, `probe_failed`=0 | -| **CSV-план правок v4** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, ~2.2 МБ) | **Отдан Alex 2026-09-26 (финал).** 4777 `fix` / 3129 `no_change` / 0 `probe_failed` | +| **CSV-план правок v5** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, ~2.2 МБ) | **Отдан Alex 2026-09-26 (финал).** 6773 `fix` / 1133 `no_change` / 0 `probe_failed`, +`new_track` | | Копия БД Jellyfin | `/tmp/jf-copy.db` (Mac) | Для чтения API-ключа (см. `[[family/how-to/jellyfin-config]]`) | | Копия плана на NAS | `/tmp/tag-fix-plan.csv` (NAS) | Для запуска `id3_direct.py`/`check_ddt_album.py` с хостовым Python | -| **Ноты ревью в vault** | `personal/plans/music-tags-review/` (4 файла) | Обзор + 3 блока по уверенности; **вход для апрува Alex**, синкнуто | +| **Ноты ревью в vault** | `personal/plans/music-tags-review/` | Ранее 4 файла; **v5 — 56 файлов**, разбитых по категориям (Rock&Folk по группам). ⚠️ собрано в `/tmp/rg3/`, в vault ещё НЕ скопировано | ### Колонки `tag-fix-plan.csv` -`path | current_verdict | cur_title | cur_artist | cur_album | new_title | new_artist | new_album | action | source` +`path | current_verdict | cur_title | cur_artist | cur_album | new_title | new_artist | new_album | new_track | action | source` + +**v5:** добавлена колонка **`new_track`** (1–99 или пусто). Заполнена у 5158 строк. Последняя колонка `source` показывает, **откуда** взято значение — `title<-filename`, `artist<-folder`, `artist<-filename`, `title<-folder+tracknum`, `translit artist tag; folder is Cyrillic`.