Перейти к содержанию

Кейсы (case-aware workflows)

Case — project-scoped сущность-носитель бизнес-кейса (клиент, сделка, заявка, тикет), вокруг которого крутятся workflow-запуски. Хранит идентификаторы (identity_keys), произвольные свойства (properties), теги, ссылки на внешние сущности (Telegram chat, email thread, Google Drive folder), статус (active/archived). Сами файлы Axon durably не хранит — доступ к содержимому идёт через connector layer по ссылкам из properties.file_sources. Канон: CONCEPT-CASE-WORKFLOWS.md §3-§4.


1. Что это и зачем

Workflow в Axon — единица исполнения; case — единица контекста. Один и тот же workflow customer_support_triage запускается на разных кейсах (case_001 = клиент A, case_002 = клиент B), а кейс собирает на себе все связанные run'ы, артефакты, переписку, файлы. Это даёт:

  • Историю по сущности. Все runs WHERE case_id = ? — что когда-либо делалось для этого клиента.
  • Идентичность. identity_keys (например, {"email": "x@y.com"} или {"crm_id": "CRM-123"}) — детерминистический ключ дедупликации: входящий ивент (новое письмо, обращение) находит существующий кейс по identity_keys, не плодит дубликаты.
  • Контекст для шагов. Step в workflow может читать case.properties (свойства), case.external_refs (привязки), case.properties.file_sources (где лежат файлы) — это «база знаний» по кейсу.
  • Case-level overrides коннекторов (D17 средний слой). Например, project-wide binding profile говорит «slot gmail_inbox → инстанс gmail_support», но конкретный кейс может переопределить: «именно для этого VIP-клиента — gmail_vip». Run-level override (третий слой) перебивает case-level.

Канонический рассказ — CONCEPT-CASE-WORKFLOWS.md. Этот мануал описывает UI и операционный путь.


2. Роли и доступ

Из core/models/auth.py. Полная матрица — Roles-And-Permissions.md.

Действие Permission owner admin manager operator reviewer read_only system
Создать кейс create_case
Редактировать properties / tags / identity_keys edit_case
Архивировать / разархивировать кейс archive_case
Привязать external ref (Telegram chat, email thread, GDrive folder) attach_case_external_ref
Удалить external ref delete_case_external_ref
Запустить workflow для кейса (Run Wizard от кейса) start_workflow
Читать кейс read

Важно: manager имеет attach_case_external_ref, но не имеет delete_case_external_ref — удаление привязки (потенциально разрушительная операция, теряется контекст) поднято до admin/owner. Operator — только чтение и запуск run'ов в рамках своих project_scopes.

Все мутации идут через CommandEnvelope через канонические handlers в core/api/commands/handlers/cases.py (CreateCaseHandler, UpdateCasePropertiesHandler, UpdateCaseStatusHandler, ArchiveCaseHandler, AttachExternalRefHandler, DeleteExternalRefHandler). Backdoor writes запрещены (см. ARCHITECTURE-V6 §17 RBAC, инвариант 5 VISION).


3. Где это в Console

Раздел сайдбара Cases в каждом проекте (URL: /{project_id}/cases).

⚠️ Раздел скрыт за фича-флагом VITE_CONSOLE_STAGE3_UI_ENABLED. Сейчас флаг не прокинут в console Dockerfile build args — в стандартном образе раздел не виден. Включается build-time: --build-arg VITE_CONSOLE_STAGE3_UI_ENABLED=true (или runtime через CONSOLE_STAGE3_UI_ENABLED=true, если console-сервер это поддерживает). См. §8 Траблшутинг.

