# Сервисы: требования к проектированию

Цель: все сервисы `kodex` устроены единообразно, с явными доменными границами, интерфейсной интеграцией и наблюдаемостью по умолчанию.

## Сервис: ответственность, имя и размещение

Имена:
- `kebab-case`.
- Имя отражает домен/роль, а не технологию.

Размещение:
- целевой путь `services/<zone>/<service-name>/`, где `<zone>` ∈ `internal|external|staff|jobs|dev`.

## Состав сервисов

Состав сервисов определяется актуальной архитектурной документацией проекта.
Устаревшие сервисы и архивные каталоги не используются как база новой реализации.

## Выбор протокола

### gRPC (внутренний sync)
- Контракты в `proto/`, совместимость обязательна.
- Используется для service-to-service взаимодействия внутри платформы.

### HTTP/REST (внешний и staff API)
- HTTP API для webhook, пользовательского интерфейса и внешних интеграций держится только на gateway-слое.
- Внутренние доменные сервисы не публикуют бизнес-ручки наружу напрямую; для них допустимы только служебные `/health/*` и `/metrics` без отдельного OpenAPI.
- OpenAPI YAML обязателен для `external|staff` gateway-сервисов и делится по осмысленным поверхностям, а не одним большим файлом на всю платформу.

### WebSocket (опционально)
- Только где есть реальный realtime use-case в staff UI.
- Контракт сообщений фиксируется в AsyncAPI.

## Внутренняя структура Go-сервиса

Внутри `services/<zone>/<service-name>/`:

- `cmd/<service-name>/main.go` — thin entrypoint.
- `internal/app/` — composition root + lifecycle + graceful shutdown.
- `internal/transport/{http,grpc,ws}/` — handlers и middleware, без доменной логики.
- `internal/transport/{http,grpc,ws}/models/` — typed DTO контракты конкретного транспорта.
- `internal/transport/{http,grpc,ws}/casters/` — явный маппинг transport DTO <-> domain/proto.
- `internal/domain/` — бизнес-правила, модели, use-cases, порты.
  - `internal/domain/service/` — доменная бизнес-логика (use-cases).
  - `internal/domain/errs/` — доменные typed errors (если нужны).
  - `internal/domain/casters/` — маппинг persistence <-> domain (без transport/pgx зависимостей).
  - `internal/domain/helpers/` — локальные доменные helpers (валидация, нормализация, конвертеры).
  - `internal/domain/types/` — доменные типы, разнесённые по категориям:
    - `internal/domain/types/entity/*.go` — сущности;
    - `internal/domain/types/value/*.go` — value objects;
    - `internal/domain/types/enum/*.go` — enum-подобные типы;
    - `internal/domain/types/query/*.go` — фильтры/параметры use-case;
    - `internal/domain/types/mixin/*.go` — общие встраиваемые фрагменты (paging/time-range).
- `internal/domain/repository/<model>/repository.go` — интерфейсы репозиториев.
- `internal/repository/postgres/<model>/repository.go` — реализации репозиториев.
- `internal/repository/postgres/<model>/sql/*.sql` — SQL (через `//go:embed`).
  - каждый named query хранится в отдельном `.sql` файле;
  - запросы именуются комментариями в стиле
    `-- name: <model>__<operation> :one|:many|:exec`
    для стабильной привязки SQL к коду;
- `internal/clients/kubernetes/` — адаптеры Kubernetes SDK.
- `internal/clients/repository/` — адаптеры provider-интерфейсов (`github`, позже `gitlab`).
- `internal/observability/` — подключение логов/трейсов/метрик.
- `cmd/cli/migrations/*.sql` — миграции БД (goose) для этого сервиса, если он держатель схемы.
  - В монорепо это означает путь:
    `services/<zone>/<service-name>/cmd/cli/migrations/*.sql`.
- `specs/openapi/<gateway-surface>.v<major>.yaml` — OpenAPI только для gateway-сервисов.
- `specs/asyncapi/<service-name>.v<major>.yaml` — async/webhook/event контракты (если используются).
- `internal/transport/*/generated/**` — только сгенерированный код.

## Конфигурация процесса

