From e7091dd819fadfdcbc4a98336e77000e8448ed66 Mon Sep 17 00:00:00 2001 From: Alexey Martemyanov Date: Sat, 26 Sep 2026 11:14:40 +0600 Subject: [PATCH] [2026-09-26] eagle: personal/projects/balda/balda-mattermost-connector.md personal/projects/balda/balda-mm-delegation-plan.md personal/projects/balda/mattermost-kraken-deploy.md personal/tech/balda-go-fx-pitfalls.md --- .../balda/balda-mattermost-connector.md | 143 ++++++++++++++---- .../balda/balda-mm-delegation-plan.md | 40 ++++- .../balda/mattermost-kraken-deploy.md | 41 +++-- personal/tech/balda-go-fx-pitfalls.md | 114 +++++++++++++- 4 files changed, 286 insertions(+), 52 deletions(-) diff --git a/personal/projects/balda/balda-mattermost-connector.md b/personal/projects/balda/balda-mattermost-connector.md index 36be74af..5d53d569 100644 --- a/personal/projects/balda/balda-mattermost-connector.md +++ b/personal/projects/balda/balda-mattermost-connector.md @@ -1,18 +1,19 @@ # Balda — Mattermost коннектор (архитектура) -> Создано 2026-09-26. Обновлено 2026-09-26 (вечер). Статус: **КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ**. +> Создано 2026-09-26. Обновлено 2026-09-26 (вечер). Статус: **✅ ЗАКРЫТА — ВСЕ QUALITY GATES ПРОЙДЕНЫ**. > Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]] (Фаза 2). > Требование Alex: поддержка **всех** функций Балды, по новым API, production-качество. > -> **Проверено (2026-09-26):** +> **Проверено (2026-09-26, финал):** > - `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** > - **DI-граф собирается целиком** с `balda.mattermost.enabled=true` — проверено реальным > `validateBaldaApplication(prepared)` через fx, а не только компиляцией > - 3 новых теста зелёные (см. §«Тесты») +> - Закоммичено: `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072) > -> **Осталось:** 2 шага — дописать ожидание маршрута Mattermost в `TestBuiltinRoutes` -> (единственная регрессия от наших правок) + `go tool golangci-lint run`. -> Подробности в §«🔴 Текущее состояние и следующий шаг». +> **Осталось:** ничего по коду. Дальше — Фаза 3 (Балда с MM на Kraken) и Фаза 4 (исполнитель). > > Разделы ниже — сначала контракт/разведка, затем **фактически написанные файлы**. Ищи > §«Что фактически написано» для реестра файлов. @@ -217,19 +218,23 @@ MM: `Source` = новый `turncmd.SourceMattermost`, `DeliveryFormat` = `Delive --- -## Что нужно добавить в shared-слой +## Что нужно добавить в shared-слой — ✅ ВСЁ ПРИМЕНЕНО -| Файл | Добавить | -|---|---| -| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | -| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"` | -| `turncmd` (source) | `SourceMattermost` | -| `docs/commands.md` | колонка Mattermost в таблице доступности команд | -| `docs/balda.md` + `README.md` | описание нового транспорта (требование AGENTS.md) | +| Файл | Добавить | Статус | +|---|---|---| +| `deliverycmd/channel.go` | `ChannelTypeMattermost ChannelType = "mattermost"` | ✅ | +| `deliveryfmt/routes.go` | `TransportMattermost = "mattermost"`, `NameMattermostMarkdown Name = "mattermost_markdown"` | ✅ | +| `turncmd` (source) | `SourceMattermost` | ✅ | +| `docs/commands.md` | колонка Mattermost в таблице доступности команд | ✅ | +| `docs/reference/configuration.md` | 5 ключей `balda.mattermost.*` | ✅ | +| `README.md` / `docs/balda.md` | ⚠️ **не обновлялись** — README всё ещё перечисляет 3 chat surface | ⏳ | ⚠️ `channel/router.go` **править не нужно** — он map-based по `locator.ChannelType`. Достаточно зарегистрировать binding в DI (группа `balda_delivery_channel_adapter`). +📌 Остаётся один незакрытый пункт требований `AGENTS.md` (не блокирующий): README и +`docs/balda.md` не упоминают Mattermost. Закрыть при подготовке PR в апстрим либо в Фазе 3. + --- ## Что фактически написано (2026-09-26) @@ -354,20 +359,66 @@ err = validateBaldaApplication(prepared) // ← вот это и есть провайдер `balda_agent: opencode_acp`. Якорь `func TestValidateCommandRunsValidation(t *testing.T) {` использован как точка вставки. -### 🔴 Текущее состояние и следующий шаг +### ✅ Текущее состояние — ЗАКРЫТО (2026-09-26, ночь) -**Осталось 2 шага (~5 минут):** +Оба ранее открытых пункта выполнены: -1. **Дописать ожидание маршрута Mattermost в `TestBuiltinRoutes`.** - `internal/apps/balda/deliveryfmt/registry_test.go`, функция `TestBuiltinRoutes`. - В map `want` добавить: - ```go - {transport: TransportMattermost, deliveryFormat: DeliveryFormatNone}: NamePlainText, - ``` - Симптом без правки: `len(BuiltinRoutes()) = 5, want 4`. - Причина: в `routes.go` мы добавили маршрут Mattermost (строка 32), а тест-ожидание не обновили. - **Это единственная регрессия, внесённая нашими правками.** -2. `go tool golangci-lint run` — ещё не запускался. +1. ✅ **`TestBuiltinRoutes`** — в map `want` добавлен маршрут Mattermost + (`internal/apps/balda/deliveryfmt/registry_test.go`). Пакет `deliveryfmt` зелёный. +2. ✅ **`golangci-lint`** — запущен, 4 замечания → исправлены (см. §«Что дал golangci-lint» ниже). + +Дополнительно в этом же заходе: +- ✅ **`.go-arch-lint.yml` обновлён** — компоненты `mattermost_transport` / `mattermost_wiring`, + права зависимости у `composition_root` и `ingress_wiring`. Требование `AGENTS.md`. +- ✅ **Удалён мёртвый typing-throttle** целиком (не заглушен). +- ✅ Закоммичено: `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072). + +**Ничего не осталось по коду.** Дальше — Фаза 3 (развернуть Балду с MM на Kraken, +провайдер DeepSeek) → [[personal/projects/balda/balda-mm-delegation-plan]]. + +### Что дал `golangci-lint` (4 замечания, все закрыты) + +| Замечание | Файл | Фикс | +|---|---|---| +| `inamedparam` ×2 | `ingress.go:452-453` (interface assertion) | `Start(ctx context.Context) error` / `Stop(ctx context.Context) error` | +| `unused` | `adapter.go:149` `shouldSendTyping` | удалён вместе со всем throttle-механизмом | +| `unused` | `ingress.go:21` `websocketReceiveTimeout` | константа удалена | +| `unlambda` | `mattermostfx/module.go:51` обёртка `func(client, logger) *Adapter` | заменена на голое `mattermost.NewAdapter` | + +**Корневая причина:** typing-throttle (`shouldSendTyping`, `typingThrottleInterval`, +`typingLastSentAt`, `typingMu`, `SetTypingThrottleInterval`) был скопирован из Zulip +по аналогии, но у MM-ботов **нет typing API** — `SendTyping` всегда `return nil`, значит +весь механизм недостижим. Удалён **целиком группой** (каскад: поле → setter → `sync` → +`zerolog` в `module.go`), а не заглушен `nolint`. + +📌 **Урок для будущих транспортов:** при копировании Zulip-адаптера проверять каждую фичу +на применимость к провайдеру. Перенос «по аналогии» создаёт мёртвый код, который линтер +находит только на финальном прогоне. Детали — [[personal/tech/balda-go-fx-pitfalls]]. + +### `.go-arch-lint.yml` — что добавлено + +Требование `AGENTS.md`: архитектурное изменение обязано обновлять arch-lint в том же коммите. +Без этого `go tool go-arch-lint check` падает двумя классами ошибок +(`composition_root shouldn't depend on …/mattermost`, `not attached to any component in archfile`). + +4 правки по образцу `zulip`: +- `components:` → `mattermost_transport` (`channel/mattermost` + `presentation/`) и + `mattermost_wiring` (`mattermostfx/`) +- `mayDependOn` для обоих новых компонентов +- `composition_root` → `+ mattermost_transport, mattermost_wiring` +- `ingress_wiring` → `+ mattermost_transport` + +Полные YAML-фрагменты — [[personal/tech/balda-go-fx-pitfalls]] §«go-arch-lint». + +### ✅ Финальные quality gates — все зелёные + +``` +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 +тесты Mattermost → 3 PASS +``` ### ✅ Предсуществующие падения тестов — НЕ наши @@ -398,14 +449,36 @@ err = validateBaldaApplication(prepared) // ← вот это и есть 6. **`commandSupport` переименован** в экспортируемый `IngressCommandSupport` (нужен для `fx.In`), оставлен алиас. -### 🔴 Известный незакрытый шаг +### ✅ Импорт mattermostfx — ЗАКРЫТО -`internal/apps/balda/app.go` — модуль уже в списке, но **импорт не добавлен**: +`internal/apps/balda/app.go` — импорты добавлены (строки 27-28, рядом с `zulipfx`): ```go +"github.com/baldaworks/balda/internal/apps/balda/channel/mattermost" "github.com/baldaworks/balda/internal/apps/balda/channel/mattermost/mattermostfx" ``` -Вставить рядом с `zulipfx` (строки 27). LSP: `undefined: mattermostfx` на строке 667. -**Без этого `go build ./...` не пройдёт** — проверять первым делом. +Корневой `mattermost` нужен для `mattermost.ValidateConfig` в `validateMattermostConfig`. +`mattermostfx.Module` в списке модулей (строка ~667). `go build ./...` = exit 0. + +### 🔴 Что понадобилось дополнительно (не было в исходном коде) + +Первоначально модуль компилировался, но **DI-граф не собирался**. Три дефекта: + +1. **`fx.In` нельзя аннотировать внешним `fx.ParamTags`.** `IngressParams` содержит `fx.In`, + а в `module.go` стоял `fx.Annotate(NewIngress, fx.ParamTags(...))` → fx падает с + `fx.In structs cannot be annotated with fx.ParamTags or fx.From`. + **Фикс:** `name:`-теги перенесены **в поля `IngressParams`**, `NewIngress` без `fx.Annotate`. + Так же сделано в `zulipfx` (`zulip.NewServer` идёт голой ссылкой, теги в `ServerParams`). +2. **Не хватало провайдеров registered strings.** Модуль запрашивает + `balda_mattermost_{server_url,token,bot_user_id,bot_username}` — их никто не предоставлял. + **Фикс:** 4 `fx.Provide` в `app.go` (блок `// Mattermost transport`), значения берутся из + `cfg.Balda.Mattermost.*` через `strings.TrimSpace`. Для несуществующих ключей + `GetString` возвращает `""` — тот же паттерн, что у `balda_zulip_*`. +3. **Валидация стояла не там.** Функцию `validateMattermostConfig` добавили в `app.go` + (рядом с `validateSlackConfig`), вызов — после `validateSlackConfig` в `Module()`. + +**Метод, который это поймал:** `go build`/`go vet` этих ошибок **не видят** — только реальный +`fx.ValidateApp` через `validateBaldaApplication`. Для любого нового транспорта писать такой +тест сразу: он ловит неполный wiring, а не только синтаксис. ### Конфиг, который нужен для запуска @@ -445,8 +518,20 @@ balda_mattermost_bot_username | `tools/deltas-mattermost-4.json` | тест fx-графа | | `tools/deltas-mattermost-5.json` | фикс `fx.In` + `module.go` | | `tools/deltas-mattermost-6.json` | 4 провайдера строк | +| `tools/deltas-mattermost-7.json` | маршрут Mattermost в `TestBuiltinRoutes` | +| `tools/deltas-mattermost-8.json` | `.go-arch-lint.yml`: `mattermost_transport`/`mattermost_wiring` | +| `tools/deltas-mattermost-9.json` | `inamedparam` + удаление `shouldSendTyping`/полей/`websocketReceiveTimeout` | +| `tools/deltas-mattermost-10.json` | удаление `SetTypingThrottleInterval` + `typingMu` + вызов/импорт `time` в `module.go` | +| `tools/deltas-mattermost-11.json` | удаление неиспользуемого импорта `sync` | +| `tools/deltas-mattermost-12.json` | `unlambda`: голое `mattermost.NewAdapter` | +| `tools/deltas-mattermost-13.json` | удаление неиспользуемого импорта `zerolog` из `module.go` | | `tools/.deltas-state.json` | состояние (sha256 применённых якорей) | +📌 **Дельты 9-13 — это один каскад линтера.** Удаление throttle-механизма тянуло +`SetTypingThrottleInterval` → `typingMu` → `sync` → `zerolog`. Пришлось делать 5 заходов, +каждый раз пересобирая. **Урок: удалять мёртвую фичу одной группой дельт**, а не по одному +симптому — тогда каскад импортов закрывается за раз. + **Формат дельты:** ```json { "file": "путь/относительно/корня", "note": "что делаем", diff --git a/personal/projects/balda/balda-mm-delegation-plan.md b/personal/projects/balda/balda-mm-delegation-plan.md index d44333a7..8a993ac1 100644 --- a/personal/projects/balda/balda-mm-delegation-plan.md +++ b/personal/projects/balda/balda-mm-delegation-plan.md @@ -201,7 +201,7 @@ Mattermost (Alex) — итог + артефакты туннель `{"status":"OK"}`. ⏳ Осталась приёмка входом в браузере (делает Alex). → [[personal/projects/balda/mattermost-kraken-deploy]] -### Фаза 2 — адаптер Mattermost в Балде — ✅ КОД СОБИРАЕТСЯ, DI-ГРАФ ВАЛИДИРУЕТСЯ +### Фаза 2 — адаптер Mattermost в Балде — ✅ ЗАКРЫТА (все quality gates пройдены) Детальный док: [[personal/projects/balda/balda-mattermost-connector]]. 1. ✅ Ветка **`feat/mattermost-channel`** от чистого `upstream/main` (`de273d8f`). @@ -219,9 +219,28 @@ Mattermost (Alex) — итог + артефакты 8. ✅ **DI-граф проверен реальным `validateBaldaApplication`** с `mattermost.enabled=true` — найдены и починены 3 дефекта (`fx.In` + `fx.ParamTags`, отсутствующие провайдеры строк, отсутствующий импорт). -9. ✅ 2 новых теста зелёные (env-override + fx-граф). -10. ⏳ **Осталось:** дописать ожидание маршрута Mattermost в `TestBuiltinRoutes` - (`deliveryfmt/registry_test.go`) + `go tool golangci-lint run`. +9. ✅ 3 новых теста зелёные (env-override + fx-граф + маршрут `TestBuiltinRoutes`). +10. ✅ **`golangci-lint` = 0 issues, `go-arch-lint` = OK**, `go build`/`go vet` = exit 0. +11. ✅ **Закоммичено:** `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072). + +**Фаза 2 закрыта на уровне кода.** Все quality gates из `AGENTS.md` пройдены. +Дальше — Фаза 3 (развернуть Балду с MM-адаптером на Kraken) и Фаза 4 (исполнитель в Docker на Mac). + +#### Дефекты, найденные на этапе закрытия (важно для будущих транспортов) + +| # | Дефект | Симптом | Фикс | +|---|---|---|---| +| 1 | `fx.In` внутри `IngressParams` + внешний `fx.ParamTags` | `fx.In structs cannot be annotated with fx.ParamTags` | теги `name:` переехали **в поля структуры**; `NewIngress` без `fx.Annotate` | +| 2 | Нет провайдеров `balda_mattermost_server_url/token/bot_user_id/bot_username` | `missing types: string[name=...]` | 4 `fx.Provide` c `fx.ResultTags` в `app.go` | +| 3 | Валидация стояла в `config.go`, где нет такой функции | якорь не найден | `validateMattermostConfig` в `app.go` рядом с `validateSlackConfig` + вызов после неё | +| 4 | `TestBuiltinRoutes` ждал 4 маршрута | `len(BuiltinRoutes()) = 5, want 4` | добавить маршрут Mattermost в `want` | +| 5 | Мёртвый typing-throttle (`shouldSendTyping`, поля, mutex, `SetTypingThrottleInterval`) | `unused` ×2, затем каскад `undefined` | удалён целиком — `SendTyping` всегда no-op | +| 6 | Обёртка `func(client, logger) *Adapter` без тела | `unlambda` (gocritic) | заменена на `mattermost.NewAdapter` | + +⚠️ **Метод проверки, который окупился:** `go build` ловит только компиляцию. **Реальный DI-граф +валидируется только `validateBaldaApplication` через fx** — именно этот тест нашёл дефекты 1-2, +которых `build`/`vet` не видят. Для любого нового транспорта писать такой тест сразу. + 🔴 **Урок Фазы 2:** `go build` = exit 0 **не значит** «транспорт рабочий». fx-несовместимости и отсутствующие провайдеры видны только при `fx.ValidateApp`/`validateBaldaApplication`. @@ -327,8 +346,17 @@ Mattermost (Alex) — итог + артефакты `fx.ParamTags`, отсутствующие провайдеры зарегистрированных строк, отсутствующий импорт. Проверка графа — реальный `validateBaldaApplication`, не компиляция. - Внесена 1 регрессия: `TestBuiltinRoutes` ждёт 4 маршрута, в `BuiltinRoutes()` их 5. - Фикс известен (дописать строку в map `want`), не применён — лимит шагов сессии. - - ⚠️ 13 падающих пакетов — **предсуществующий дефект апстрима**, подтверждено `git stash`. + ⚠️ 13 падающих пакетов — **предсуществующий дефект апстрима**, подтверждено `git stash`. +- **2026-09-26 (ночь)**: **Фазы 1 и 2 ЗАКРЫТЫ.** + - `TestBuiltinRoutes` починен (маршрут Mattermost добавлен в `want`). + - `golangci-lint` нашёл 4 замечания → исправлены: мёртвый typing-throttle удалён целиком + (`shouldSendTyping`, поля, mutex, `SetTypingThrottleInterval`) — `SendTyping` у MM всегда no-op; + `inamedparam` в assertion; неиспользуемые импорты; `unlambda` в `module.go`. + - `.go-arch-lint.yml` обновлён (по требованию AGENTS.md): компоненты `mattermost_transport` + и `mattermost_wiring`, право зависимости у `composition_root` и `ingress_wiring`. + - **Все quality gates зелёные:** `build` 0 · `vet` 0 · `golangci-lint` **0 issues** · + `go-arch-lint` **OK** · 3 новых теста PASS. + - **Закоммичено:** `004fda2f feat(mattermost): add Mattermost transport adapter` (40 файлов, +3072). ## Связанные заметки - [[personal/projects/balda/mattermost-kraken-deploy]] — Фаза 1: MM на Kraken (arm64-блокер, деплой) diff --git a/personal/projects/balda/mattermost-kraken-deploy.md b/personal/projects/balda/mattermost-kraken-deploy.md index b48af0d2..6e732387 100644 --- a/personal/projects/balda/mattermost-kraken-deploy.md +++ b/personal/projects/balda/mattermost-kraken-deploy.md @@ -5,8 +5,9 @@ > админ создан, **регистрация закрыта** (проверено живым запросом → HTTP 501). > Часть проекта [[personal/projects/balda/balda-mm-delegation-plan]]. > -> **Осталось:** Alex подтвердил, что `mm.qentra.top` работает (2026-09-26). -> **Сделано:** `SITEURL` → `https://mm.qentra.top`, MM перезапущен, проверено через туннель. +> **Сделано (2026-09-26, позже):** `SITEURL` → `https://mm.qentra.top`, MM перезапущен → `health=healthy restarts=0`. +> Ping через туннель с Mac: `{"status":"OK"}`. `mmctl config get ServiceSettings.SiteURL` → `https://mm.qentra.top`. +> > **Осталось:** приёмка входом в браузере (делает Alex). --- @@ -146,7 +147,7 @@ PGDBNAME=mattermost | postgres | `postgres:16-alpine` | arm64 есть | | `MM_SQLSETTINGS_DRIVERNAME` | `postgres` | | | `MM_SQLSETTINGS_DATASOURCE` | `postgres://mmuser:***@postgres:5432/mattermost?sslmode=disable&connect_timeout=10` | пароль подставляется **в момент генерации compose** из `.env` | -| `MM_SERVICESETTINGS_SITEURL` | `http://192.168.1.14:8065` (LAN, временно) | → поменять на `https://mm.qentra.top` после настройки туннеля | +| `MM_SERVICESETTINGS_SITEURL` | ✅ **`https://mm.qentra.top`** (обновлено 2026-09-26; было `http://192.168.1.14:8065`) | MM отдаёт ссылки и WS-URL по этому хосту; LAN-значение ломало бы WebSocket через туннель | | `MM_SERVICESETTINGS_ENABLELOCALMODE` | `true` | нужен для healthcheck `mmctl --local` | | `MM_TEAMSETTINGS_ENABLEOPENSERVER` | `"false"` | 🔴 **закрытая регистрация** | | `MM_TEAMSETTINGS_ENABLEUSERCREATION` | `"false"` | 🔴 **закрытая регистрация** | @@ -325,23 +326,31 @@ compose — только скриптом с бэкапом. ⚠️ При пересборке compose из генератора SITEURL перезапишется на LAN-значение. Долгосрочно — менять значение в генераторе, а не в файле. -### Команды для следующего шага +⚠️ **Приёмка = вход в `https://mm.qentra.top` в браузере**, не «curl отвечает». +Приёмка `SITEURL` и техническая проверка через туннель — сделаны (см. таблицу выше). + +**Осталось одно:** Alex открывает `https://mm.qentra.top` в браузере и логинится `admin`. + +**Пароль админа получен 2026-09-26** и передан Alex'ом в чат (значение в доке не хранится — +только путь к файлу). Если понадобится снова: ```bash -# 1. Поменять SITEURL на внешний (после подтверждения, что mm.qentra.top открывается) -ssh kraken 'cd /home/kraken/mattermost && sed -i \ - "s|MM_SERVICESETTINGS_SITEURL: .*|MM_SERVICESETTINGS_SITEURL: \"https://mm.qentra.top\"|" docker-compose.yml' -ssh kraken 'cd /home/kraken/mattermost && docker compose up -d mattermost' - -# 2. Проверить, что MM отвечает через туннель (с Mac) -curl -s https://mm.qentra.top/api/v4/system/ping - -# 3. Забрать пароль админа +# Забрать пароль админа ssh kraken 'sudo cat /srv/dev-disk-by-uuid-49e8f586-3839-4c5d-a1e1-58bfc3579ade/docker/mattermost/config/.adminpw' +# Вывод вида: PW=<28 символов> ← в контейнере файл лежит без префикса ``` -⚠️ **Приёмка = вход в `https://mm.qentra.top` в браузере**, не «curl отвечает». -Иначе: `sed -i` в п.1 — временная мера; при пересборке compose из генератора SITEURL -перезапишется. Долгосрочно — поменять значение в генераторе скрипта. + +⚠️ Файл принадлежит uid **2000** и лежит в `config/` — читать только через `sudo -n`, +и только при наличии `< /dev/null` в ssh-команде (иначе `Connection closed by remote host`). + +### Проверка, что всё живо (одна команда) + +```bash +ssh kraken "docker inspect mattermost --format '{{.State.Health.Status}} restarts={{.RestartCount}}' && curl -s http://localhost:8065/api/v4/system/ping" < /dev/null +curl -s -m 20 https://mm.qentra.top/api/v4/system/ping # с Mac, через туннель +``` +Ожидаемо: `healthy restarts=0` + `{"status":"OK",...}` в обоих случаях. + ## Связанные заметки diff --git a/personal/tech/balda-go-fx-pitfalls.md b/personal/tech/balda-go-fx-pitfalls.md index ead0c97e..0d5f38d6 100644 --- a/personal/tech/balda-go-fx-pitfalls.md +++ b/personal/tech/balda-go-fx-pitfalls.md @@ -1,6 +1,7 @@ # Balda — Go/fx pitfalls и метод правок (зеркало скилла) -> Создано 2026-09-26. Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26. +> Создано 2026-09-26. Обновлено 2026-09-26 (ночь) — добавлены lint/arch-lint и закрытие Фазы 2. +> Зеркало секции скилла `devops/docker-balda-service`, добавленной 2026-09-26. > Источник: работа над Mattermost-адаптером Балды (Фаза 2). > Проектные доки: [[personal/projects/balda/balda-mattermost-connector]], > [[personal/projects/balda/balda-mm-delegation-plan]]. @@ -121,6 +122,117 @@ fx.Provide( 4. `validateConfig` + вызов рядом с `validateSlackConfig` 5. включить в `validateBaldaChannelConfiguration` (`cmd/balda/command.go`) — иначе деплой только на этом транспорте отбивается «at least one channel is required» +6. `.go-arch-lint.yml` — компоненты `_transport`/`_wiring` + права у + `composition_root` и `ingress_wiring` (см. §go-arch-lint выше) +7. тест на `validateBaldaApplication` с `.enabled=true` (см. §«go build ≠ …» выше) +8. тест env-override в `cmd/balda/command_test.go` (по образцу + `TestLoadConfigDocument_AppliesZulipEnvOverrides`) +9. если добавлял маршрут в `BuiltinRoutes()` — обновить `TestBuiltinRoutes` +10. доки: `docs/reference/configuration.md` (ключи), `docs/commands.md` (колонка транспорта) + +**Финальные quality gates (все обязательны, из `AGENTS.md`):** +```bash +go build ./... # 0 +go vet ./... # 0 +go tool golangci-lint run # 0 issues +go tool go-arch-lint check --project-path . # OK +go test -race ./... +``` +⚠️ `go-arch-lint` **не входит** в `golangci-lint` — запускать отдельно, иначе ошибка +архитектуры всплывёт только на CI/ревью. + +--- + +## 🔴 `go-arch-lint` — обязательный шаг для нового транспорта + +⚠️ **Это не «опциональная придирка»: `AGENTS.md` прямо требует обновлять +`.go-arch-lint.yml` в том же изменении, что и архитектуру.** Без правки линтер падает +с двумя классами ошибок: + +``` +Component composition_root shouldn't depend on .../channel/mattermost +File /internal/apps/balda/channel/mattermost/adapter.go not attached to any component in archfile +``` + +**Что добавить в `.go-arch-lint.yml` (4 правки) — по образцу `zulip`:** + +1. **Секция `components:`** — пары `in:`-компонентов: +```yaml + # Provider-owned Mattermost transport. It may depend only on + # transport-neutral contracts; application/runtime adapters live in wiring. + mattermost_transport: + in: + - internal/apps/balda/channel/mattermost + - internal/apps/balda/channel/mattermost/presentation + + # Composition-root adapters for Mattermost delivery, prompt, and structured renderers. + mattermost_wiring: + in: + - internal/apps/balda/channel/mattermost/mattermostfx +``` +⚠️ Root-пакет и `presentation/` идут в **один** компонент `_transport`, а `_fx`-пакет — +отдельно в `_wiring`. Именно так у `zulip`/`telegram`/`slackagent`. + +2. **Правила `mayDependOn`:** + - `mattermost_transport` → `foundation, contracts, neutral_registry, mattermost_transport` + - `mattermost_wiring` → `foundation, contracts, neutral_registry, application, delivery, + storage, mattermost_transport, mattermost_wiring` + +3. **`composition_root`** → добавить `mattermost_transport`, `mattermost_wiring` + (иначе «composition_root shouldn't depend on …» — это ожидаемый отказ, там + перечислены все транспорты). + +4. **`ingress_wiring`** → добавить `mattermost_transport` (соседствует с `telegram_transport`, + `zulip_transport`). + +Проверка: `go tool go-arch-lint check --project-path .` → должно быть `OK - No warnings found`. + +--- + +## 🔴 `golangci-lint` на новом транспорте: мёртвый typing-throttle + +Первый прогон дал 4 замечания — все в новом коде. Важный урок: **не гейтить линтер +nolint-комментарием, а удалять мёртвый код.** + +| Замечание | Что было | Фикс | +|---|---|---| +| `inamedparam` ×2 | `Start(context.Context) error` в interface-assertion | `Start(ctx context.Context) error` | +| `unused` | `shouldSendTyping()` — недостижим, т.к. `SendTyping` всегда no-op | удалён | +| `unused` | `websocketReceiveTimeout` — константа без использования | удалена | +| `unlambda` | `func(client, logger) *Adapter { return NewAdapter(...) }` | заменено на `mattermost.NewAdapter` | + +**Корневая причина `unused`:** механизм typing-throttle (`shouldSendTyping`, поля +`typingThrottleInterval`/`typingLastSentAt`, `typingMu sync.Mutex`, +`SetTypingThrottleInterval`) был скопирован из Zulip по аналогии. Но у Mattermost +**ботов нет typing API** (`SendTyping` осознанно возвращает `nil`), поэтому весь механизм +недостижим. Удалён целиком — не заглушён. + +**Правило: при копировании транспорта с Zulip проверять каждую фичу на применимость.** +Zulip-специфичное, что НЕ переносится в транспорт без поддержки: +- typing-throttle (нужен интерактивный typing API) +- логика `sendWithPlainFallback` — переносится, но только на content-rejection + +⚠️ **Каскад после удаления полей:** удаление `typingThrottleInterval` потянуло `SetTypingThrottleInterval` +→ `sync` (импорт) → `zerolog` (импорт в `module.go`). Удалять надо **всей группой**, +иначе серия `undefined`/`imported and not used`. + +--- + +## ✅ Тест-регрессия: `BuiltinRoutes` и длина списка + +Добавление маршрута в `BuiltinRoutes()` (`deliveryfmt/routes.go`) **ломает** `TestBuiltinRoutes` +в `deliveryfmt/registry_test.go`, если не обновить ожидание: + +``` +len(BuiltinRoutes()) = 5, want 4 +``` + +Фикс — добавить строку в map `want`: +```go +{transport: TransportMattermost, deliveryFormat: DeliveryFormatNone}: NamePlainText, +``` +📌 Нашлось только сравнением «с правками / без правок» через `git stash` — в списке +предсуществующих падений апстрима этого теста **не было**, значит это наша регрессия. ---