docs(): added RUNBOOK, compliance matrix, ADR, refactored C4 and

repository map

zavoz
This commit is contained in:
ITQ
2026-02-24 13:17:24 +03:00
parent 7bf3ccee5c
commit 740fd2d7bd
18 changed files with 542 additions and 10 deletions
+7
View File
@@ -0,0 +1,7 @@
# 1. Цели архитектуры
1. Обеспечить воспроизводимый критичный поток `decide -> event -> report/guardrail` без ручных допущений в runtime.
2. Гарантировать безопасный lifecycle эксперимента: ревью, статусные переходы, блокировка конфликтных запусков.
3. Сохранить корректность данных для аналитики: идемпотентный приём событий, атрибуция через `decision_id`, обработка out-of-order через `PendingEvent`.
4. Поддержать операционную проверяемость: `health/readiness`, метрики, структурированные логи, фоновые задачи.
5. Дать артефакты, по которым жюри может пройти критерии `B1..B10` и выбранные `FX` без домысливания.
+26
View File
@@ -0,0 +1,26 @@
# 2. Контекст и границы
## Внутри системы
- Один backend-контейнер (Django + Django Ninja API).
- Доменные модули в `apps/*`, HTTP-слой в `api/v1/*`.
- Хранение: реляционная БД (PostgreSQL в целевом окружении, SQLite по умолчанию локально).
- Кэш/брокер: Redis/Valkey в целевом окружении, LocMem cache локально/в тестах.
- Фоновые задачи: Celery worker/beat (guardrails, notifications, cleanup pending).
## Внешние акторы
- Product client: вызывает `POST /api/v1/decide`, отправляет `POST /api/v1/events`.
- Experimenter/Approver/Admin/Viewer: управляют экспериментами и смотрят результаты через API.
- Каналы уведомлений: Telegram API и SMTP.
- Система наблюдаемости: скрейпинг `/metrics`, чтение структурированных логов.
## Служебные контракты
- `GET /health`: liveness.
- `GET /ready`: readiness (cache, db, storage, celery ping).
- `GET /metrics`: prometheus endpoint.
## Явная граница фактов
Документация привязана к текущей реализации в `src/backend`.
+42
View File
@@ -0,0 +1,42 @@
# 3. Драйверы и NFR
## Корректность и воспроизводимость
- Детерминированная раздача варианта через SHA-256 hash по `subject_id` и `experiment_id`.
- Один активный эксперимент на флаг (`running/paused`) через DB constraint.
- Строгие переходы lifecycle и блокировка недопустимых переходов.
- Идемпотентность событий по `event_id`.
## Safety
- Guardrails с порогом, окном наблюдения и автоматическим действием (`pause` или `rollback`).
- Ограничение участия пользователя: max concurrent + cooldown.
- Conflict domains для детерминированного разрешения коллизий экспериментов.
## Целостность аналитики
- Атрибуция через `decision_id`.
- Для событий с `requires_exposure=True`: без exposure событие уходит в `PendingEvent` и промотируется позже.
- Отчёт строится по вариантам и выбранным метрикам эксперимента.
## Эксплуатация
- Health/readiness probes.
- Prometheus-инструментация HTTP и бизнес-счётчиков.
- Structured logging с correlation id.
- Регламентные фоновые задачи Celery.
## Производительность
Реализовано:
- кэш флагов и активных экспериментов;
- кэш результата `decide` (TTL через `DECISION_RESULT_CACHE_TTL_SECONDS`);
- режим записи `Decision`: `sync|async|disabled` (`DECISION_WRITE_MODE`) с принудительным sync для `experiment_assigned`;
- async persistence через `events.persist_decision`;
- `reports` считает `average` через `Avg`, `percentile` через DB aggregate (`PERCENTILE_CONT` для PostgreSQL);
- в `reports` убрана materialization `decision_ids`, используется `Subquery`.
Текущие ограничения:
- в режиме `async` устойчивость записи зависит от здоровья Celery worker/broker;
- SQL-ветка percentile зависит от СУБД (PostgreSQL/не-PostgreSQL fallback);
- отдельный нагрузочный benchmark-артефакт не приложен.
+45
View File
@@ -0,0 +1,45 @@
# 4. Ключевые архитектурные решения
## Базовые решения
| ID | Решение | Зачем | Последствия |
|---|---|---|---|
| ADR-01 | Модульный monolith на Django + Django Ninja | Быстрый delivery и единая транзакционная модель | Вертикальное масштабирование, не микросервисы |
| ADR-02 | Разделение на `apps/*` и `api/v1/*` | Чёткие границы между domain и transport | Нужна дисциплина зависимостей |
| ADR-03 | Hash-based deterministic assignment в `decide` | Выполнение B2-4 (стабильность результата) | Чувствительность к качеству `subject_id` |
| ADR-04 | Review policy: `ApproverGroup` + fallback `ReviewSettings` | Выполнение требований по ревью и fallback | Политика плоская, без орг-иерархий |
| ADR-05 | Lifecycle как state machine + validators + DB constraint | Невалидные переходы и двойной запуск на флаг блокируются | Правила статусов централизованы |
| ADR-06 | Event ingestion с дедупом и `PendingEvent` | Идемпотентность и out-of-order атрибуция | Дополнительная сложность обработки pending |
| ADR-07 | Guardrails как first-class модели + auto action | Авто-реакция на деградацию и аудит триггера | Риск ложных срабатываний при шумных данных |
| ADR-08 | Conflict domains (`mutual_exclusion`, `priority`) | Детерминированное разрешение пересечений экспериментов | Дополнительные проверки на старте и в decide |
| ADR-09 | Базовая observability (`health`, `ready`, `/metrics`, JSON logs) | Проверяемость B9 и эксплуатационная диагностика | SLA readiness подтверждается только live-demo |
| ADR-10 | Проверка качества через `just` и автотесты | Воспроизводимая верификация B1/B8/B10 | - |
| ADR-11 | Кэш результата `decide` в cache backend (Redis/Valkey в целевом окружении) | Снизить CPU/DB в hot-path | Ниже latency на повторяющихся запросах; риск stale-ответа в рамках TTL |
| ADR-12 | Режим записи `Decision`: `sync\|async\|disabled`; async через `events.persist_decision`; `experiment_assigned` всегда sync | Снять write-pressure с hot-path без потери атрибуции | В `async` режиме нужна стабильность broker/worker и мониторинг очереди |
| ADR-13 | RBAC через роли `admin/experimenter/approver/viewer` + JWT bearer + endpoint guards | Выполнение требований доступа и ревью-ответственности из раздела 0 ТЗ | Вся авторизация централизована в role guards; нужен контроль качества секретов JWT в окружении |
| ADR-14 | Типизированные feature flags (`string/boolean/integer`) и публичный контракт обновления только `default_value` | Исключить несогласованные значения флагов и обеспечить безопасные переключения без релиза | Смена `key/value_type` решается созданием нового флага и миграцией использования |
| ADR-15 | Каталог типов событий (`EventType`) с `required_fields`, `is_exposure`, `requires_exposure` | Явно отделить схему событий от runtime-обработки и обеспечить корректную атрибуцию | Требуется дисциплина ведения каталога; неактивные типы событий блокируют ingestion |
| ADR-16 | Каталог метрик (`MetricDefinition`) + явная привязка метрик к эксперименту (`ExperimentMetric`) | Выполнение требований 5.4 ТЗ и единый расчёт для reports/guardrails | Изменение `calculation_rule` влияет на интерпретацию отчётов и требует управляемых изменений |
| ADR-17 | Завершение эксперимента через отдельную сущность `ExperimentOutcome` с обязательным `rationale` и валидируемыми outcome-сценариями | Выполнение требований 2.6 и B6-4/B6-5 | Решение по rollout/rollback/no_effect становится аудитируемым и воспроизводимым |
| ADR-18 | Уведомления как rule-based pipeline: `NotificationRule` -> `NotificationLog` -> async flush, с dedup/rate-limit по `event_key` | Закрыть FX notifications без “шторма” сообщений | Ограничение по каналам (`telegram`, `smtp`), требуется мониторинг failed delivery |
| ADR-19 | Поиск по эвристике среди Learnings | Реализовать поиск по базе | Поиск эвристический (веса/Jaccard), не ML и требует аккуратной интерпретации |
## Неочевидные инварианты
- В один момент времени на один `FeatureFlag` разрешён только один активный эксперимент (`running|paused`) через partial `UniqueConstraint`.
- После старта эксперимента заморожены поля, влияющие на раздачу (`traffic_allocation`, `targeting_rules`, `flag`), и запрещены изменения вариантов вне `draft`.
- Перед отправкой на review проверяются инварианты состава вариантов: минимум 2, ровно 1 control, сумма весов строго равна `traffic_allocation`.
- Один approver может оставить только одно одобрение по эксперименту (`unique_approval_per_user`).
- Для `requires_exposure=True` событие без exposure не атрибутируется сразу: уходит в `PendingEvent`, промотируется после прихода exposure, затем очищается по TTL.
- Одна метрика не может быть прикреплена к эксперименту дважды (`unique_experiment_metric`).
- `Learning` хранится в one-to-one связи с экспериментом.
## Ключевые риски
| Риск | Проявление | Смягчение | Остаток |
|---|---|---|---|
| Stale ответ из кэша `decide` | Кратковременный возврат устаревшего результата в пределах TTL | Revision-aware cache key + короткий TTL | Низкий/средний |
| Потеря throughput при проблемах Celery в `async` | Очередь растёт, запись `Decision` отстаёт | Режимы `sync|async|disabled`, force-sync для `experiment_assigned`, fallback на sync при ошибке enqueue | Средний |
| Тяжёлые запросы отчётов на больших данных | Рост latency для percentile/агрегаций | DB aggregate + `Subquery`, фильтрация attributed событий, индексы | Средний |
| Потеря отложенных атрибуций | `PendingEvent` истекает до прихода exposure | TTL 7 дней + cleanup + промоция при exposure | Средний |
| Сбой каналов уведомлений | Отправка не доходит в Telegram/SMTP | `NotificationLog` со статусами и ошибками | Средний |
+33
View File
@@ -0,0 +1,33 @@
# 5. Критичный путь
## Decide (`apps/decision/services.py`)
1. `flag` из cache/DB.
2. `active_experiment` из cache/DB.
3. Формируется cache key результата (`flag`, `subject`, digest атрибутов, ревизии `flag/experiment`).
4. При cache hit возвращается тот же outcome/reason/value с новым `decision_id`.
5. При cache miss выполняются проверки: running experiment, targeting, participation limits, domain conflicts, traffic allocation.
6. Если назначен вариант, выбирается детерминированно по hash + weights.
7. Результат может быть закэширован (по whitelist reason).
8. Запись `Decision` зависит от `DECISION_WRITE_MODE`: `sync|async|disabled`; для `experiment_assigned` запись всегда sync.
Reason-коды: `flag_not_found`, `no_active_experiment`, `targeting_mismatch`, `participation_limit`, `domain_conflict`, `outside_traffic_allocation`, `no_variants`, `experiment_assigned`.
## Events (`apps/events/services.py`)
1. Валидация типа события и payload.
2. Дедуп по `event_id`.
3. Exposure: `Exposure` + `Event`, затем промоция `PendingEvent`.
4. Conversion: при `requires_exposure=True` без exposure -> `PendingEvent` (TTL 7 дней), иначе attributed `Event`.
## Reports (`apps/reports/services.py`)
- По каждому варианту: `exposures`, `unique_subjects`, значения выбранных метрик.
- Поддерживается период `start/end`.
- `average` считает DB `Avg`, `percentile` - DB aggregate (`PERCENTILE_CONT` в PostgreSQL).
- Связь событий с экспозициями строится через `Subquery`, без materialization списка `decision_ids` в Python.
## Guardrails (`apps/guardrails/services.py`)
- Celery проверяет running эксперименты.
- При breach: `GuardrailTrigger` + действие (`pause`/`rollback`) + `ExperimentLog` + enqueue уведомления.
+34
View File
@@ -0,0 +1,34 @@
# 6. Эксплуатация и наблюдаемость
## Endpoints
- `/health` -> liveness (`health_check.Memory`).
- `/ready` -> readiness (`Cache`, `Database`, `Storage`, `Celery Ping`).
- `/metrics` -> Prometheus metrics.
SLA `ready <= 180s` подтверждается только live-demo.
## Метрики и логи
- Инфраструктурные метрики через `django-prometheus` middleware.
- Бизнес-счётчики: `lotty_decide_requests_total`, `lotty_events_ingested_total`.
- Production logs: JSON + `Correlation-ID` (`django-guid`).
## Celery задачи
Периодические:
- `guardrails.check_all` - 60s.
- `notifications.flush_pending` - 30s.
- `events.cleanup_expired_pending` - 3600s.
По запросу:
- `events.persist_decision` - асинхронная запись `Decision` при `DECISION_WRITE_MODE=async`.
## Runtime knobs
- `DECISION_RESULT_CACHE_TTL_SECONDS` (по умолчанию `60`).
- `DECISION_WRITE_MODE` (`sync|async|disabled`, по умолчанию `sync`).
## Команды
`cd src/backend && just run|test|test-coverage|lint|format`
+8
View File
@@ -0,0 +1,8 @@
# 7. Явные упрощения
1. Один backend-сервис (модульный monolith), без выделения микросервисов по доменам.
2. Отдельный statistical significance engine не реализован; решения фиксируются на метриках и guardrails.
3. Отчёты считаются синхронно в запросе, без pre-aggregation pipeline и materialized views (потому что нету ClickHouse).
4. Режим `DECISION_WRITE_MODE=sync` используется по умолчанию; `async`/`disabled` - эксплуатационные режимы.
5. Кэш результата `decide` основан на TTL и revision-aware ключе, без отдельной ручной инвалидции.
6. Similar learnings считаются эвристикой (Jaccard/веса), без ML-ранжирования.
+21
View File
@@ -0,0 +1,21 @@
# 8. Проверяемость
## Базовый набор
1. `cd src/backend && just --list`
2. `cd src/backend && just test`
3. `cd src/backend && just test-coverage && just show-coverage`
## Точечные проверки по доменам
1. `cd src/backend && uv run python manage.py test apps.decision.tests.test_decide`
2. `cd src/backend && uv run python manage.py test apps.events.tests.test_services tests.integration.test_events`
3. `cd src/backend && uv run python manage.py test apps.reports.tests.test_reports apps.guardrails.tests.test_guardrails`
4. `cd src/backend && uv run python manage.py test apps.reviews.tests.test_reviews_policy apps.experiments.tests.test_services`
5. `cd src/backend && uv run python manage.py test apps.conflicts.tests.test_conflicts apps.notifications.tests.test_notifications apps.learnings.tests.test_learnings`
## Runtime
- `GET /health` -> `200`
- `GET /ready` -> `200` после готовности зависимостей
- `GET /metrics` -> есть инфраструктурные и бизнес метрики
+16
View File
@@ -0,0 +1,16 @@
# ADR - LOTTY Backend
## Состав
| Файл | Содержание |
|---|---|
| `01-goals.md` | Цели архитектуры |
| `02-context.md` | Границы системы и акторы |
| `03-drivers-nfr.md` | Драйверы и NFR |
| `04-decisions.md` | Ключевые решения, неочевидные инварианты и ключевые риски |
| `05-critical-path.md` | Критичный поток `decide -> event -> report/guardrail` |
| `06-operations.md` | Эксплуатация и наблюдаемость |
| `07-simplifications.md` | Явные упрощения |
| `08-verification.md` | Как проверять критерии |
Связанная матрица соответствия: [compliance-matrix.md](../compliance-matrix.md).
+4 -2
View File
@@ -6,15 +6,17 @@
- Django settings: [src/backend/config/settings](./src/backend/config/settings) - Django settings: [src/backend/config/settings](./src/backend/config/settings)
- URL root: [src/backend/config/urls.py](./src/backend/config/urls.py) - URL root: [src/backend/config/urls.py](./src/backend/config/urls.py)
- API wiring: [src/backend/api/urls.py](./src/backend/api/urls.py) - API wiring: [src/backend/api/urls.py](./src/backend/api/urls.py)
- API v1 router: [src/backend/api/v1/router.py](./src/backend/api/v1/router.py) - API v1 роутер: [src/backend/api/v1/router.py](./src/backend/api/v1/router.py)
- Команды и проверки: [src/backend/justfile](./src/backend/justfile) - Команды и проверки: [src/backend/justfile](./src/backend/justfile)
- Файлы конфигурации деплоя: [deploy](./deploy)
- Файлы конфигурации для сервисов: [infrastructure/configs](./infrastructure/configs)
## 2. Основные доменные модули ([src/backend/apps](./src/backend/apps)) ## 2. Основные доменные модули ([src/backend/apps](./src/backend/apps))
- [flags](./src/backend/apps/flags) - feature flags и типизация значений. - [flags](./src/backend/apps/flags) - feature flags и типизация значений.
- [experiments](./src/backend/apps/experiments) - жизненный цикл эксперимента, варианты. - [experiments](./src/backend/apps/experiments) - жизненный цикл эксперимента, варианты.
- [reviews](./src/backend/apps/reviews) - группы аппруверов, дефолтная политика апрува. - [reviews](./src/backend/apps/reviews) - группы аппруверов, дефолтная политика апрува.
- [decision](./src/backend/apps/decision) - логика выбора варианта для флага (детерминизм, таргетинг, лимиты участия, конфликт-домены). - [decision](./src/backend/apps/decision) - логика выбора варианта для флага (детерминизм, таргетинг, лимиты участия).
- [events](./src/backend/apps/events) - обработка эвентов: валидация, дедуп, атрибуция, pending events. - [events](./src/backend/apps/events) - обработка эвентов: валидация, дедуп, атрибуция, pending events.
- [metrics](./src/backend/apps/metrics) - каталог и правила вычисления метрик. - [metrics](./src/backend/apps/metrics) - каталог и правила вычисления метрик.
- [reports](./src/backend/apps/reports) - отчёты по экспериментам в разрезе вариантов/периода. - [reports](./src/backend/apps/reports) - отчёты по экспериментам в разрезе вариантов/периода.
+18
View File
@@ -10,6 +10,24 @@ Service for managing A/B testing experiments. Drive your tests without breaking
### Diagrams ### Diagrams
#### C4 Context
![C4 context](./assets/plantuml/rendered/c4_context.png)
Source: [C4 context](./assets/plantuml/raw/c4-context.puml)
#### C4 Container
![C4 container](./assets/plantuml/rendered/c4_container.png)
Source: [C4 container](./assets/plantuml/raw/c4-container.puml)
#### C4 Component
![C4 component](./assets/plantuml/rendered/c4_component.png)
Source: [C4 component](./assets/plantuml/raw/c4-component-critical-path.puml)
### Testing report ### Testing report
Can be seen in GitLab CI/CD pipeline run Can be seen in GitLab CI/CD pipeline run
+204
View File
@@ -0,0 +1,204 @@
# Runbook
Этот runbook задаёт основной сценарий проверки по критериям `B1..B10` и выбранным `FX`.
## 1. Что проверяем
Сквозной поток:
- `decide` -> `events` -> `report`;
- lifecycle/review/guardrails;
- observability (`/health`, `/ready`, `/metrics`);
- инженерная дисциплина (tests, lint, format);
- выбранные FX: notifications, learnings, conflicts.
Артефакты:
- `compliance-matrix.md`
- `ADR/*`
- `MAP.md`
- `assets/plantuml/raw/c4-context.puml`
- `assets/plantuml/raw/c4-container.puml`
- `assets/plantuml/raw/c4-component-critical-path.puml`
## 2. Предусловия
### 2.1 Основной сценарий (по умолчанию): Docker Compose
Нужно:
- Docker
- Docker Compose
Файлы:
- `compose.yaml` - базовый стек
- `compose.prod.yaml` - базовый стек + observability
- Детали по compose-конфигурации: `README.md` -> `Setup with docker compose`.
Порты по умолчанию (см. `.env.template`):
- reverse proxy: `80`
- backend direct: `14609`
- static direct: `14610`
### 2.2 Дополнительный локальный сценарий (для тестов/линтинга)
Нужно:
- Python `>=3.13,<3.15`
- `uv`
- `just`
Рабочая директория:
- `cd src/backend`
## 3. Основной сценарий запуска (Docker Compose)
### 3.1 Выбор стека
Базовый стек:
```bash
docker compose -f compose.yaml up -d
```
Полный стек (с observability):
```bash
docker compose -f compose.prod.yaml --profile observability up -d
```
### 3.2 Проверка, что сервисы поднялись
```bash
docker compose -f compose.yaml ps
# или
docker compose -f compose.prod.yaml --profile observability ps
```
Ожидаем `running/healthy` для `postgresql`, `valkey`, `backend`, `backend-celery-worker`, `backend-celery-beat`.
### 3.3 Runtime sanity-check
Через reverse proxy (`80`):
```bash
curl -i http://127.0.0.1/health
curl -i http://127.0.0.1/ready
curl -s http://127.0.0.1/metrics | head -n 40
```
Альтернатива напрямую в backend (`14609`):
```bash
curl -i http://127.0.0.1:14609/health
curl -i http://127.0.0.1:14609/ready
curl -s http://127.0.0.1:14609/metrics | head -n 40
```
### 3.4 Просмотр логов
```bash
docker compose -f compose.yaml logs -f backend backend-celery-worker backend-celery-beat
# или
docker compose -f compose.prod.yaml --profile observability logs -f backend backend-celery-worker backend-celery-beat
```
## 4. Проверка критериев B1..B10
### B1. Запуск и воспроизводимость
Compose runtime-подтверждение:
```bash
docker compose -f compose.yaml up -d
docker compose -f compose.yaml ps
curl -i http://127.0.0.1/health
curl -i http://127.0.0.1/ready
```
Показываем:
- сервисы запускаются без ручных скрытых шагов;
- backend жив и готов после зависимостей.
### B2-B6, B8, B10. Функциональность, отчёты, тестирование и дисциплина
Эти проверки выполняются локально из `src/backend` (dev-зависимости):
```bash
cd src/backend
just --list
just test
just test-coverage
just show-coverage
just lint
just format
```
Точечные наборы:
```bash
cd src/backend
uv run python manage.py test apps.decision.tests.test_decide
uv run python manage.py test apps.events.tests.test_services tests.integration.test_events
uv run python manage.py test apps.reports.tests.test_reports apps.guardrails.tests.test_guardrails
uv run python manage.py test apps.reviews.tests.test_reviews_policy apps.experiments.tests.test_services
uv run python manage.py test tests.integration.test_negative tests.integration.test_happy_path tests.integration.test_api_contract
```
### B7. Архитектурные артефакты
Проверяем:
- `compliance-matrix.md`
- `ADR/README.md`
- `ADR/04-decisions.md`
- `MAP.md`
- `assets/plantuml/raw/c4-context.puml`
- `assets/plantuml/raw/c4-container.puml`
- `assets/plantuml/raw/c4-component-critical-path.puml`
### B9. Эксплуатационная готовность и наблюдаемость
Runtime через compose:
```bash
curl -i http://127.0.0.1/health
curl -i http://127.0.0.1/ready
curl -s http://127.0.0.1/metrics | head -n 40
```
## 5. Проверка выбранных допфич (FX)
Локально из `src/backend`:
FX: Notifications
```bash
uv run python manage.py test apps.notifications.tests.test_notifications api.v1.notifications.tests.test_notifications_api
```
FX: Learnings
```bash
uv run python manage.py test apps.learnings.tests.test_learnings api.v1.learnings.tests.test_learnings_api
```
FX: Conflicts
```bash
uv run python manage.py test apps.conflicts.tests.test_conflicts api.v1.conflicts.tests.test_conflicts_api
```
Объединённый прогон:
```bash
uv run python manage.py test \
apps.notifications.tests.test_notifications \
apps.learnings.tests.test_learnings \
apps.conflicts.tests.test_conflicts \
api.v1.notifications.tests.test_notifications_api \
api.v1.learnings.tests.test_learnings_api \
api.v1.conflicts.tests.test_conflicts_api
```
## 6. Остановка и очистка
Базовый стек:
```bash
docker compose -f compose.yaml down
```
Полный стек:
```bash
docker compose -f compose.prod.yaml --profile observability down
```
С удалением томов:
```bash
docker compose -f compose.yaml down -v
# или
docker compose -f compose.prod.yaml --profile observability down -v
```
@@ -7,20 +7,21 @@ title LOTTY Backend - C4 Component
Container_Boundary(api, "Backend Container (Django + Django Ninja)") { Container_Boundary(api, "Backend Container (Django + Django Ninja)") {
Component(decision_endpoint, "Decision Endpoint", "api/v1/decision/endpoints.py", "POST /api/v1/decide") Component(decision_endpoint, "Decision Endpoint", "api/v1/decision/endpoints.py", "POST /api/v1/decide")
Component(decision_service, "Decision Service", "apps/decision/services.py", "Flag lookup, targeting DSL, participation limits, hash-based variant assignment") Component(decision_service, "Decision Service", "apps/decision/services.py", "Flag lookup, targeting DSL, limits/conflicts, hash-based assignment, decide-result cache, sync/async decision persistence")
Component(events_endpoint, "Events Endpoint", "api/v1/events/endpoints.py", "POST /api/v1/events") Component(events_endpoint, "Events Endpoint", "api/v1/events/endpoints.py", "POST /api/v1/events")
Component(events_service, "Events Service", "apps/events/services.py", "Validation, dedup, exposure/conversion attribution, pending promotion") Component(events_service, "Events Service", "apps/events/services.py", "Validation, dedup, exposure/conversion attribution, pending promotion")
Component(events_tasks, "Events Tasks", "apps/events/tasks.py", "persist_decision, cleanup pending")
Component(reports_endpoint, "Reports Endpoint", "api/v1/reports/endpoints.py", "GET /reports/{experiment_id}") Component(reports_endpoint, "Reports Endpoint", "api/v1/reports/endpoints.py", "GET /reports/{experiment_id}")
Component(reports_service, "Reports Service", "apps/reports/services.py", "Per-variant metric calculation (ratio, count, average, percentile)") Component(reports_service, "Reports Service", "apps/reports/services.py", "Per-variant metrics (ratio/count/average/percentile)")
Component(guardrails_service, "Guardrails Service", "apps/guardrails/services.py", "Threshold checks, auto pause/rollback") Component(guardrails_service, "Guardrails Service", "apps/guardrails/services.py", "Threshold checks, auto pause/rollback")
} }
ContainerDb(db, "Relational DB", "PostgreSQL / SQLite", "Flags, experiments, variants, decisions, events, exposures") ContainerDb(db, "Relational DB", "PostgreSQL / SQLite", "Flags, experiments, variants, decisions, events, exposures")
Container(cache, "Cache", "Valkey / LocMem", "Flag and active experiment cache") Container(cache, "Cache + Broker", "Valkey / Redis / LocMem", "Flag/active experiment/decide-result cache, Celery broker")
Container(worker, "Celery Beat", "Celery", "Periodic guardrail checks (60s)") Container(worker, "Celery Worker / Beat", "Celery", "Periodic guardrails/notifications/cleanup + async decision persistence")
Rel(decision_endpoint, decision_service, "Delegates") Rel(decision_endpoint, decision_service, "Delegates")
Rel(decision_service, cache, "Reads cached flag / active experiment") Rel(decision_service, cache, "Reads cached flag / active experiment")
@@ -28,6 +29,7 @@ Rel(decision_service, db, "Reads variants, writes Decision")
Rel(events_endpoint, events_service, "Delegates batch") Rel(events_endpoint, events_service, "Delegates batch")
Rel(events_service, db, "Writes Event / Exposure / PendingEvent") Rel(events_service, db, "Writes Event / Exposure / PendingEvent")
Rel(events_tasks, db, "Writes Decision async")
Rel(reports_endpoint, reports_service, "Builds report") Rel(reports_endpoint, reports_service, "Builds report")
Rel(reports_service, db, "Reads attributed events / exposures") Rel(reports_service, db, "Reads attributed events / exposures")
+4 -4
View File
@@ -11,8 +11,8 @@ Person(ops_user, "Experimenter/Approver/Admin/Viewer")
System_Boundary(lotty_boundary, "LOTTY Backend") { System_Boundary(lotty_boundary, "LOTTY Backend") {
Container(api, "Backend API", "Django + Ninja", "REST endpoints: flags, experiments, decide, events, reports, guardrails, notifications, conflicts, metrics, learnings, reviews, users, auth") Container(api, "Backend API", "Django + Ninja", "REST endpoints: flags, experiments, decide, events, reports, guardrails, notifications, conflicts, metrics, learnings, reviews, users, auth")
ContainerDb(db, "PostgreSQL / SQLite", "Relational DB", "Experiments, variants, decisions, events, exposures, outcomes, guardrails, notifications, conflicts, logs") ContainerDb(db, "PostgreSQL / SQLite", "Relational DB", "Experiments, variants, decisions, events, exposures, outcomes, guardrails, notifications, conflicts, logs")
Container(cache, "Valkey / LocMem Cache", "Cache + Celery broker", "Flag/experiment cache, Celery task broker") Container(cache, "Valkey / Redis / LocMem Cache", "Cache + Celery broker", "Flag/experiment/decide-result cache, Celery task broker")
Container(worker, "Celery Worker / Beat", "Background worker", "Periodic: guardrails check, notifications flush, pending events cleanup") Container(worker, "Celery Worker / Beat", "Background worker", "Periodic jobs + async decision persistence")
} }
System_Ext(notifications, "Notification Channels", "Telegram / SMTP") System_Ext(notifications, "Notification Channels", "Telegram / SMTP")
@@ -20,9 +20,9 @@ System_Ext(notifications, "Notification Channels", "Telegram / SMTP")
Rel(product_client, api, "Gets flag decisions, sends events", "HTTP/JSON") Rel(product_client, api, "Gets flag decisions, sends events", "HTTP/JSON")
Rel(ops_user, api, "Admin/experiment/review calls", "HTTP/JSON") Rel(ops_user, api, "Admin/experiment/review calls", "HTTP/JSON")
Rel(api, db, "Read/write application state", "SQL") Rel(api, db, "Read/write application state", "SQL")
Rel(api, cache, "Read/write cache", "Redis protocol") Rel(api, cache, "Read/write cache, enqueue async tasks", "Redis protocol")
Rel(worker, db, "Read/write application state", "SQL") Rel(worker, db, "Read/write application state", "SQL")
Rel(worker, cache, "Receives tasks, reads cached state", "Redis protocol") Rel(worker, cache, "Consumes tasks, reads cached state", "Redis protocol")
Rel(worker, notifications, "Delivers queued notifications", "HTTP / SMTP") Rel(worker, notifications, "Delivers queued notifications", "HTTP / SMTP")
@enduml @enduml
Binary file not shown.
Binary file not shown.
Binary file not shown.
+65
View File
@@ -0,0 +1,65 @@
# Матрица соответствия: задание -> критерий -> реализация
Статусы:
- `подтверждено` - есть прямая реализация и проверка тестами/командами.
- `частично (live-demo)` - реализация есть, но критерий окончательно закрывается только на живом запуске.
| ID задания | ID критерия | Проблема/риск | Где реализовано | Как проверяется | Какие данные нужны | Статус |
|---|---|---|---|---|---|---|
| `D.4` | `B1-1` | Без предусловий жюри не сможет воспроизвести запуск | `RUNBOOK.md`, `src/backend/README.md` | Ручная проверка разделов предусловий | Python, uv, just, рабочая директория `src/backend` | подтверждено |
| `D.4` | `B1-2` | Неоднозначные команды старта ломают проверку | `RUNBOOK.md`, `src/backend/justfile` | `cd src/backend && just --list` | Доступ к `just` | подтверждено |
| `D.3` | `B1-3` | Скрытые ручные шаги делают запуск невоспроизводимым | `RUNBOOK.md`, `compose.yaml`, `compose.prod.yaml` | Полный прогон по runbook на стенде | Чистое окружение и runtime-сервисы | частично (live-demo) |
| `3.7` | `B1-4` | Сервис может стартовать, но быть неготовым к запросам | `src/backend/api/urls.py` (`/health`, `/ready`) | Runtime `curl /health` и `curl /ready` | Поднятый backend и зависимости | частично (live-demo) |
| `D.5` | `B1-5` | Без e2e happy-path нельзя доказать работоспособность | `src/backend/tests/integration/test_happy_path.py`, `src/backend/tests/integration/test_api_contract.py` | `cd src/backend && just test` | Тестовые фикстуры и встроенный test DB | подтверждено |
| `1.3, 3.4` | `B2-1` | Возврат не-default без активного эксперимента искажает контроль | `src/backend/apps/decision/services.py` | `apps.decision.tests.test_decide.DecideForFlagTest.test_no_active_experiment` | Флаг с default и без running эксперимента | подтверждено |
| `1.3, 2.7` | `B2-2` | Пользователь вне таргетинга не должен получать variant | `src/backend/apps/decision/services.py`, `src/backend/libs/dsl/*` | `apps.decision.tests.test_decide.TargetingRulesTest.test_targeting_fail_returns_default` | Эксперимент с targeting rules и mismatching subject | подтверждено |
| `1.3, 3.4` | `B2-3` | При применимом эксперименте нужен variant, а не default | `src/backend/apps/decision/services.py` | `apps.decision.tests.test_decide.DecideForFlagTest.test_running_experiment_assigns_variant` | Running experiment с вариантами | подтверждено |
| `3.5.1` | `B2-4` | Нестабильная выдача ломает статистику и UX | `src/backend/apps/decision/services.py` (`_hash_subject`) | `apps.decision.tests.test_decide.DecideForFlagTest.test_deterministic_assignment` | Повторные вызовы для одного subject | подтверждено |
| `2.2` | `B2-5` | Игнорирование weights делает тест нерепрезентативным | `src/backend/apps/decision/services.py` (`_select_variant`) | `apps.decision.tests.test_decide.SelectVariantTest.test_selects_by_weight` | Варианты с разными весами | подтверждено |
| `2.4` | `B3-1` | Без перехода в review нет управляемого процесса запуска | `src/backend/apps/experiments/services.py` (`experiment_submit_for_review`) | `apps.experiments.tests.test_services` | Draft experiment с валидными вариантами | подтверждено |
| `2.4` | `B3-2` | Без авто-перехода в approved ревью-процесс зависает | `src/backend/apps/experiments/services.py` (`experiment_approve`) | `apps.experiments.tests.test_services` | Approver + достигнутый порог approvals | подтверждено |
| `0.3, 2.4` | `B3-3` | Запуск без порога approvals делает процесс небезопасным | `src/backend/apps/experiments/services.py`, `src/backend/tests/integration/test_negative.py` | `InvalidLifecycleTransitionsTest.test_cannot_start_without_enough_approvals` | `default_min_approvals > 1`, недостаток approve | подтверждено |
| `2.5` | `B3-4` | Невалидные lifecycle переходы ломают состояние | `src/backend/apps/experiments/models.py` (`ALLOWED_TRANSITIONS`) | `src/backend/tests/integration/test_negative.py` | Эксперимент и попытка запрещённого перехода | подтверждено |
| `0.2, 0.3` | `B3-5` | Неназначенный пользователь не должен влиять на review | `src/backend/apps/reviews/selectors.py`, `src/backend/apps/experiments/services.py` | `apps.reviews.tests.test_reviews_policy`, `test_negative.ReviewPolicyEnforcementTest` | Approver group / fallback настройки | подтверждено |
| `4.5` | `B4-1` | Невалидные типы событий портят аналитику | `src/backend/apps/events/services.py` (`_validate_event_payload`) | `apps.events.tests.test_services` | Batch с ошибочными типами полей | подтверждено |
| `4.5` | `B4-2` | Отсутствие обязательных полей делает события непригодными | `src/backend/apps/events/services.py`, `src/backend/apps/events/models.py` | `apps.events.tests.test_services`, `test_negative.EventValidationIntegrationTest` | События без required base/properties | подтверждено |
| `4.3` | `B4-3` | Дубли событий завышают метрики | `src/backend/apps/events/services.py` (`_is_duplicate`) | `apps.events.tests.test_services`, `tests.integration.test_events` | Два события с одним `event_id` | подтверждено |
| `4.1, 4.4` | `B4-4` | Без связи exposure с decision теряется атрибуция | `src/backend/apps/events/models.py` (`Exposure.decision_id`) | `apps.events.tests.test_services` | Валидный `decision_id` и exposure event | подтверждено |
| `4.4.1` | `B4-5` | Конверсия без exposure не должна попадать в отчёт | `src/backend/apps/events/services.py` (`requires_exposure`, `PendingEvent`) | `apps.events.tests.test_services`, `tests.integration.test_events` | Conversion до exposure и последующая промоция | подтверждено |
| `6.2` | `B5-1` | Guardrail без metric_key нефункционален | `src/backend/apps/guardrails/models.py` (`metric`) | `apps.guardrails.tests.test_guardrails` | Guardrail с привязанной метрикой | подтверждено |
| `6.2` | `B5-2` | Guardrail без threshold не может сработать | `src/backend/apps/guardrails/models.py` (`threshold`) | `apps.guardrails.tests.test_guardrails` | Guardrail с заданным порогом | подтверждено |
| `6.3` | `B5-3` | Превышение порога должно фиксироваться автоматически | `src/backend/apps/guardrails/services.py` | `apps.guardrails.tests.test_guardrails`, `tests.integration.test_guardrails` | Метрика выше threshold в окне | подтверждено |
| `6.4` | `B5-4` | После trigger должно выполняться действие безопасности | `src/backend/apps/guardrails/services.py` (`pause`, `rollback`) | `apps.guardrails.tests.test_guardrails` | Running experiment + breach | подтверждено |
| `6.5` | `B5-5` | Без аудита триггеров нельзя объяснить остановку | `src/backend/apps/guardrails/models.py` (`GuardrailTrigger`), `apps/experiments/models.py` (`ExperimentLog`) | `apps.guardrails.tests.test_guardrails` | Triggered guardrail | подтверждено |
| `3.6` | `B5-6` | Один пользователь не должен постоянно быть в экспериментах | `src/backend/apps/decision/services.py` (`MAX_CONCURRENT_EXPERIMENTS`, `COOLDOWN_DAYS`) | `apps.decision.tests.test_decide.ParticipationLimitsTest` | Несколько running/completed экспериментов на одного subject | подтверждено |
| `5.2` | `B6-1` | Отчёт без периода неуправляем и спорный | `src/backend/apps/reports/services.py` | `apps.reports.tests.test_reports.CalculateMetricValueTest.test_period_filter` | События внутри/вне окна периода | подтверждено |
| `5.3` | `B6-2` | Без разреза по вариантам нельзя сравнивать A/B | `src/backend/apps/reports/services.py` (`variant_reports`) | `apps.reports.tests.test_reports.BuildExperimentReportTest` | Эксперимент минимум с двумя вариантами | подтверждено |
| `5.4` | `B6-3` | Отчёт должен показывать именно выбранные метрики эксперимента | `src/backend/apps/metrics/models.py`, `src/backend/apps/reports/services.py` | `apps.reports.tests.test_reports` | `ExperimentMetric` привязки | подтверждено |
| `2.6` | `B6-4` | Нужна фиксация исхода (`rollout/rollback/no_effect`) | `src/backend/apps/experiments/services.py` (`experiment_complete`) | `apps.experiments.tests.test_services` | Completed experiment и выбранный outcome | подтверждено |
| `2.6` | `B6-5` | Решение без rationale теряет объяснимость | `src/backend/apps/experiments/services.py` (валидация `rationale`) | `tests.integration.test_negative.InvalidLifecycleTransitionsTest.test_cannot_complete_without_rationale` | Пустой rationale при complete | подтверждено |
| `D.5(B7)` | `B7-1` | Неясный нейминг усложняет поддержку и демо | `src/backend/apps/*`, `src/backend/api/v1/*` | Архитектурный walkthrough | Репозиторий проекта | подтверждено |
| `D.5(B7)` | `B7-2` | Без границ модулей растёт связность и регрессии | `src/backend/apps/*`, `src/backend/api/v1/*` | Проверка структуры директорий и зависимостей | Репозиторий проекта | подтверждено |
| `D.4, D.6` | `B7-3` | Без матрицы тяжело трассировать критерии | `compliance-matrix.md` | Проверка заполнения всех ID | Этот файл | подтверждено |
| `D.4` | `B7-4` | Без ADR сложно доказать осознанные trade-off | `ADR/04-decisions.md` | Сверка решений с кодом | ADR и код `src/backend` | подтверждено |
| `D.4` | `B7-5` | Нужна контекстная диаграмма границ системы | `assets/plantuml/raw/c4-context.puml` | Просмотр диаграммы | PlantUML файл | подтверждено |
| `D.4` | `B7-6` | Нужна container-диаграмма взаимодействий | `assets/plantuml/raw/c4-container.puml` | Просмотр диаграммы | PlantUML файл | подтверждено |
| `D.4` | `B7-7` | Нужна component-диаграмма по критичному пути | `assets/plantuml/raw/c4-component-critical-path.puml` | Просмотр диаграммы | PlantUML файл | подтверждено |
| `D.4` | `B7-8` | Без карты репозитория навигация медленная | `MAP.md` | Проверка ссылок на точки входа | Файл `MAP.md` | подтверждено |
| `D.7` | `B7-9` | Неявные упрощения искажают ожидания жюри | `ADR/07-simplifications.md` | Проверка явного списка ограничений | ADR пакет | подтверждено |
| `D.5(B8)` | `B8-1` | Без негативных тестов edge-cases не покрыты | `src/backend/tests/integration/test_negative.py` | `cd src/backend && just test` | Тестовая БД и фикстуры | подтверждено |
| `D.5(B8)` | `B8-2` | Критичный поток нужен в интеграционных/контрактных тестах | `src/backend/tests/integration/test_happy_path.py`, `src/backend/tests/integration/test_api_contract.py` | `cd src/backend && just test` | Тестовые фикстуры | подтверждено |
| `D.4` | `B8-4` | Нужен измеримый отчёт по покрытию | `src/backend/justfile`, `src/backend/pyproject.toml` | `cd src/backend && just test-coverage && just show-coverage` | Coverage tool из dev dependencies | подтверждено |
| `3.7` | `B9-1` | Readiness должен быть однозначным и проверяемым | `src/backend/api/urls.py` (`/ready`) | Runtime: `curl -i /ready` после старта | Поднятые cache/db/storage/celery | частично (live-demo) |
| `3.7` | `B9-2` | Нужен отдельный liveness probe | `src/backend/api/urls.py` (`/health`) | Runtime: `curl -i /health` | Поднятый backend | подтверждено |
| `D.5(B9)` | `B9-3` | Без метрик нет наблюдаемости hot-path | `src/backend/config/settings/base.py`, `src/backend/apps/decision/services.py`, `src/backend/api/v1/events/endpoints.py` | Runtime: `curl /metrics` | Поднятый backend | подтверждено |
| `D.5(B9)` | `B9-4` | Неструктурированные логи сложны для алертов и анализа | `src/backend/config/settings/base.py` (json formatter, django-guid) | Запуск в non-debug и просмотр stdout | Конфигурация `DJANGO_DEBUG=false` | подтверждено |
| `D.5(B9)` | `B9-6` | Рост трафика/данных может деградировать latency даже после оптимизаций | `src/backend/apps/decision/services.py`, `src/backend/apps/reports/services.py`, `src/backend/apps/events/tasks.py`, `ADR/04-decisions.md` (P1-P4) | Код-ревью + live-demo под нагрузкой | Сценарий увеличенного трафика и наблюдение очереди/latency | частично (live-demo) |
| `D.5(B9)` | `B9-7` | Без индексов и оптимизаций горячие запросы дорожают | `src/backend/apps/experiments/models.py`, `src/backend/apps/events/models.py`, `src/backend/apps/guardrails/models.py`, `src/backend/apps/notifications/models.py`, `src/backend/apps/learnings/models.py` | Схема моделей и миграций | БД-схема проекта | подтверждено |
| `D.5(B10)` | `B10-1` | Отсутствие автоматического линтинга снижает качество | `src/backend/justfile`, `src/backend/pyproject.toml`, `.gitlab-ci.yml` | `cd src/backend && just lint` | Dev dependencies | подтверждено |
| `D.5(B10)` | `B10-2` | Отсутствие форматирования повышает шум в diff | `src/backend/justfile`, `src/backend/pyproject.toml`, `.gitlab-ci.yml` | `cd src/backend && just format` | Dev dependencies | подтверждено |
| `7` | `FX-1` | Без рабочего сценария уведомления бесполезны | `src/backend/apps/notifications/*`, `src/backend/api/v1/notifications/*` | `cd src/backend && uv run python manage.py test apps.notifications.tests.test_notifications api.v1.notifications.tests.test_notifications_api` | Notification channels/rules/log fixtures | подтверждено |
| `7` | `FX-2` | Нужны явные ограничения по каналам и шумоподавлению | `ADR/07-simplifications.md`, `ADR/04-decisions.md`, `src/backend/apps/notifications/services.py` | Сверка docs и реализации rate-limit/dedup | Документация + код notifications | подтверждено |
| `9` | `FX-1` | Без рабочего knowledge base повторяются эксперименты | `src/backend/apps/learnings/*`, `src/backend/api/v1/learnings/*` | `cd src/backend && uv run python manage.py test apps.learnings.tests.test_learnings api.v1.learnings.tests.test_learnings_api` | Learning fixtures, experiments, tags | подтверждено |
| `9` | `FX-2` | Нужны явные ограничения алгоритма похожести | `ADR/07-simplifications.md`, `ADR/04-decisions.md`, `src/backend/apps/learnings/services.py` | Сверка docs и реализации similarity scoring | Документация + код learnings | подтверждено |
| `11` | `FX-1` | Без резолва конфликтов метрики неинтерпретируемы | `src/backend/apps/conflicts/*`, `src/backend/api/v1/conflicts/*`, `src/backend/apps/decision/services.py` | `cd src/backend && uv run python manage.py test apps.conflicts.tests.test_conflicts api.v1.conflicts.tests.test_conflicts_api` | Эксперименты в доменах с policy `mutual_exclusion/priority` | подтверждено |
| `11` | `FX-2` | Нужны явные границы конфликтных политик | `ADR/07-simplifications.md`, `ADR/04-decisions.md`, `src/backend/apps/conflicts/services.py` | Сверка docs и `resolve_domain_conflict` | Документация + код conflicts | подтверждено |