Экран Что на нём
Cases list (/{project_id}/cases) таблица: case_id префикс, display_name, case_type, status (active/archived), tags, updated_at. Фильтры: по case_type, по статусу, поиск по identity_keys (через GIN-индекс). Кнопка + New case.
Case create (/{project_id}/cases/new) форма: case_type (snake_case, ^[a-z][a-z0-9_]*$), display_name, identity_keys (JSON), tags (массив), properties (JSON textarea). После создания — редирект на detail.
Case detail (/{project_id}/cases/{case_id}) вкладки/секции: Identity (case_id, type, identity_keys, status), Properties (read-only JSON view), Tags, External refs (список Telegram chat / email thread / GDrive folder / др. с кнопкой Detach для admin/owner), Connector overrides (D17, picker если есть workflow с binding profile), File sources (свойство properties.file_sources — список FileSourceRef), Workflow runs (все runs, у которых case_id = текущий), Actions: Edit, Archive, Start workflow (открывает Run Wizard в режиме «от кейса» — case_id подставлен).
Case edit (/{project_id}/cases/{case_id}/edit) редактирование display_name, properties (JSON), tags, identity_keys. Изменения идут через update_case_properties (для properties/tags/identity_keys/display_name).

JSON-редактор properties — обычный textarea с валидацией формы (parseJsonObject / formatJson utility). Никакого schema-driven form builder в текущей версии нет — задача владельца проекта валидировать форму свойств в коде (publish-time input_schema валидатор для case-aware workflows проверяет, что case.properties соответствует ожидаемой shape).


4. Концепции (mental model)

Project ── 1 : N ── Case ── 1 : N ── Workflow run (case_id FK)
                     ├── identity_keys (JSON)   ─── детерминистическая дедупликация
                     ├── properties     (JSON)   ─── произвольные поля + file_sources
                     ├── tags           (array)  ─── ярлыки для фильтрации
                     ├── external_refs  (1:N)    ─── Telegram chat, email thread, GDrive folder
                     ├── connector_overrides (D17) ─── case-level override binding profile
                     └── status: active | archived

identity_keys — JSON-объект, например {"email": "lead@acme.com"} или {"crm_id": "CRM-123", "channel": "telegram"}. Уникальность опциональная (зависит от типа кейса). Поиск по identity_keys использует GIN-индекс cases_identity_keys_gin_idx. Ивент-роутер при входе пытается найти кейс по identity_keys прежде чем создать новый.

properties — произвольный JSON object под бизнес-данные кейса. Свободная форма, но есть convention:

  • properties.file_sources: list[FileSourceRef] — стандартный способ объявить, где лежат файлы кейса (Google Drive folder, Dropbox, и т.д.). См. §6.2. Сами файлы Axon не хранит — только ссылку и connector binding. Доступ к содержимому — через connector layer (Google Drive adapter и т.д.).
  • properties.connector_overrides: dict[str, str] (D17 средний слой) — case-scope override binding profile проекта. Формат {"requirement_key": "ci_<ulid>"}. Валидируется shape-only в validate_case_properties_connector_overrides() (имя слота + формат ci_<ulid>); семантическая проверка (CI существует, того же проекта, usable) выполняется на start_workflow через binding resolver (D45 защита в глубину).

tags — массив строк, чисто UI-overlay для фильтрации (по аналогии с workflow labels, см. CONCEPT-WORKFLOW-LABELS.md §1). Execution layer не читает tags, не маршрутизирует по ним и не меняет поведение.

status — два значения: active / archived. Архивация — это status transition, не удаление; кейс остаётся в БД для аудита, истории и compliance. Архивированные кейсы скрыты в UI по умолчанию, но доступны через фильтр статуса.

external_refs — отдельная таблица (1:N к case). Каждая запись: external_ref_id (префикс xref_), kind (telegram_chat / email_thread / gdrive_folder / др.), ref_value (опаковый id у провайдера), display_name. Используется, чтобы связать кейс с внешней сущностью без проникновения в properties (схема + права на удаление под admin/owner).

case_aware workflows. Workflow с case_aware=true (флаг в metadata) обязательно стартует с case_id. Publish-time валидатор проверяет, что определение объявляет case_aware корректно и что input_schema совместима с case-aware shape. Run Wizard в этом режиме требует выбор кейса перед Create + Start. См. WORKFLOW-ARCHITECTURE.md §4.


5. Флоу: пошаговые сценарии

