---
doc_id: ARC-CK8S-DATA-MODEL-0001
type: data-model
title: kodex — сквозная модель данных
status: active
owner_role: SA
created_at: 2026-04-26
updated_at: 2026-05-27
related_issues: [599, 600, 601, 602, 655, 711, 322, 893]
related_prs: []
approvals:
  required: ["Owner"]
  status: approved
  request_id: "owner-2026-04-26-platform-architecture-frame"
  approved_by: "ai-da-stas"
  approved_at: 2026-04-26
---

# Сквозная модель данных

## Кратко

PostgreSQL хранит платформенное состояние, но не становится складом всего подряд. У каждого сервиса-владельца есть собственная БД или собственный строго изолированный контур владения. Между сервисами-владельцами нет `FOREIGN KEY`, межбазовых join и каскадов. Согласованность поддерживается через контракты, доменные события, outbox/inbox, проекции чтения и сверку.

## Базовые правила

| Правило | Следствие |
|---|---|
| Один класс данных имеет одного сервиса-владельца | Схема, миграции и бизнес-инварианты принадлежат сервису-владельцу. |
| БД на сервис | Даже при одном физическом PostgreSQL-кластере данные разных сервисов-владельцев изолированы. |
| Нет реляционных связей между БД владельцев | Ссылки на чужие агрегаты хранятся как внешние идентификаторы. |
| Проекция чтения не является истиной | Пользовательский интерфейс и поиск могут читать проекции, но команды идут в сервисы-владельцы. |
| Внешняя система остаётся владельцем своих объектов | Платформа хранит только зеркало, метаданные, жизненный цикл и аудит, которые нужны ей самой. |
| Полные технические логи не пишутся в PostgreSQL | Храним статус, причину ошибки, ссылки и короткий хвост лога. |
| Конкурентные изменения идут через сервис-владелец | Агрегаты имеют версию, команды имеют идемпотентный ключ, долгие занятости оформляются как lease или резервация. |
| Визуальные данные хранятся ссылками | Агрегаты для отображения в пользовательском интерфейсе хранят ссылку на картинку в S3-compatible объектном хранилище, а не бинарные данные в PostgreSQL. |

## Что хранит платформа

| Класс | Сервис-владелец | Примеры |
|---|---|---|
| Доступ | `access-manager` | Пользователь, организация, группа, членство, действие доступа, правило доступа, allowlist, поставщик внешних аккаунтов, внешний аккаунт, ссылка на секрет, запись аудита. |
| Общий журнал событий | `platform-event-log` | Append-only поток доменных событий, checkpoint потребителей и короткие аренды чтения. |
| Проекты | `project-catalog` | Проект, репозиторий, описание сервиса, источник документации, правило ветки, релизная политика, правило размещения. |
| Зеркало провайдера | `provider-hub` | Операционное состояние внешнего аккаунта у провайдера, входящий журнал webhook, нормализованное событие, проекция рабочего артефакта, связь, курсор синхронизации, статус рассинхронизации, состояние лимитов. |
| Пакеты | `package-hub` | Пакет, версия пакета, источник каталога, установка, верификация, схема секрета, ценовые метаданные. |
| Агентная работа | `agent-manager` | Процесс, этап, профиль роли, версия шаблона промпта, агентный запуск, агентная сессия, правило автоматизации, результат приёмки, состояние ожидания. |
| Серверы и кластеры | `fleet-manager` | Сервер, кластер, здоровье кластера, связность, привязка размещения. |
| Среда исполнения | `runtime-manager` | Слот, подготовка workspace, задание, шаг задания, ссылка на технический артефакт среды, политика очистки, prewarm pool, короткий хвост лога. |
| Риски и релизы | `governance-manager` | Risk profile, risk rule, risk assessment, risk factor, review signal, gate policy, gate request, gate decision, release decision package, release decision, release safety state, blocking signal. |
| Взаимодействия | `interaction-hub` | Диалоговая ветка, сообщение, запрос доставки согласования, уведомление, подписка, попытка доставки, обратный вызов канала. |
| Операционные проекции | `operations-hub` | Элемент ленты событий, элемент операторской очереди, агрегированный статус, представление блокировки, представление инцидента. |
| Биллинг | `billing-hub` | Биллинговый аккаунт, запись затрат, правило распределения, черновик счёта, распределение выручки пакета. |

## Что не хранит платформа как свою истину