- Runtime config читается через `github.com/caarlos0/env/v11`.
- У сервиса должна быть явная `Config`-структура с тегами `env` и `envDefault`/`required`, если значение обязательно.
- Простые обязательные env задавать встроенными тегами `required` и `notEmpty`; ручная `Validate()` остаётся только для условных правил, cross-field инвариантов, диапазонов чисел и соотношений длительностей.
- На env-поля, обязательность которых зависит от других полей конфигурации, не ставить `required`/`notEmpty`: `caarlos0/env` проверяет эти теги до ручной `Validate()`, поэтому условная валидация должна оставаться в `Validate()`.
- Ручные helper-функции поверх `os.Getenv` для обычного чтения env не добавлять.
- Ошибку парсинга конфигурации возвращать из `LoadConfig` и логировать только в entrypoint/composition root.
- Для уже развёрнутого сервиса новые обязательные env/secrets/config вводить staged-путём:
  сначала inventory/манифесты/секреты и безопасная диагностика, затем использование в коде, затем
  удаление старого пути отдельным срезом. Нельзя добавлять `required`/`notEmpty` так, чтобы live
  deployment перестал стартовать без явного runbook шага.

## PostgreSQL repository

- Реализация repository, transport-сервер и другие production-типы, которые обязаны удовлетворять интерфейсу, должны иметь compile-time assertion вида `var _ target.Interface = (*Type)(nil)`.
- Для чтения `pgx.Rows` по умолчанию использовать помощники `pgx`: `CollectRows`, `RowTo`, `RowToStructByPos` или `RowToStructByName`.
- Ручной сканер допустим, когда строка требует явной доменной конвертации: nullable-значения `pgtype`, enum-значения, JSON payload в типизированную struct-модель, вычисляемые поля или нестандартный порядок сборки aggregate.
- SQL-параметры с очевидным типом из контекста колонки не надо перегружать кастами. Явные PostgreSQL-касты обязательны для nullable-параметров, массивов, JSON/JSONB, `LIMIT/OFFSET` и других выражений, где планировщик или pgx не видит однозначный тип.
- `optional` в proto/request означает наличие или отсутствие значения на транспортной границе, но не означает автоматический SQL `NULL`.
- В БД `NULL` использовать только когда отсутствие значения бизнесово отличается от пустой строки или нуля: внешние ссылки, необязательные временные метки, необязательные идентификаторы провайдера, необязательные ключи идемпотентности.
- Текстовые поля для безопасного отображения, описаний, ссылок на изображения, provider ref и scope ref хранить как `NOT NULL DEFAULT ''`, если пустое значение означает “не задано”.
- Миграции для live-кластера пишутся как новая forward-only история. Уже применённые migration-файлы не
  редактируются; destructive/drop/rename проходят через `expand -> migrate -> contract`.

## Требования к transport DTO и кастерам

- Контракт ответа transport-ручек должен быть строго типизирован.
- Для HTTP/gRPC handlers запрещено возвращать `map[string]any`, `[]any`, `any`.
- Маппинг между transport DTO, proto и доменными моделями должен быть вынесен в отдельные `casters`.
- Handler’ы не содержат доменных преобразований “вручную” и не агрегируют произвольные структуры ad-hoc.
- Для JSON payload в БД/событиях/очередях (например `run_payload`, `flow_events.payload`) использовать typed structs и кастеры; `map[string]any` допускается только как временная локальная отладка и не коммитится.

## Сигнатуры функций и параметры

- Сигнатуры функций и методов писать в одну строку, если это технически возможно без потери читаемости.
- Если аргументов много, вводить входную `Params/Config/Input` структуру вместо длинного списка аргументов.
- Для конструкторов сервисов использовать конфиг-структуры (`Config`) вместо позиционных аргументов.

## Строковые доменные значения

- Повторяющиеся доменные строковые значения выносить в константы.
- Для закрытых наборов строк использовать typed aliases (`type ... string`) и константы поверх них.
- В production-коде запрещено размножать одинаковые литералы статусов, типов событий, типов агрегатов, scope/action ключей и других доменных идентификаторов.
- Типы событий, типы агрегатов и payload-модели доменных событий брать из `libs/go/platformevents/**`, сгенерированных из AsyncAPI. Ручные event-константы допустимы только как локальные alias на generated-константы.
- Системные action key и resource type платформы брать из общего каталога `libs/go/accesscatalog`; не дублировать их строковыми литералами в сервисах-владельцах и потребителях `access-manager`.
- Доменные enum конкретного сервиса не выносить в shared-библиотеку без отдельного архитектурного решения. Если enum нужен другому сервису как контракт, он должен жить в protobuf/gRPC или AsyncAPI и попадать в код через codegen.

## Чтение данных без N+1