5.1 Создать кейс вручную

  1. manager+ идёт в Cases → + New case.
  2. Заполняет case_type (snake_case, например customer_lead / support_ticket), display_name, опционально identity_keys (JSON), tags, properties. Совет: заполняй identity_keys сразу — это ключ дедупликации; пустой объект разрешён, но тогда ивент-роутер не сможет найти кейс при следующем входящем ивенте.
  3. Submit → CreateCaseHandler (create_case, sync_apply, internal) → INSERT в app.cases, генерируется case_id = case_<ulid>. Outbox-событие case.created (для ивент-роутеров и read-projection rebuild). Редирект на detail.

5.2 Запустить workflow для кейса (Run Wizard от кейса)

  1. На Case detailStart workflow. Открывается Run Wizard, case_id уже подставлен и заблокирован для смены.
  2. Выбираешь WorkflowDefinition (среди опубликованных case-aware definitions проекта).
  3. Заполняешь input_data (форма строится по input_schema definition'а).
  4. Опционально override bindings (run-level, перебивает case-level и project-level): для multi-credential workflow выбираешь конкретный CI на каждый connector_requirement.
  5. Create + Startcreate_workflow + start_workflow (два command'а). На start_workflow system actor workflow_start_dispatcher (sub-step pin_workflow_run_bindings) пиннит финальный snapshot bindings: профиль → case_overrides → run_overrides. Snapshot сохраняется в app.workflow_run_bindings_snapshot и читается каждым step'ом при исполнении.

5.3 Привязать external ref (Telegram chat, email thread, GDrive folder)

  1. На Case detail → External refs → + Attach.
  2. Выбираешь kind (telegram_chat, email_thread, gdrive_folder, ...), вводишь ref_value (chat id / thread id / folder URL), display_name.
  3. Submit → AttachExternalRefHandler (attach_external_ref, sync_apply). Запись в case_external_refs, outbox-событие case.external_ref_attached.
  4. Для GDrive: используй удобный URL — parse_gdrive_folder_url() извлечёт folder_id. Это не подменяет properties.file_sources (это разные слои: external_refs — общая привязка, file_sources — конвенция для коннекторного слоя).

5.4 Архивировать кейс

  1. На Case detail → Actions → Archive (или массово через Cases list, если поддержано).
  2. Подтверждение → ArchiveCaseHandler (archive_case, sync_apply). UPDATE app.cases status = 'archived', archived_at = now(), archived_by_actor_id. Canonical timestamp — в audit.audit_log (event_type='case.archived').
  3. Что происходит с активными runs кейса: активные runs не отменяются автоматически. Архивация — это бизнес-сигнал «кейс завершён», а не системный teardown. Если нужно остановить runs — это отдельные cancel_workflow команды (по списку run'ов кейса). Это сознательное разделение: undo и status transitions — независимые контракты (VISION инвариант 3).
  4. Разархивация: через update_case_status(status='active') тем же handler-ом (UpdateCaseStatusHandler), если это разрешено бизнес-политикой; в стандартной поставке archived — terminal в UI, но не в БД.

5.5 Удалить external ref

  1. admin+ идёт Case detail → External refs → ⋯ → Delete (видно только для admin/owner: у manager этой кнопки нет — RBAC enforced server-side + UI-gate).
  2. Подтверждение → DeleteExternalRefHandler (delete_external_ref, sync_apply).

6. Справочник опций

6.1 Поля кейса (app.cases)

Поле Тип Описание
case_id text PK префикс case_, формат case_<26-char ULID, Crockford>. CHECK constraint: case_id ~ '^case_[0-9a-hjkmnp-tv-z]{26}$'.
project_id text FK NOT NULL; cross-project leak запрещён на уровне БД (FK + composite checks).
case_type text snake_case, 1-64 символа, ^[a-z][a-z0-9_]*$. Свободный business-driven type.
display_name text человекочитаемое имя.
identity_keys jsonb объект (jsonb_typeof = 'object'), default {}. Идентичность кейса; ключ дедупликации; GIN-индекс по полю.
properties jsonb произвольный object; convention поля — file_sources и connector_overrides (см. ниже).
tags text[] UI overlay.
status text 'active' или 'archived' (CHECK constraint).
archived_at / archived_by_actor_id timestamp / text заполняются при archive_case.
created_at / created_by_actor_id timestamp / text audit fields.
updated_at / updated_by_actor_id timestamp / text audit fields.
version bigint optimistic concurrency (используется expected_version в CommandEnvelope).

6.2 FileSourceRef (convention properties.file_sources[*])

Источник: core/models/file_sources.py.

Поле Тип Описание
kind 'gdrive_folder' \| ... тип источника; должен соответствовать adapter_key привязанного коннектора
display_name str (1-255) человекочитаемое имя для UI
folder_id str provider-specific id (для GDrive — folder ID)
folder_url str (опц.) удобный URL; валидация проверяет, что folder_id соответствует URL (_folder_id_matches_url)

Хелпер parse_gdrive_folder_url() извлекает folder_id из URL вида https://drive.google.com/drive/folders/<id> (в т.ч. с query-параметрами).

Файлы Axon durably не хранит. FileSourceRef — это ссылка на folder у провайдера. Содержимое читается коннектором (Google Drive adapter и т.д.) в момент исполнения шага workflow. Удаление кейса не удаляет файлы у провайдера; удаление папки у провайдера не отражается в Axon (нужен health-probe на стороне коннектора).

6.3 External ref kinds

case_external_refs.kind — открытое множество строк (без CHECK enum), но в текущей версии используются: telegram_chat, email_thread, gdrive_folder, slack_channel (если slack-коннектор активен). Новые kind'ы добавляются по мере появления адаптеров.

6.4 connector_overrides (D17 case-level средний слой)

case.properties.connector_overrides: dict[str, str] (опционально).

Формат: {"<requirement_key>": "ci_<ulid>"}. Каждый ключ — имя слота из WorkflowDefinition.connector_requirements[*].requirement_key; значение — connector_instance_id (см. Connectors-Credentials.md §6.2).

Shape валидируется в validate_case_properties_connector_overrides() при update_case_properties (имя слота non-empty, ID соответствует формату ci_<26-char ULID>). Семантическая проверка (CI существует, того же проекта, usable — D42) выполняется binding resolver-ом на start_workflow через resolve_workflow_run_bindings() (D45 defense-in-depth, не доверяет валидаторам случая).


7. Жизненный цикл и обслуживание

Статусный автомат: active → archived (archive_case); опционально archived → active (update_case_status, если бизнес-политика позволяет). Canonical timestamp — audit.audit_log event case.archived / case.unarchived.

Активные runs кейса при архивации не отменяются автоматически (см. §5.4).

External refs живут отдельной таблицей; удаление кейса (если допущено — в текущей версии не реализовано, см. §9) каскадно удалит ссылки, но не сами объекты у провайдера.

GDPR-каскад при удалении проекта. При archive_project / project teardown — кейсы проекта получают cascade (audit.case.cascade_archived_by_project); полный erase данных (личной информации в properties / identity_keys) выполняется отдельным GDPR sweeper-ом (см. CONCEPT-CASE-WORKFLOWS.md §7 GDPR helpers — typed DTOs shared с Zone 3).

Read-projection read.case_registry (alembic 035) rebuildable; перестраивается из app.cases outbox-консьюмером (refresh/case_registry.py). При проблемах с UI — axon refresh-read-models case_registry (см. Security-And-Audit.md или CLI).


8. Траблшутинг

Симптом Причина Что делать
Раздел Cases не виден в сайдбаре VITE_CONSOLE_STAGE3_UI_ENABLED не включён на сборке console (Dockerfile default — false) Пересобрать console-образ с --build-arg VITE_CONSOLE_STAGE3_UI_ENABLED=true (см. console/Dockerfile). Если используется runtime override — проверить CONSOLE_STAGE3_UI_ENABLED=true в env.
Кнопка Delete external ref не видна Нет permission delete_case_external_ref (только admin/owner) Эскалировать до admin/owner или использовать archive_case если связь больше не нужна.
update_case_properties возвращает 422 «connector_overrides shape invalid» properties.connector_overrides содержит не-string ключи или value не в формате ci_<26-char ULID> Проверить shape: {"slot_name": "ci_01HXXX..."}. Не путать с cred_<ulid> (это credential_id, не CI).
start_workflow от кейса валится с binding_resolver: case_override references unknown CI Case-level override указывает на удалённый / cross-project / unusable CI Открыть Case detail → Connector overrides, обновить выбор или удалить override (вернётся в project binding profile).
Ивент-роутер плодит дубли кейсов на одинаковый входящий email identity_keys не заполнены / неконсистентны между источниками Заполнить identity_keys = {"email": "<addr>"} для существующих и обновить роутер (или business workflow), чтобы он клал тот же ключ.
Кейс не находится по identity_keys через поиск Console Не использован GIN-индекс — поиск идёт по подмножеству ключей (containment @>) Сейчас Console использует prefix-search по display_name + точный match по identity_keys; для произвольного query — Query API GET /api/v1/query/cases?identity_keys=<json>.
После архивации кейса остались pending approvals от его workflow runs Архивация не отменяет runs (см. §5.4) Отдельно cancel_workflow для каждого run'а либо завершить approvals руками.

9. Ограничения и инварианты

  • project_id NOT NULL. Кейс всегда принадлежит ровно одному проекту. Cross-project FK / refs запрещены на уровне БД и валидируются handler-ом.
  • Файлы не хранятся durably. file_sources — только ссылки; content — у провайдера, доступ через connector layer. Axon не реплицирует и не кэширует содержимое (за исключением transient artefacts конкретного step'а).
  • Mutate только через canonical handlers. CreateCase, UpdateCaseProperties, UpdateCaseStatus, ArchiveCase, AttachExternalRef, DeleteExternalRef. Backdoor SQL UPDATE — нарушение VISION инварианта 5 (CommandEnvelope — единственный путь мутации).
  • identity_keysjsonb_typeof = 'object' (CHECK constraint cases_identity_keys_object). Массивы / скаляры отвергаются.
  • case_type regex ^[a-z][a-z0-9_]*$, 1-64 символа (CHECK constraint cases_case_type_format).
  • status ∈ {'active', 'archived'} (CHECK constraint cases_status_values). Никаких других значений; pending/in_progress/closed/etc. — это бизнес-семантика для workflow runs или для properties.business_status, не для самого кейса.
  • D17 case-level override — only connector_overrides. В текущей версии случай переопределяет binding profile только через properties.connector_overrides shape. Произвольное «case overrides любое поле run'а» — не контракт.
  • Secrets никогда не в properties. core/security/credential_scan.py отвергает update_case_properties если properties содержит секрето-подобные строки (токены/ключи/credential URLs). Это инвариант: case — публичный бизнес-контекст, не credential store.
  • Удаление кейса (hard delete) — beyond Stage 3. В текущей версии есть только archive (soft transition). Hard delete + GDPR purge — отдельный backlog (см. CONCEPT-CASE-WORKFLOWS.md §7 + OWNERSHIP-MAP Zone 5 sweepers).

10. Связанные мануалы и каноны

  • Workflows.md — определения / Run Wizard / binding profiles; case-aware workflows запускаются именно отсюда.
  • Connectors-Credentials.md — connector instances; case-level overrides (D17) указывают на CI.
  • Approvals.md — как утверждать шаги в workflow run'е кейса.
  • Undo-And-Compensation.md — что значит «отменить шаг» в контексте case-aware workflow.
  • Security-And-Audit.md — audit-events case.created / case.archived / case.external_ref_*.
  • Каноны: CONCEPT-CASE-WORKFLOWS.md §3 (case как атом контекста), §4 (file_sources convention), §7 (GDPR), §10.2 (FileSourceRef); ARCHITECTURE-V6.md §11 (cases ↔ connector layer); WORKFLOW-ARCHITECTURE.md §4 (case-aware workflows).