| Внешний владелец | Что остаётся у него |
|---|---|
| GitHub/GitLab | Полное тело `Issue`, `PR/MR`, review, diff, ветки, теги, нативные связи провайдера и комментарии как источник истины. |
| Kubernetes | Состояние pod/job/deployment, события кластера и полные контейнерные логи. |
| Реестр контейнерных образов | Образы, манифесты, blobs и теги как каталог образов. |
| Репозитории проекта | Файлы кода, проектной документации и документации сервисов. |
| Репозитории пакетов | Исходники пакетов, манифесты и версии как нативное Git-состояние провайдера. |
| Объектное хранилище | Временные голосовые и медиа-вложения. |

## Минимальные агрегаты по сервисам-владельцам

### `access-manager`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `Organization` | Контур владения, биллинга и доступа. | У каждой установки есть организация-владелец. |
| `User` | Пользователь платформы. | Самостоятельная регистрация запрещена; вход возможен только через разрешённый email и SSO/OIDC. |
| `Group` | Глобальная или организационная группа. | Пользователь может состоять в нескольких группах. |
| `Membership` | Связь пользователя, организации, группы или проекта. | Права вычисляются с учётом наследования и явных исключений. |
| `AccessAction` | Каталог пользовательских и пакетных действий, на которые выдаются правила доступа. | Системные действия платформы code-owned и связаны с i18n message id в коде; новые пакеты могут добавлять свои действия без миграции общей enum-схемы. |
| `ExternalProvider` | Поставщик внешних аккаунтов и его визуальные метаданные. | Внешние аккаунты ссылаются на поставщика внутри БД `access-manager`. |
| `ExternalAccount` | Внешний аккаунт как субъект политики. | Область применения, разрешённые операции и статус владеются `access-manager`. |
| `ExternalAccountBinding` | Привязка внешнего аккаунта к организации, проекту, репозиторию, роли или пакету. | Другие сервисы получают разрешение на использование, но не меняют политику напрямую. |
| `SecretBindingRef` | Ссылка на секрет внешнего аккаунта. | Сырые секреты не хранятся в PostgreSQL, не попадают в аудит и разрешаются только через общий resolver-контракт после проверки доступа. |
| `AccessDecisionAudit` | След принятого решения доступа. | Должен объяснять, почему действие разрешено или запрещено. |

### `project-catalog`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `Project` | Контейнер репозиториев, политик и ограничений среды исполнения. | Проект принадлежит организации. |
| `RepositoryBinding` | Привязка репозитория к проекту. | Один проект может иметь несколько репозиториев. |
| `DocumentationSource` | Источник проектной или сервисной документации. | Доступ агента может быть только на чтение или с правом изменения. |
| `ServiceDescriptor` | Проверенная часть `services.yaml`. | Не должен тащить устаревшие сервисы из архива. |
| `ReleasePolicy` | Правила веток, выкладки, отката и ссылки на risk profile. | Ветки и теги остаются нативными объектами провайдера; gate decisions и release decisions остаются в `governance-manager`. |
| `PlacementPolicy` | Ограничения инфраструктурного размещения. | Используется `fleet-manager` и `runtime-manager`. |

### `provider-hub`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `ProviderAccountRuntimeState` | Операционное состояние внешнего аккаунта у провайдера. | Каноническая политика аккаунта хранится в `access-manager`; здесь хранятся состояние авторизации, лимитов и результата операций. |
| `WebhookEvent` | Сырой входящий сигнал провайдера. | Дедупликация по delivery id или аналогу обязательна. |
| `ProviderWorkItemProjection` | Зеркало `Issue` или `PR/MR`. | Источник истины остаётся у провайдера. |
| `ProviderRelationship` | Нормализованная связь нативных объектов провайдера. | Не заменяет связь у провайдера. |
| `SyncCursor` | Контур инкрементальной сверки. | Должен иметь окно перекрытия и состояние лимитов. |
| `ProviderLimitSnapshot` | Снимок лимитов внешнего API. | Для `gh` это приближённый учёт, а не полный аудит каждой операции. |

### `agent-manager`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `FlowDefinition` | Версионируемый шаблон процесса. | Истина времени исполнения хранится в БД, репозиторий содержит установочные фикстуры. |
| `StageDefinition` | Этап процесса. | Этап не равен роли агента. |
| `RoleProfile` | Роль агента, права, среда исполнения и политика. | Роль может быть встроенной или пользовательской. |
| `PromptTemplateVersion` | Версия `work` или `revise` шаблона. | Используется через публикацию и аудит. |
| `Run` | Агентный запуск. | Не смешивается с платформенным заданием. |
| `AgentSession` | Продолжаемая сессия агента. | Может быть возобновлена с замечаниями. |
| `AcceptanceResult` | Результат машинной приёмки. | Должен ссылаться на проверенные артефакты. |