- Не выполнять обращения к БД, внешним API, Kubernetes API, кэшу или методам репозитория внутри циклов обхода коллекций, если те же данные можно получить пачкой.
- Для графов, списков и деревьев использовать пакетный обход фронтира: собрать набор идентификаторов, выполнить один или несколько пакетных запросов, затем обрабатывать результат в памяти.
- Если пакетный контракт ещё не введён и срез остаётся малым, допускается локальный кэш запроса внутри сценария, но только как осознанный промежуточный шаг с отдельной задачей на полноценную пакетную оптимизацию.
- Фильтры по нескольким статусам, типам или идентификаторам передавать в репозиторий одним параметром (`ANY(...)`, `IN (...)`, временная таблица или аналогичный механизм), а не выполнять одинаковый запрос по одному значению в цикле.
- Любой оставленный N+1-путь должен быть явно обоснован в PR и иметь ограничение по размеру входных данных или задачу на устранение.

## Имена операций для ошибок и наблюдаемости

- Операции, которые попадают в ошибки, логи, метрики, спаны или идемпотентный след, задавать именованными константами.
- Формат значения: `<layer>.<Type>.<Method>`, например `domain.Repository.GetOrganization` или `domain.Service.CreateOrganization`.
- Вызовы repository/helper функций не должны передавать произвольные строковые литералы вроде `"get organization"` как имя операции.
- Если метод переименован, константа операции меняется вместе с методом и проверяется на уровне ревью.

## Идемпотентный повтор команд

- `command_id` и `idempotency_key` не являются границей авторизации и не могут сами по себе подтверждать доступ к сохранённому результату.
- Перед возвратом сохранённого результата повтора нужно загрузить фактический aggregate и сверить его область доступа с текущим запросом: project, organization или другой доменный scope.
- Если сохранённый aggregate относится к другой области доступа, возвращать конфликт, а не найденный aggregate и не результат из другого проекта/организации.
- Повторяющуюся схему повтора выносить в helper use-case слоя, чтобы новые команды не копировали небезопасную последовательность `find command result -> return aggregate`.
- Авторизация текущего actor выполняется до возврата сохранённого результата по той области доступа, которая действительно владеет сохранённым aggregate или явно сверена с ним.

## Пользовательские тексты и локализация

- Запрещено хранить в Go-коде пользовательские UI-тексты (HTTP ответы для UI, тексты комментариев в GitHub, уведомления и т.п.).
- Тексты, отображаемые пользователю, должны храниться во внешних ресурсах:
  - шаблоны через `embed` (`*.tmpl`, `*.md.tmpl`) с рендером контекста;
  - либо централизованные локализационные файлы (`yaml/json`) с загрузкой через библиотеку i18n.
- В коде допускаются только технические ключи/идентификаторы, а не готовые пользовательские фразы.
- Для backend i18n использовать общий runtime `libs/go/i18n` на базе `github.com/nicksnyder/go-i18n/v2`.
- i18n применять только к системным полям и системным сообщениям: названиям действий, безопасным сообщениям, уведомлениям и подсказкам. Пользовательские поля (`display_name`, описание проекта, текст задачи, комментарий) пока хранятся в БД в одной локали, которую ввёл пользователь.
- Системные каталоги действий доступа не заполнять локализованными строками в SQL-миграциях. Для code-owned действий хранить stable action key и i18n message id в коде, а пользовательские или пакетные действия заводить как данные только там, где они действительно изменяемы пользователем.

## Доменные контексты (минимум)

В сервисе-владельце доменной логики должны быть явные доменные контексты (`bounded contexts`), если внутри сервиса есть несколько самостоятельных причин для изменения.

Правила:
- состав контекстов берётся из актуальной архитектурной документации проекта;
- не переносить универсальный список контекстов из другого сервиса или старой реализации;
- если сервис владеет несколькими поддоменами, каждый поддомен получает отдельные модели, use-case слой и repository-интерфейсы;
- если контекст становится самостоятельным владельцем данных и процесса, его нужно выделять в отдельный сервис или явно фиксировать как осознанное исключение.

## Нефункциональные требования

Обязательное:
- Health: `/health/livez`, `/health/readyz`.
- Metrics: `/metrics`.
- Structured logs, без секретов/PII.
- OTel tracing и пропагация контекста.
- Graceful shutdown по `SIGINT|SIGTERM|SIGQUIT|SIGHUP`.
- Базовый контекст приложения (`context.Background()`) создаётся только в `cmd/<service>/main.go` и прокидывается дальше через `internal/app`. Для graceful shutdown внутри `internal/app` использовать производный контекст от входного `ctx`, например `context.WithTimeout(context.WithoutCancel(ctx), timeout)`, а не новый `context.Background()`. В transport/domain/repository-слоях прямой вызов `context.Background()` запрещён.

## Запрещено

- Доменная логика в `transport/*`.
- Прямой импорт `client-go`/`go-github` в домене.
- SQL строками в Go-коде.
- Обход interface-layer и вызов vendor SDK из use-case слоя.
- Использование `map[string]any`/`any` как публичного transport-контракта.
