[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

This commit is contained in:
Alexey Martemyanov committed 2026-09-26 11:14:40 +06:00
1 parent 06950241dd
commit e7091dd819
4 files changed
+286 -52

No files matched your search

@@ -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": "что делаем",
@@ -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-блокер, деплой)
@@ -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",...}` в обоих случаях.
## Связанные заметки
+113 -1
View File
@@ -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. `validate<X>Config` + вызов рядом с `validateSlackConfig`
5. включить в `validateBaldaChannelConfiguration` (`cmd/balda/command.go`) —
иначе деплой только на этом транспорте отбивается «at least one channel is required»
6. `.go-arch-lint.yml` — компоненты `<x>_transport`/`<x>_wiring` + права у
`composition_root` и `ingress_wiring` (см. §go-arch-lint выше)
7. тест на `validateBaldaApplication` с `<x>.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` — в списке
предсуществующих падений апстрима этого теста **не было**, значит это наша регрессия.
---