### `governance-manager`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `RiskProfile` | Scope набора risk rules и gate policy. | Не заменяет проектную политику; scope и release refs приходят из `project-catalog`. |
| `RiskRule` | Версионируемое правило классификации риска. | Должно объяснять min risk class и сработавший matcher. |
| `RiskAssessment` | Оценка риска перехода, `PR/MR`, release candidate, runtime job, policy change или документа. | Effective risk class не может быть ниже initial без явного human decision. |
| `RiskFactor` | Фактор, повысивший или объяснивший риск. | Полный diff, секреты и сырые payload не хранятся. |
| `ReviewSignal` | Сигнал от reviewer, QA, lexical gatekeeper, risk gatekeeper, SRE, security или custom role. | Signal входит в evidence package, но не заменяет обязательный Human gate. |
| `GatePolicy` | Правила, когда нужен Human gate или role-driven gate. | Может усиливать проектную policy, но не владеет проектной policy. |
| `GateRequest` | Конкретный запрос решения с evidence package. | Delivery и callbacks идут через `interaction-hub`, decision state хранится здесь. |
| `GateDecision` | Итог Human gate или policy gate. | Должен быть связан с target transition/release и actor/ref решения. |
| `ReleaseDecisionPackage` | Снимок evidence для релизного go/no-go. | Содержит refs на project/provider/runtime/agent, а не копии чужой истины. |
| `ReleaseDecision` | Go/no-go/hold/rollback/follow-up решение. | Успешный deploy `job` сам по себе не завершает release decision loop. |
| `ReleaseSafetyState` | Состояние release safety-loop после deploy и postdeploy. | `stable`, `hold`, `rollback` и `follow_up_required` основаны на signals и decisions. |
| `BlockingSignal` | Сигнал, блокирующий переход или релиз. | Источник сигнала трассируется и закрывается явным reason. |

### `runtime-manager`

| Агрегат | Назначение | Важные инварианты |
|---|---|---|
| `Slot` | Изолированная среда исполнения. | Базовый вариант первой версии — namespace. |
| `WorkspaceMaterialization` | Попытка подготовки локальных источников в слоте. | Состав источников приходит из `project-catalog`, а runtime фиксирует только результат подготовки. |
| `Job` | Техническая операция платформы. | Сборка, выкладка, зеркалирование, очистка и проверка здоровья являются одним классом задания. |
| `JobStep` | Детализация выполнения задания. | Полный лог не хранится в PostgreSQL. |
| `RuntimeArtifactRef` | Ссылка на внешний технический артефакт. | Платформа не становится реестром образов. |
| `CleanupPolicy` | Правила срока хранения и регламентной очистки. | Сбой очистки должен быть видим оператору. |
| `PrewarmPool` | Управляемый пул прогретых слотов. | Дефицит прогретых слотов должен быть видим как tuning signal. |

## Межсервисная синхронизация

### Outbox/inbox

Каждый сервис-владелец, меняющий важное состояние, записывает событие в outbox в той же транзакции, где меняет своё состояние. Доставщик сервиса-владельца публикует событие в отдельный платформенный контур `platform-event-log` на PostgreSQL. Каждый потребитель читает этот журнал со своим inbox/checkpoint и обрабатывает событие идемпотентно.

Та же транзакция должна фиксировать новую версию агрегата и идемпотентный след команды, если агрегат может изменяться конкурентно. Повторная доставка команды или события не должна создавать второе бизнес-изменение.

Минимальный доставщик на PostgreSQL должен забирать события короткой арендой записи, ограничивать размер пачки, хранить номер попытки, время следующей попытки, тип сбоя, признак постоянного сбоя и короткий текст последней ошибки. Успешная или неуспешная отметка попытки должна проверять номер попытки, чтобы поздний обработчик не снял аренду с записи, которую уже забрал другой экземпляр. Это даёт доставку минимум один раз; потребители обязаны дедуплицировать событие по `event_id`.

