[2026-09-26] eagle: family/how-to/vault-git-sync.md personal/plans/balda-mm-pr-plan.md personal/tech/balda-go-fx-pitfalls.md personal/tech/music-tags-repair-truenas.md

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 14:42:05 +06:00
1 parent e200596c45
commit 036e7a7091
4 files changed
+272 -30

No files matched your search

+30 -1
View File
@@ -12,6 +12,35 @@ tags:
# Vault Git Sync
> **🔴 STATE 2026-09-26 — РАСХОЖДЕНИЕ РАСПИСАНИЯ: Taiga-сервер синкается РАЗ В ЧАС, не 5 минут.**
> Job `sync-vault` внутри контейнера `hermes-taiga` (Hermes cron, id `29851c413903`) расписан
> **`0 * * * *`** — то есть раз в час, **хотя таблица ниже (и `family/how-to/obsidian-sync`)
> заявляет «*/5»**. Обнаружено при проверке «доехал ли док до телефона»: ноты, залитые с Mac
> в 14:32, в клоне `obsidian-syncthing` отсутствовали.
>
> **Следствие:** изменения с Eagle/Kraken доезжают до телефона с задержкой **до 60 минут**.
> Цепочка `bare repo → git-клон obsidian-syncthing → Syncthing → телефон` рвётся на **втором**
> шаге (`Syncthing здоров` ≠ `док доехал`).
>
> **Ручной триггер (проверено):**
> ```bash
> docker exec hermes-taiga sh -c 'cd /opt/data && HERMES_HOME=/opt/data \
> /opt/hermes/.venv/bin/hermes cron run 29851c413903'
> # → Triggered job: sync-vault … Ran now: succeeded.
> ```
> После триггера: клон обновлён, Syncthing `needFiles: 0`, телефон `completion: 100`.
>
> **Открытый вопрос к Alex: менять расписание на `*/5 * * * *` или оставить раз в час?**
>
> ⚠️ **Диагностика синка — только изнутри контейнера Syncthing:** порт `8384` слушает лишь
> в контейнере, `curl` с хоста TrueNAS даёт пустоту.
> ```bash
> docker exec syncthing sh -c 'K=$(grep -oE "<apikey>[^<]+" /var/syncthing/config/config.xml | sed "s|<apikey>||"); \
> curl -s -H "X-API-Key: $K" "http://localhost:8384/rest/db/status?folder=k5vyy-gzdgj"'
> ```
> Папка Syncthing: id `k5vyy-gzdgj` «Obsidian Vault», устройства `SM-S931B` (телефон) и `syncthing-taiga`.
> Готовый скрипт: `~/Developer/personal-os/tools/music-tags/check_syncthing2.sh`.
> **✅ STATE 2026-09-22 — рецидив ACL закрыт перманентно.** Дефект вернулся (35 root-каталогов),
> push с Mac падал. Починено: **236/236 каталогов `INHERIT` + `WRITE`** через
> `midclt filesystem.setacl` (read-modify-write + ожидание job). **Наследование подтверждено
@@ -76,7 +105,7 @@ No stash. No GIT_DIR/GIT_WORK_TREE workarounds.
|--------|-------------------------------|---------------|----------------------|
| Eagle | `~/scripts/sync-vault.sh` | full clone | **launchd** `com.sync-vault` (⚠️ НЕ Hermes cron) `StartInterval=300` |
| Kraken | `~/scripts/sync-vault.sh` | sparse clone | host crontab */5 |
| Taiga | `/opt/data/sync-vault.sh` | 3-phase orch. | Hermes cron */5 |
| Taiga | `/opt/data/sync-vault.sh` | 3-phase orch. | 🔴 **Hermes cron `0 * * * *` (РАЗ В ЧАС!)** — job `29851c413903`, см. STATE 2026-09-26 выше. Не `*/5`, как считалось ранее |
> **⭐ УТОЧНЕНО 2026-09-02 (Eagle/мак sync-механизм):** автосинка на маке запускается **НЕ Hermes cron, а launchd-агентом** `~/Library/LaunchAgents/com.sync-vault.plist`:
> - Программа: `/Users/admin/scripts/sync-vault.sh`, `StartInterval = 300` сек (5 мин)
+79 -1
View File
@@ -7,7 +7,85 @@
> Со всеми поддерживаемыми фичами и тестами. Вперёд! Без вопросов и требующих апрува команд.»
> Часть [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2 → PR в апстрим).
## Итог
## ✅ Итог: ветка PR-clean (2026-09-26, финал)
**7 коммитов**, рабочее дерево **чистое**, `tools/` **исключён** из ветки.
```
9f1e6c9c chore: ignore local project-editing tooling
58f27120 docs(mattermost): document the transport setup and behavior
80d4f41d test(mattermost): cover the transport, the ingress and the handler
c7f18654 fix(mattermost): send a monotonic seq on every client frame
6dc3077a feat(mattermost): add the inbound handler and wire it into DI
fa1a2855 feat(auth): support the Mattermost channel subject
004fda2f feat(mattermost): add Mattermost transport adapter ← база прошлой сессии
```
### 🔴 Уборка `tools/` — важный урок про PR
База `004fda2f` (прошлая сессия, **не мои 5 коммитов**) затащила в ветку **16 файлов
`tools/`**: `apply-project-deltas.py`, `run-project-deltas.sh`, `deltas-mattermost*.json`,
`.deltas-state.json`. **В апстриме каталога `tools/` нет вообще** — проверено
`git ls-tree upstream/main tools/` → пусто.
**Как найдено:** `git diff --stat upstream/main...HEAD` показал `tools/` в списке файлов PR.
**Фикс (коммит `9f1e6c9c`):**
1. `/tools/` добавлен в `.gitignore`;
2. `git rm -r --cached tools/` — файлы **остались на диске** (39 файлов), ушли только из git.
⚠️ **`gitignore` НЕ отвязывает уже отслеживаемые файлы** — нужен именно `git rm --cached`.
⚠️ **Проверять состав PR через `git diff upstream/main...HEAD`, а не по своим коммитам.**
Свои 5 коммитов были чистыми (`git log upstream..HEAD -- tools/` → пусто), но ветка в
целом — нет, из-за базового коммита.
**Результат:** в PR-diff **41 файл**, все — файлы проекта, ни одного `tools/`.
### Финальные гейты — все зелёные на закоммиченном дереве
```
go build ./... → exit 0
go vet ./... → exit 0
go tool golangci-lint run → 0 issues
go tool go-arch-lint check --project-path . → OK - No warnings found
go test -race (затронутые пакеты) → все ok
тесты mattermost + handlersfx (verbose) → 126 PASS, 0 FAIL, 0 SKIP
```
**Тестов добавлено:** **103** (`grep -c '^func Test'`), по файлам:
| Файл | Тестов |
|---|---|
| `channel/mattermost/ingress_test.go` | 23 |
| `channel/mattermost/inbound_test.go` | 18 |
| `channel/mattermost/adapter_test.go` | 17 |
| `handlersfx/mattermost_ingress_test.go` | 17 |
| `channel/mattermost/locator_codec_test.go` | 15 |
| `channel/mattermost/client_test.go` | 13 |
### Предсуществующие падения — НЕ наши (проверено `git stash`)
5 пакетов: `cmd/balda`, `apps/balda`, `mcpruntime`, `session`, `state`. Те же 4 теста
падают **без** моих правок:
`TestInitCommand_NonInteractiveAutoSelectsRootAndGeneratesDetectedAgents`,
`TestCanonicalSessionMemoryRuntimeComposesPortableCapabilities`,
`TestCanonicalSessionMemoryRuntimeMigratesAndReopensGroupedStore`,
`TestResolveWorkspaceBaseBranch_ConfigPreferredWhenValid`.
### Лишнее, что я НЕ отправляю в PR
`tools/mm-*.sh` (9 ops-скриптов), `tools/deltas-*.json` (16 файлов дельта-групп),
`tools/apply-project-deltas.py`, `tools/run-project-deltas.sh`, `tools/.deltas-state.json`
— локальный инструментарий, остаётся на диске, в git не идёт.
### Осталось (по желанию Alex)
1. `git push -u origin feat/mattermost-channel` → открыть PR в `baldaworks/balda`.
2. Фаза 3/4 — [[personal/projects/balda/balda-kraken-mm-deploy]].
---
## Итог (промежуточный, до уборки tools/)
Ветка `feat/mattermost-channel`, **6 коммитов** (базовый `004fda2f` + 5 новых). Готово к `git push` и PR.
+31
View File
@@ -1192,6 +1192,37 @@ git log --oneline --name-only <mой_первый_коммит>^..HEAD -- '<dir>
Альтернатива без «удаления»: оставить файлы untracked, а из индекса убрать одним
явным коммитом по команде Alex.
## 🔴 PR-гигиена: проверять состав ветки, а не свои коммиты
База ветки `004fda2f` (коммит **прошлой сессии**) затащила в PR **16 файлов `tools/`**
(`apply-project-deltas.py`, `run-project-deltas.sh`, `deltas-mattermost*.json`,
`.deltas-state.json`). В апстриме каталога `tools/` **нет вообще** — проверка:
`git ls-tree upstream/main tools/` → пусто.
🔴 **Свои коммиты были чистыми**, но ветка — нет:
```bash
git log --oneline --name-only upstream/main..HEAD -- 'tools/' # → показал базу, не мои коммиты
```
Поэтому смотреть надо **состав PR**, а не свои коммиты:
```bash
git diff --name-only upstream/main...HEAD # все файлы PR
git diff --name-only upstream/main...HEAD | grep -v '^tools/' # что должно остаться
```
**Фикс — два шага, оба обязательны:**
```bash
echo '/tools/' >> .gitignore
git rm -r --cached tools/ # 🔴 файлы ОСТАЮТСЯ на диске, уходят только из git
```
⚠️ **`gitignore` НЕ отвязывает уже отслеживаемые файлы.** Без `git rm --cached` файлы
останутся в индексе навсегда, сколько ни игнорь их.
⚠️ `git rm -r --cached` уходит на **апрув** (рекурсивное удаление) — если апрув истёк,
команда **не выполнилась**, состояние целое; проверить `git diff --cached --name-only`
до повтора.
⚠️ Артефакты деплоя (`mm-*.sh` ops-скрипты) в PR не шлём — только код транспорта,
wiring, тесты, доки.
## Связанные заметки
- [[personal/projects/balda/balda-mattermost-connector]] — Фаза 2, где всё это найдено
+132 -28
View File
@@ -18,8 +18,81 @@ related:
---
# Music Tags Repair — TrueNAS (7906 файлов)
> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26), затем ПЕРЕДЕЛАНЫ после замечаний Alex. CSV v2 передан. Документы ревью залиты в vault и синкнуты. Правки файлов НЕ начаты.**
> Следующий шаг — апрув Alex по CSV, затем бэкап + применение.
> **Статус: ЭТАПЫ 1–2 ЗАВЕРШЕНЫ (2026-09-26). CSV v3 + ноты ревью отданы Alex. Правки файлов НЕ начаты.**
> Следующий шаг — апрув Alex по нотам ревью, затем бэкап + применение.
> Перед выдачей проведён **полный аудит всех 4774 строк** (`audit_plan.py` + `final_gate.py`) — найдено и исправлено 6 багов.
## 🔴 ВТОРАЯ ВОЛНА ИСПРАВЛЕНИЙ (2026-09-26, «Сам проверил весь файл на адекватность?»)
Alex: **«Сам проверил весь файл на адекватность? Чтобы я в очевидные вещи тебя не тыкал.»**
Честный ответ был — **нет, полностью не проверил**: до этого гонялись только выборочные проверки
(`find_bad_titles.py` — 5 метрик). Полный аудит по всем 4774 строкам нашёл **6 реальных багов**.
| # | Баг | Файлов | Как проявлялся | Фикс |
|---|---|---:|---|---|
| 1 | `artist = Сказки` | **238** | Папка-контейнер `!Детские/Сказки/` попадала в artist → Jellyfin показал бы 238 треков одним «исполнителем» | `NON_ARTIST` применяется к контейнеру; под деревом сказок одиночная папка идёт в **album**, artist пустой |
| 2 | Mojibake выживал рядом с латиницей | **127** | `Èâàíóøêè International`, `Ìóìèé Òðîëëü vs DJ Scratchin` — guard по доле кириллицы **на всю строку** отвергал конверсию | `fix_encoding` переписан **пословно** (`re.split(r"(\s+)")`): битые слова чинятся, латиница не трогается |
| 3 | Имя радиовыпуска как артист | **31** | `!Radio/y OOF/`, `v 2009-03 mIr.vEsNЫ` → artist | `is_release_folder()`: `!`-префикс, год, `RELEASE_WORDS`; плюс fallback на имя файла |
| 4 | Номер склеен с названием | **17** | `11 Track 11`, `04 Yves Larock - Rise Up` | Срезание ведущего номера + `title<-filename(denumbered)` |
| 5 | `album = '+'` | **10** | Папка `старые/+/` — только символы | `clean_folder()`: компонент без букв/цифр отбрасывается |
| 6 | `©` как title | **1** | Значение из одних символов | `clean_value()` отвергает значение без `isalnum()` |
**Дополнительно поймано и исправлено:**
- `Пилот/1999 Жывой Концерррт` — фильтр релизов убивал **легитимный альбом** с годом. Разделено: строгая проверка только для **слота артиста** (`dirs[-2]`), альбом может содержать год.
- 18 правок без `source` → теперь помечаются `encoding/cleanup only`.
- Ложные тревоги, разобранные вручную (НЕ баги): `Би-2/2001 Би-2`, `Рубль/Рубль`, `Сектор Газа/1989 - Сектор Газа` — **одноимённые альбомы**; `NTL`, `Eagles`, `Carl Orff` — реальная латиница; `NTL ft. MC Молодой`, `Flёur` — нормальные смешанные имена.
### 📊 Цифры после второй волны
| Метрика | v1 (отклонён) | v2 | **v3 (финал)** |
|---|---:|---:|---:|
| `fix` | 4601 | 4961 | **4774** |
| `no_change` | 2946 | 2945 | 3132 |
| `probe_failed` | 359 | 0 | **0** |
| Ноты: «требует решения» | 9 | 9 | **296** |
| Ноты: средняя | 2611 | 2611 | 2031 |
| Ноты: высокая | 2341 | 2341 | 2447 |
> 📌 **«Требует решения» вырос 9 → 296 — это честнее.** Раньше вместо признания «источника нет»
> подставлялся мусор (`Сказки`, `y OOF`). Теперь такие файлы явно идут в блок 1.
**Итоговые гейты (все PASS):**
| Проверка | Скрипт | Результат |
|---|---|---|
| Полный аудит по классам ошибок | `audit_plan.py` | mojibake = **0**, `generic_artist` = **0**, `numeric_title` — законные |
| Полнота и отсутствие заглушек | `final_gate.py` | **PASS** — нет `probe_failed`, пустых title, U+FFFD, правок без источника |
| Регресс-тесты | `test_plan2.py` | **65/65 PASS** |
**Оставшиеся флаги аудита — все проверены вручную и законны:** `album == artist` (49, одноимённые альбомы),
`translit_kept` (83, реальная латиница), `mixed_script` (19, `NTL ft. MC Молодой`), `numeric_title` (15, `007`/`48`).
## 📡 SYNCTHING: ноты доехали, но найдено расхождение в расписании
Проверка по просьбе Alex («Проверь что syncthing всосал доку»). Первая проверка показала:
`obsidian-syncthing` **отстал на 3 коммита**, нот в нём нет.
**Причина — не Syncthing.** Цепочка «bare repo → git-клон `obsidian-syncthing` → Syncthing → телефон»
рвётся на **втором** шаге: job `sync-vault` в контейнере `hermes-taiga` расписан **`0 * * * *` — раз в час**,
хотя в `[[family/how-to/vault-git-sync]]` записано «каждые 5 минут».
**Что сделано:** job триггернут вручную (`hermes cron run 29851c413903`). Проверено:
| Проверка | Значение |
|---|---|
| Клон `obsidian-syncthing` | ✅ HEAD `e200596`, 4 файла на месте |
| Syncthing: очередь | ✅ `needFiles: 0`, `needItems: 0`, `needDeletes: 0` |
| Синк с телефоном | ✅ `completion: 100`, `remoteState: valid` |
| Ошибки | ✅ `errors: 0` |
| Версия Syncthing | `v2.1.5 "Hafnium Hornet"`, папка `k5vyy-gzdgj` «Obsidian Vault» |
**Питфоллы синка этой сессии:**
- 🔴 **Порт Syncthing `8384` слушает ТОЛЬКО внутри контейнера** — `curl` с хоста TrueNAS отдаёт пустоту.
Запрос API делать через `docker exec syncthing curl -s -H "X-API-Key: …" http://localhost:8384/rest/…`.
- ⚠️ **`obsidian-syncthing` — git-клон (remote `/vault.git`), а не Syncthing-папка.** Обновляет его
Taiga по крону; Syncthing лишь раздаёт то, что уже в клоне. «Syncthing здоров» ≠ «док доехал».
- ⚠️ **Скрипт диагностики синка** — `check_syncthing2.sh` (в `tools/music-tags/`): версия, статус папки,
connections, completion для телефона. Писать в файл, а не инлайн — иначе `APIKEY=` в heredoc режется.
## 📋 ДОКУМЕНТЫ РЕВЬЮ В VAULT (2026-09-26)
@@ -30,9 +103,9 @@ CSV отдан не только файлом, но и **читаемыми но
| Файл | Содержимое |
|---|---|
| `music-tags-review.md` | **Обзор** — сводка, ссылки на три блока |
| `1-needs-decision.md` | **Требует решения** (9 файлов) — исполнителя взять неоткуда |
| `2-medium-confidence.md` | **Средняя уверенность** (2611) — из имён файлов и папок |
| `3-high-confidence.md` | **Высокая уверенность** (2341) — точная перекодировка |
| `1-needs-decision.md` | **Требует решения** (296) — исполнителя взять неоткуда |
| `2-medium-confidence.md` | **Средняя уверенность** (2031) — из имён файлов и папок |
| `3-high-confidence.md` | **Высокая уверенность** (2447) — точная перекодировка |
**Формат (запрос Alex):** порядок от «требует решения» к «высокая уверенность»,
каждый файл = чекбокс `- [ ] **имя.mp3**` + подпункты `field: старое → новое` + источник.
@@ -40,10 +113,10 @@ CSV отдан не только файлом, но и **читаемыми но
**Генератор:** `render_grouped_doc.py` (Mac, `~/Developer/personal-os/tools/music-tags/`).
**Синк:** `bash ~/scripts/sync-vault.sh` → `Pushed ✓`, коммит `14e9f570`.
Проверено в bare repo: `git ls-tree -r HEAD | grep music-tags` → 4 файла на месте.
**Синк:** `bash ~/scripts/sync-vault.sh` → `Pushed ✓`. Последний коммит после второй волны
залит, клон `obsidian-syncthing` обновлён до `e200596`, Syncthing раздал телефону (`completion: 100`).
> ⚠️ Блоки 2 и 3 — по 730–780 КБ, 5000+ строк. Obsidian открывает, но скролл тяжёлый.
> ⚠️ Блоки 2 и 3 — по 580–790 КБ. Obsidian открывает, но скролл тяжёлый.
> Облегчение — оглавление по папкам в начале каждого файла (клик → прыжок к разделу).
> Если Alex попросит — разбить по категориям на отдельные файлы.
@@ -333,15 +406,19 @@ if cyr < 2: # одна буква — совпадение
| # | Шаг | Статус |
|---|---|---|
| 1 | **Инвентаризация** read-only: `ffprobe` по 7906 файлам → CSV | ✅ **ГОТОВО (v2)** — `tag_inventory2.py`, `probe_failed`=0 |
| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v2)** — `~/Downloads/music-tags/tag-fix-plan.csv` |
| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex 2026-09-26) | ✅ **ГОТОВО** — `personal/plans/music-tags-review/`, синкнуто (`Pushed ✓`, коммит `14e9f570`) |
| 3 | **Апрув Alex по CSV** → только после этого правки | ⏳ **ЖДЁМ** (v1 отклонён, v2 + ноты отданы) |
| 2 | **CSV с предлагаемым исправлением** → Alex | ✅ **ГОТОВО (v3)** — `~/Downloads/music-tags/tag-fix-plan.csv`, 4774 `fix` |
| 2a | **Ноты для ревью в Obsidian** → Alex (запрос Alex) | ✅ **ГОТОВО** — `personal/plans/music-tags-review/`, синкнуто + доехало до телефона |
| 2b | **Полный аудит всех строк** (`audit_plan.py` + `final_gate.py`) → 6 багов исправлено | ✅ **ГОТОВО** (запрос Alex «Сам проверил весь файл?») |
| 3 | **Апрув Alex по нотам ревью** → только после этого правки | ⏳ **ЖДЁМ** (v1 и v2 отклонены, v3 + ноты отданы) |
| 4 | **Бэкап** — копировать **только перезаписываемые** файлы в `Music/backup/` | ⏳ |
| 5 | **Ремонт** — перекодировка mojibake + восстановление из имён путей + чистка мусора | ⏳ |
| 6 | **Приёмка** — Alex слушает выборку | ⏳ |
> 📌 **Что именно ждёт апрува:** 4961 `fix` из 7906. Alex проходит по трём нотам ревью,
> 📌 **Что именно ждёт апрува:** 4774 `fix` из 7906. Alex проходит по трём нотам ревью,
> отмечает чекбоксы. Ничего не применяется до его «go».
>
> 🔴 **Открытый вопрос к Alex:** расписание job `sync-vault` в Taiga — `0 * * * *` (раз в час),
> хотя доки говорят «каждые 5 минут». Менять на `*/5 * * * *` или оставить?
### Ключевые решения по плану (правки Alex)
@@ -379,19 +456,25 @@ if cyr < 2: # одна буква — совпадение
| `id3_direct.py` | **Прямой парсер ID3v2** из байтов файла (минуя `ffprobe`). Поддержка ID3v2.2/2.3/2.4, кодировок 0–3, extended header. Используется как fallback и как отладочный инструмент |
| `analyse_inventory.py` | Анализ CSV инвентаря: доли вердиктов, проверка обратимости mojibake, разбор `lost`/`probe_failed` |
| `build_fix_plan.py` | **Этап 2 (v1, устарел).** Содержал ранний выход по `probe_failed` — из-за него ДДТ отдавались пустыми |
| `build_fix_plan2.py` | **Этап 2 (v2, актуальный).** Ранний выход убран, `prettify_translit` добавлен. Содержит `fix_encoding`, `clean_value`, `repair`, `split_artist_title`, `title_from_name`, `derive_from_path`, `prettify_translit` |
| `build_fix_plan2.py` | **Этап 2 (v3, актуальный).** Пословный `fix_encoding`, `NON_ARTIST`-контейнеры, `is_release_folder`, `clean_folder`, `prettify_translit`, срезание ведущего номера. Функции: `_try_convert`, `has_release_in_path`, `fix_encoding`, `clean_value`, `clean_folder`, `repair`, `split_artist_title`, `title_from_name`, `derive_from_path`, `is_release_folder`, `prettify_translit` |
| `audit_plan.py` | **ПОЛНЫЙ аудит плана по всем строкам** (не сэмплы). Классы: `damaged_output`, `mixed_script_artist`, `translit_kept`, `watermark`, `numeric_title`, `artist_equals_title`, `generic_artist`, `album_equals_artist`, `one_char_title`, `whitespace`, `no_visible_change` |
| `final_gate.py` | **Гейт полноты перед выдачей.** Проверяет: совпадение числа строк с инвентарём, отсутствие заглушек (`probe_failed`/`investigation`), пустых title, U+FFFD, правок без `source`. Печатает PASS или точные строки |
| `deepdive_audit.py` | Углублённый разбор крупнейших классов (`generic_artist`, `translit_kept`) — по папкам и сырым значениям |
| `separate_moji.py` | Отделяет **настоящий mojibake** от **законной латиницы** (`NTL`, `Eagles`) по доле маркеров CP1251 |
| `review_classes.py` | Печать контекста по каждому классу аудита, сгруппировано по папке — чтобы судить целым альбомом |
| `check_syncthing2.sh` | **Диагностика синка**: версия Syncthing, статус папки, connections, completion для телефона. Запрос через `docker exec syncthing curl … :8384` |
| `check_fix_plan.py` | Контроль качества плана: U+FFFD на выходе, пустые title/artist, сэмплы «до/после» по категориям |
| `find_bad_titles.py` | Ищет недоработки: `TrackNN` в title, голые номера, пустой artist |
| `check_ddt_album.py` | Печатает **полный набор сырых тегов** файлов по маске — для проверки «баг скрипта vs так в файле» |
| `diagnose_probe_failures.py` | Анализ заголовков файлов, упавших в `probe_failed`: ID3-версия, `hdr_ok`, размеры |
| `test_encoding_guard.py` | **32 регресс-теста.** Защита от порчи корректного UTF-8 (`Björk`, `Saint-Saëns`, `Noël`) + `split_artist_title` |
| `test_plan2.py` | **Регресс-тесты v2.** Включая отказ сплита на event-именах (`НАШЕСТВИЕ 2009 - ДЕНЬ ВТОРОЙ` остаётся целиком в title) |
| `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 блока по уверенности + обзор, группировка по папкам, чекбоксы, оглавление |
| Артефакт | Где | Что |
|---|---|---|
| CSV-инвентарь v2 | `/mnt/RED_2TB/storage/tmp/tag-inventory2.csv` (NAS) → `~/Downloads/music-tags/tag-inventory.csv` (Mac) | 7906 строк, колонка `source`, `probe_failed`=0 |
| **CSV-план правок v2** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, 2.2 МБ) | **Отдан Alex 2026-09-26.** 4961 `fix` / 2945 `no_change` / 0 `probe_failed` |
| **CSV-план правок v3** | `~/Downloads/music-tags/tag-fix-plan.csv` (Mac, ~2.2 МБ) | **Отдан Alex 2026-09-26 (финал).** 4774 `fix` / 3132 `no_change` / 0 `probe_failed` |
| Копия БД 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**, синкнуто |
@@ -436,6 +519,14 @@ if cyr < 2: # одна буква — совпадение
- 🔴 **`execute_code` требует апрува** — Alex явно сказал: **«Без аппрува команды используй! Скрипты пиши»**. Писать исполняемое в файл (`write_file`) → запускать через `terminal`. Не инлайн.
- 🔴 **Наивная перекодировка CP1251→Latin-1 портит корректный UTF-8** (`Saint-Saëns` → `Saint-Saлns`). Обязателен guard по доле кириллицы (см. выше).
- 🔴 **`ffprobe` молча отдаёт ПУСТЫЕ теги** при битом фрейме внутри (напр. `comment='eng'`) — при `exit=0` и валидном JSON. Нельзя считать «пусто от `ffprobe`» доказательством отсутствия тегов. Нужен fallback-парсер ID3 (`id3_direct.py`).
- 🔴 **Guard по доле кириллицы на ВСЮ строку ломает смешанные значения.** `Èâàíóøêè International` — битая кириллица + настоящая латиница; соотношение на строку не проходит порог, и mojibake **выживает**. Конвертировать **пословно** (`re.split(r"(\s+)")`), плюс второй проход по одиночным буквам (`Василий Ê`) при условии, что соседнее слово конвертировалось.
- 🔴 **Токены с пунктуацией вне Latin-1 (`Ãëþê'OZA`) не конвертируются целиком** — `encode("latin-1")` падает на апострофе. Нужен посимвольный проход с проверкой итоговой доли кириллицы (≥50%).
- 🔴 **Папка-контейнер (`Сказки`, `Разное`, `Различные исполнители`) — не артист.** Прямая подстановка `d_artist` даёт 238 файлов с одним «исполнителем». Контейнерный уровень отбрасывается; под деревом сказок одиночная папка идёт в **album**, artist пустой.
- 🔴 **Папки релизов (`!Radio`, `y OOF`, `v 2009-03 mIr.vEsNЫ`) — не артисты.** Признаки: `!`-префикс, год в имени, `RELEASE_WORDS` (`radio`, `chart`, `mix`, `окtoberfest`…).
- ⚠️ **Фильтр релизов не должен применяться к альбомам.** `Пилот/1999 Жывой Концерррт` содержит год, но это легитимный альбом. Проверять **только слот артиста** (`dirs[-2]`), не все компоненты пути.
- ⚠️ **Значения из одних символов (`©`, `+`, `(17)`) — мусор.** И для тегов (`clean_value`), и для папок (`clean_folder`) — отвергать, если нет ни одной буквы/цифры.
- ⚠️ **Ведущий номер, склеенный с названием (`11 Track 11`, `04 Yves Larock - Rise Up`),** — срезать регекспом и брать имя файла.
- ⚠️ **Проверка «без источника» может ложно срабатывать** на правках, где изменились только пробелы/водяные знаки. Помечать такие явно (`encoding/cleanup only`), а не оставлять пустым `source`.
- 🔴 **Ранний `continue` по «нераспознанному» вердикту = потеря файлов.** Именно так 359 файлов ушли заглушками, а ДДТ — без artist/title. **Неизвестный вход обрабатывать общей логикой, а не пропускать.**
- 🔴 **Нельзя отдавать пользователю вывод, не проверив сэмплы проблемных групп.** CSV v1 ушёл Alex без проверки `probe_failed` и `!Retro` — и был справедливо раскритикован. Проверка = прогон `check_fix_plan.py` + `find_bad_titles.py` + выборочный `check_ddt_album.py`.
- ⚠️ **Правка скриптов только локально → `scp` заново.** `sed -i` на NAS применён быть не должен (была такая ошибка, исправлена).
@@ -445,20 +536,30 @@ if cyr < 2: # одна буква — совпадение
### 🧠 ГЛАВНЫЙ ПРОЦЕССНЫЙ УРОК ЭТОЙ СЕССИИ
Alex: **«Ты бы хоть постеснялся не проверив мне файл сувать!»**
Alex (вторая волна): **«Сам проверил весь файл на адекватность? Чтобы я в очевидные вещи тебя не тыкал.»**
Порядок работы, который себя оправдал **после** исправления:
**Ключевое различие, которое стоило двух итераций: выборочная проверка ≠ полный аудит.**
После первой правки были «пройдены все проверки» — но это были 5 метрик на сэмплах. Полный аудит
по всем 4774 строкам нашёл ещё **6 багов**, крупнейший — 238 файлов с `artist = Сказки`.
Порядок работы, который себя оправдал:
1. Написать скрипт **в файле** (`write_file`) — не инлайн, не `execute_code`.
2. Прогнать **автотесты** на краевых случаях (латиница с умляутами, дефисы, номера).
3. Прогнать **контрольные проверки** на реальном выводе: U+FFFD, пустые поля, оставшийся мусор.
4. **Посмотреть сэмплы «до/после» глазами** по каждой проблемной группе.
5. Проверить **сырые данные конкретного файла**, прежде чем спорить «это баг / не баг».
6. **Только потом** отдавать пользователю.
2. Прогнать **автотесты** на краевых случаях (латиница с умляутами, дефисы, номера, смешанные строки).
3. **Прогнать ПОЛНЫЙ аудит по всем строкам**, а не по сэмплам (`audit_plan.py`).
4. **Прогнать гейт полноты** (`final_gate.py`) — заглушки, пустые поля, правки без источника.
5. Посмотреть сэмплы «до/после» глазами по каждой проблемной группе.
6. Проверить **сырые данные конкретного файла**, прежде чем спорить «это баг / не баг».
7. Отделить **настоящие дефекты от законных случаев** (`separate_moji.py`, `review_classes.py`):
`NTL`/`Eagles` — латиница, `Би-2/2001 Би-2` — одноимённый альбом.
8. **Только потом** отдавать пользователю.
Три бага были пойманы пунктами 3–4 **до** выдачи. Пункт 5 отличает «баг скрипта» от «так в файле» (случай ДДТ).
**Заглушка `needs investigation` в отчёте — это не результат, а моя недоработка.**
**Заглушка `needs investigation` в отчёте — это не результат, а недоработка.**
**«Требует решения» — законный исход, но должен быть явным, а не замаскированным мусором.**
### 🐞 Три бага, пойманные ДО выдачи CSV (файлы не пострадали)
### 🐞 Девять багов, пойманных ДО выдачи (файлы не пострадали)
**Первая волна (3 бага, сэмплами):**
| # | Баг | Как пойман | Фикс |
|---|---|---|---|
@@ -466,9 +567,12 @@ Alex: **«Ты бы хоть постеснялся не проверив мне
| 2 | `Camille Saint-Saëns` → `Camille Saint-Saлns` — перекодировка ломала корректный UTF-8 | `check_fix_plan.py`, сэмплы «partial» | Guard по доле кириллицы в результате (≥50%, ≥2 буквы) |
| 3 | Управляющие байты (`\x1d`) в ID3 «чинились» вместо пометки | Просмотр сэмплов | `CONTROL_RE` → такие значения отвергаются, берётся имя файла |
> 🔑 **Методический вывод:** контроль качества плана (`check_fix_plan.py`) на сэмплах
> «до/после» поймал все три бага. Без этого CSV ушёл бы Alex с испорченными предложениями.
> **Всегда проверять вывод скрипта на сэмплах до передачи пользователю.**
**Вторая волна (6 багов, полным аудитом):** см. таблицу «ВТОРАЯ ВОЛНА ИСПРАВЛЕНИЙ» в начале документа —
`artist = Сказки` (238), mojibake рядом с латиницей (127), имя радиовыпуска как артист (31),
номер склеен с названием (17), `album = '+'` (10), `©` как title (1).
> 🔑 **Методический вывод:** сэмплы ловят грубые ошибки, полный аудит — системные.
> Оба нужны. Без полного аудита CSV v2 ушёл бы Alex с 238 файлами «исполнитель Сказки».
## Связанные заметки