From 036e7a7091b246c1afc8895698090729cc7b8ba4 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sat, 26 Sep 2026 14:42:05 +0600 Subject: [PATCH] [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 --- family/how-to/vault-git-sync.md | 31 +++- personal/plans/balda-mm-pr-plan.md | 80 ++++++++++- personal/tech/balda-go-fx-pitfalls.md | 31 ++++ personal/tech/music-tags-repair-truenas.md | 160 +++++++++++++++++---- 4 files changed, 272 insertions(+), 30 deletions(-) diff --git a/family/how-to/vault-git-sync.md b/family/how-to/vault-git-sync.md index 03240483..0aec40dd 100644 --- a/family/how-to/vault-git-sync.md +++ b/family/how-to/vault-git-sync.md @@ -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 "[^<]+" /var/syncthing/config/config.xml | sed "s|||"); \ +> 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 мин) diff --git a/personal/plans/balda-mm-pr-plan.md b/personal/plans/balda-mm-pr-plan.md index 4eb0d168..71b9db0c 100644 --- a/personal/plans/balda-mm-pr-plan.md +++ b/personal/plans/balda-mm-pr-plan.md @@ -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. diff --git a/personal/tech/balda-go-fx-pitfalls.md b/personal/tech/balda-go-fx-pitfalls.md index d4b386a8..ccdfe0a9 100644 --- a/personal/tech/balda-go-fx-pitfalls.md +++ b/personal/tech/balda-go-fx-pitfalls.md @@ -1192,6 +1192,37 @@ git log --oneline --name-only ^..HEAD -- ' Альтернатива без «удаления»: оставить файлы 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, где всё это найдено diff --git a/personal/tech/music-tags-repair-truenas.md b/personal/tech/music-tags-repair-truenas.md index 111b9cb9..0eadb858 100644 --- a/personal/tech/music-tags-repair-truenas.md +++ b/personal/tech/music-tags-repair-truenas.md @@ -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 файлами «исполнитель Сказки». ## Связанные заметки