MVP-доставка для нескольких потребителей строится без отдельной шины сообщений: сервисный outbox публикует событие в отдельную БД `platform-event-log`, а потребители ведут собственные inbox/checkpoint. Диагностические каналы, которые только пишут событие в лог процесса и помечают outbox опубликованным, не являются MVP-доставкой и не должны включаться в контурах, где события нужны другим сервисам.

Минимальный общий журнал владеется контуром `platform-event-log`, а не доменными сервисами-публикаторами. Его миграции живут отдельно от миграций `access-manager` и других сервисов-владельцев. Журнал состоит из двух таблиц:
- `platform_event_log` — append-only поток доменных событий с монотонным `sequence_id`, стабильным `event_id`, источником сервиса, типом события, версией схемы, агрегатом, полезной нагрузкой и временем фиксации;
- `platform_event_consumer_checkpoints` — независимый checkpoint каждого потребителя с последним обработанным `sequence_id`, владельцем короткой аренды, временем окончания аренды и durable retry-состоянием (`retry_sequence_id`, `retry_attempt`, короткий `last_error`).

Сервисный доставщик outbox подключается к `platform-event-log` отдельным DSN и пишет в `platform_event_log` идемпотентно по `event_id`: повтор с тем же `event_id` и тем же неизменяемым содержимым события считается успешным, а повтор с тем же `event_id`, но другим источником, типом, версией схемы, агрегатом, полезной нагрузкой или временем события считается конфликтом публикации. Потребители используют общий runtime `libs/go/eventconsumer`: он claim/read через `eventlog.Store`, ведёт `consumer_name`, lease owner, checkpoint и durable retry attempt, применяет registry по `event_type/schema_version`, ограничивает concurrency, отдаёт handler context с deadline, делает retry/backoff через shared checkpoint lease и двигает checkpoint только после идемпотентной обработки или bounded poison-решения. Это даёт веерную доставку без копирования события в отдельную строку на каждого потребителя и оставляет переход к Kafka или другому брокеру совместимым с текущими контрактами.

Текущая модель `platform-event-log` хранит общий поток, checkpoint и минимальное durable retry-состояние потребителя. Persistent business failed/poison state записывается в доменном сервисе-владельце как безопасный diagnostic/status, если у него есть такая модель. Consumer runtime не хранит raw payload, секреты, diff, prompt, stdout/stderr или большие детали; в checkpoint, логах и hook-диагностике остаются только тип события, версия, sequence, статус, safe code и короткий summary.

Пример удаления организации:
1. `access-manager` меняет состояние организации на archived или deleted.
2. В той же транзакции он пишет событие `access.organization.archived`.
3. `project-catalog` архивирует или блокирует новые операции по проектам организации.
4. `provider-hub` перестаёт использовать внешние аккаунты этой организации после события или отказа `access-manager`.
5. `runtime-manager` останавливает или переводит слоты и задания в запрещённое состояние по политике.
6. `billing-hub` закрывает расчётный период или помечает контур для финального счёта.
7. `operations-hub` обновляет проекцию чтения и операторскую ленту событий.

### Сверка

Сверка нужна для:
- восстановления после потерянных webhook или событий;
- проверки горячих нативных объектов провайдера;
- сверки состояния среды исполнения с Kubernetes;
- проверки источников пакетов и доступности внешних аккаунтов;
- восстановления проекций чтения.

Сверка не должна превращаться в постоянный полный обход всех внешних систем. Она использует курсор, окно перекрытия, приоритеты горячих сущностей и лимитный бюджет.

## Событийная шина

В первой реализации целевой контур — outbox/inbox на PostgreSQL, отдельный общий журнал `platform-event-log` и worker-доставка. Kafka или другой брокер не является обязательным стартовым требованием.

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

Требование к дизайну: формат событий, `event_id`, `aggregate_id`, `occurred_at`, версия схемы и идемпотентность должны проектироваться так, чтобы будущий брокер не ломал доменные контракты.

## Сроки хранения и ограничения хранения

- Сырые данные webhook хранятся с ограниченным сроком хранения.
- Короткие хвосты логов по заданиям хранятся для быстрого пользовательского интерфейса и диагностики.
- Полные логи остаются в контуре среды исполнения и логирования.
- Временные медиа имеют срок жизни и ссылку на объектное хранилище.
- Зеркало провайдера не должно бесконечно хранить лишние исходные данные без бизнес-смысла.

## Апрув

- request_id: `owner-2026-04-26-platform-architecture-frame`
- Решение: approved
- Комментарий: модель данных входит в сквозной архитектурный каркас платформы.
