---
name: bizagi-bpm
description: >
  Generate Bizagi Modeler .bpm files (XPDL 2.2 + BPSim 1.0) from a natural-language process description.
  Use this skill whenever the user wants to: create a Bizagi diagram, generate a .bpm file,
  build a BPMN process model, complete ПР-5 or ПР-6 academic assignments, add BPSim
  simulation scenarios (AS-IS / TO-BE), add a DataStore with fields and sample rows,
  add a SubProcess, or attach a file to a diagram element. Also use it when the user says
  "сделай диаграмму", "создай .bpm", "bizagi", "BPMN процесс", "ПР-5", "ПР-6",
  "симуляция КАК ЕСТЬ / КАК ДОЛЖНО БЫТЬ". Even if the user just mentions "бизнес-процесс"
  and wants a file, use this skill - it handles all Bizagi-specific complexity automatically.
---

# Bizagi BPM Skill v4.5

**Релиз 2026-05-26.** Скилл валидирован end-to-end:
- Сгенерированный BPSim побайтно совпадает с каноном студентов
- Resource Analysis запускается в Bizagi UI на сгенерированных файлах
- Слепые тесты субагентами: 10+ файлов, все прошли с первого захода
- Финальный стресс-тест: 5 lanes на 3-way разветвление, 35 активностей через helper-формулы
- Layout: блоки выровнены по дорожкам с точностью 0 px (`LANE_HEADER=54`)
- XML-escape, multi-.diag, Participants, attachments, BPSim - всё работает

---

## КРИТИЧЕСКОЕ правило: вертикальные стрелки между lanes

**Чтобы стрелка между двумя lanes была строго вертикальной (без зигзагов от
auto-router'а Bizagi), элементы-источник и приёмник должны иметь ОДИНАКОВЫЙ X.**

Если X разные на 50+ px - Bizagi нарисует стрелку вправо-вниз-влево (зигзаг),
что выглядит криво. На одинаковом X стрелка идёт прямо вниз.

```python
# ПРАВИЛЬНО:
activity(T_SEND, "Подать заявку", "UserTask",
         L_EMP, 200, y_in_lane(Y_EMP, H_EMP, 70), *SIZE_TASK)
activity(T_CHECK,"Проверить лимит","ServiceTask",
         L_HR,  200, y_in_lane(Y_HR,  H_HR,  70), *SIZE_TASK)
# T_SEND -> T_CHECK идёт строго вниз.

# НЕПРАВИЛЬНО (зигзаг):
activity(T_SEND, ..., L_EMP, 200, ..., *SIZE_TASK)
activity(T_CHECK,..., L_HR,  400, ..., *SIZE_TASK)   # X=400 - крюк
```

---

## ОБЯЗАТЕЛЬНО: 3 предупреждения в финальном сообщении пользователю

После каждой успешной генерации `.bpm` файла **необходимо включить в финальное
сообщение пользователю эти 3 пункта** (адаптируя имена под конкретный кейс):

> 1. **Прикреплённый документ.** В файле модели находится встроенный документ.
>    Чтобы его найти и открыть в Bizagi - **полный путь** (4 координаты):
>    - Имя файла: `<имя.docx>`
>    - **Закладка диаграммы**: «<Имя видимого Pool>»
>    - **Дорожка (Lane)**: «<Имя lane где лежит элемент>»
>    - **Элемент**: «<Имя активности>»
>    - **Шаги**: переключитесь в **Modeling view** (НЕ Simulation), щёлкните правой
>      кнопкой по нужному элементу -> **«Прикреплённые файлы»** (Attached files) ->
>      `<имя файла>` -> «Открыть» (или «Сохранить как»).
>
> 2. **Как запустить симуляцию.** Используйте **Process Simulation -> Start**
>    (зелёный треугольник в верхнем левом углу режима симуляции). НЕ
>    используйте «What-If Analysis» - это инструмент сравнения уже запущенных
>    сценариев, а не сам запуск; симуляция через него выглядит как «всегда
>    падающая». Сценарии («Как есть» / «Как должно быть») привязаны к
>    закладке `<имя главной диаграммы>`.
>
> 3. **Ручные правки.** Если будете изменять диаграмму в Bizagi UI и сохранять
>    её - **повторный запуск `gen.py` перезапишет файл**, ручные правки пропадут.
>    Либо больше не запускайте генератор, либо переносите свои правки в код.

Без этих 3 предупреждений пользователи теряют время на «почему симуляция не
работает» (What-If), не могут найти прикреплённый файл (даже зная имя элемента),
и удивляются что правки в UI пропали после повторной генерации.

---

## Несколько диаграмм в одном .bpm

Bizagi требует архитектуру **1 видимая диаграмма = 1 inner .diag** в outer `.bpm`-zip.
Есть 3 способа сделать «несколько диаграмм»:

### Способ 1 - `write_bpm()` для каждой = отдельный .bpm файл

Для ПР-5 (2 разные диаграммы) самый простой путь: два `write_bpm()` с разными
именами файлов. Подгружать в Bizagi нужно каждый по отдельности.

### Способ 2 - `write_bpm()` + SubProcess inline (свёрнутый подпроцесс)

В главной диаграмме `activity(..., "SubProcess", sub_ref=SUB_PROC_ID)` +
дополнительные `workflow_process(SUB_PROC_ID, ...)` блоки в том же `.bpm`.
В UI - **одна закладка**, подпроцессы раскрываются двойным кликом по
shell-активности.

> ⚠️ **ВАЖНО: НЕ создавайте `pool_main()` для subprocess'а.**
> SubProcess WorkflowProcess добавляется ТОЛЬКО в `wf_xml` блок `make_package()`,
> и НЕ должен иметь своего `pool_main()` в `pools_xml`. Иначе Bizagi отрисует
> его как extra Pool сверху в той же диаграмме - получите «наложение дорожек»
> и укороченную lane (её ширина зависит от содержимого подпроцесса).
>
> ```python
> # ПРАВИЛЬНО:
> pools = "\n".join([
>     pool_empty(PK_EMPTY, WF_EMPTY, POOL_W, TOTAL_H),
>     pool_main(PK_MAIN, "Главный процесс", WF_MAIN, 30, 30, POOL_W, TOTAL_H, lanes),
> ])  # ← всего 2 Pool, без отдельного pool_main для SUB
> wf_xml = "\n".join([
>     empty_workflow_process(WF_EMPTY),
>     workflow_process(WF_MAIN, "Главный", acts_main, trans_main, arts, assocs),
>     workflow_process(WF_SUB,  "Подпроцесс", acts_sub, trans_sub),   # ← только в wf_xml
> ])
> diag_xml = make_package(pkg_id, "Главный процесс", pools, wf_xml)
>
> # НЕПРАВИЛЬНО (наложение дорожек):
> pools = "\n".join([
>     pool_empty(...),
>     pool_main(PK_MAIN, "Главный процесс", WF_MAIN, ...),
>     pool_main(PK_SUB,  "Подпроцесс",      WF_SUB, ...),   # ← лишний Pool, ошибка!
> ])
> ```

### Способ 3 - `write_bpm_multi()` - несколько настоящих закладок

Когда требуется несколько верхнеуровневых процессов как самостоятельные
закладки в UI (каноническая архитектура студенческих ПР-6).

```python
ID_MAIN = g();  ID_SUB1 = g();  ID_SUB2 = g()

main_diag = make_package(ID_MAIN, "Главный процесс", main_pools, main_wfs)
sub1_diag = make_package(ID_SUB1, "Подпроцесс А",    sub1_pools, sub1_wfs)
sub2_diag = make_package(ID_SUB2, "Подпроцесс Б",    sub2_pools, sub2_wfs)

write_bpm_multi("MyProject.bpm", [
    {"pkg_id": ID_MAIN, "diag_xml": main_diag, "bpsim_xml": main_bpsim,
     "attachments": [(docx_path, attach_elem_id)]},
    {"pkg_id": ID_SUB1, "diag_xml": sub1_diag},
    {"pkg_id": ID_SUB2, "diag_xml": sub2_diag},
], out_dir=OUT, participants=participants)
```

Первая в списке - selected по умолчанию (можно переопределить
`"is_selected": True`).

### Что НЕ работает

Несколько ВИДИМЫХ Pool в один `.diag` через несколько `workflow_process()` без
`sub_ref` - Bizagi выдаёт **EmptyDocumentException**. Используйте один из 3
способов выше.

---

## Participants - имена ресурсов в Resource Analysis

Без `participants=` ресурсы в Excel-отчёте показываются как
`Resource_1`/`Resource_2`. С ним:

```python
participants = [
    {"id": "Resource_1", "name": "Менеджер по работе с клиентами",
     "desc": "Регистрирует заявку и обрабатывает её."},
    {"id": "Resource_2", "name": "ML-классификатор",
     "desc": "Автоматически определяет категорию запроса."},
]
write_bpm("file.bpm", diag_xml, bpsim_xml, out_dir=OUT, participants=participants)
# Или: write_bpm_multi(..., participants=participants)
```

`id` должен совпадать с тем что используется в `sim_resource(res_id, ...)` и
`sim_task(..., res=res_id)`.

---

## Bundled library — копирование и валидация версии

`scripts/bizagi.py` - полный XPDL 2.2 + BPSim 1.0 generator.

### Правильный порядок копирования (важно — не пропускайте!)

Старая `bizagi.py` в рабочей папке (или Python-кэш в `__pycache__`) - частая
причина странных ошибок типа «функция `write_bpm_multi` не найдена». Делайте
**всегда из бандла поверх**:

```bash
# 1. Скопировать ПОВЕРХ из бандла - бандл всегда свежее локальной копии
cp "$SKILL_DIR/scripts/bizagi.py" "$OUT_DIR/bizagi.py"

# 2. Удалить Python-кэш (иначе reload подхватит старый байткод)
rm -rf "$OUT_DIR/__pycache__"
```

### Валидация версии — обязательно перед написанием gen.py

Не запуская Python, проверьте через `grep` что в скопированном файле
присутствуют ключевые символы текущей версии. Каждая команда **должна вернуть
≥ 1**:

```bash
grep "^__version__"                "$OUT_DIR/bizagi.py"   # должно показать v5.5+
grep -c "^def _validate_transitions" "$OUT_DIR/bizagi.py" # runtime-валидация (v5.5+)
grep -c "^def write_bpm_multi"     "$OUT_DIR/bizagi.py"   # multi-diag (v3.7+)
grep -c "LANE_HEADER = 54"         "$OUT_DIR/bizagi.py"   # точное центрирование (v3.13+)
grep -c "^def make_participants_xml" "$OUT_DIR/bizagi.py" # имена ресурсов (v3.7+)
grep -c "^SIZE_TASK"               "$OUT_DIR/bizagi.py"   # константы размеров (v3.12+)
grep -c "^def y_in_subrow"         "$OUT_DIR/bizagi.py"   # подряды в lane (v3.12+)
wc -l "$OUT_DIR/bizagi.py"                                # должно быть ~1600+ строк
```

Если хоть один `grep -c` вернул `0` или `wc -l` < 1500 - файл старый
(возможно из предыдущего проекта или обрезан). Пересоздайте копированием из
`$SKILL_DIR/scripts/bizagi.py` ещё раз, проверив что бандл сам не битый
(`wc -l "$SKILL_DIR/scripts/bizagi.py"` тоже ~1600+).

**Доп. контроль на runtime:** при `import bizagi` библиотека печатает
`bizagi.py loaded — v5.5` в stderr. Если ты видишь меньшую версию или
строки нет вообще — это старая копия. Удали `__pycache__/`, перекопируй,
проверь grep ещё раз.

### Если __pycache__ залочен ОС и не удаляется

В крайнем случае - **скопировать `bizagi.py` в свежую папку** и работать оттуда:

```bash
mkdir -p /tmp/bizagi_fresh && cp "$SKILL_DIR/scripts/bizagi.py" /tmp/bizagi_fresh/
# Запускать gen.py с PYTHONPATH=/tmp/bizagi_fresh или скопировать gen.py туда же.
```

## Справочник THEORY.md - когда обращаться

Рядом со SKILL.md лежит `THEORY.md` - краткий справочник BPMN-нотации в
реализации Bizagi Modeler 3.3. Используйте его когда:
- Пользователь спрашивает про конкретный элемент или иконку.
- Возникает вопрос про семантику симуляции (LevelOne/Three/Four,
  TriangularDistribution, Wait vs Processing).
- Нужно подсказать пользователю где в Bizagi UI настраивается то или иное.
- Возникает ошибка валидации Bizagi.

---

## Step −1 - Проверить часовой пояс ДО всего остального

**КРИТИЧНО:** делай это в самом начале, до сбора требований и до любого кода.
Иначе на финальной стадии застрянешь на вопросе «какой у тебя пояс?», и если
пользователь к тому моменту уже ушёл — `.bpm` уедет на N часов, и пользователь
увидит расхождение в Истории обновлений Bizagi.

**Сделай это сразу после копирования `bizagi.py` в рабочую папку:**

```bash
python -c "from datetime import datetime; t = datetime.now().astimezone(); off = t.utcoffset().total_seconds()/3600; print(f'TZ_OFFSET_HOURS={off:+g}')"
```

- Если вывод `TZ_OFFSET_HOURS=+3` (или другое ненулевое число) — пояс ОК,
  ничего у пользователя не спрашиваешь, переходишь к Step 0.
- Если вывод `TZ_OFFSET_HOURS=+0` — окружение в UTC. **СПРОСИ СЕЙЧАС:**

  > Я запускаюсь в окружении с UTC — Bizagi после Save запишет твоё локальное
  > время, а мой `.bpm` уедет на N часов. Уточни, какой у тебя часовой пояс?
  > Если в Москве — просто скажи «MSK» или «3». Я предположил [MSK / Алматы /
  > Берлин — выбери по языку переписки] — подтверди или поправь.

  Пока ждёшь ответ — собирай требования Step 0. Если пользователь ушёл и не
  ответил — поставь `bizagi.set_timezone(3)` (MSK как дефолт для русскоязычных)
  и в финальном сообщении явно укажи: «Поставил MSK, если другой — переустанови».

**После того как пояс известен — обязательно вставь в начало `gen.py`:**

```python
import bizagi
bizagi.set_timezone(3)   # число часов от UTC; принимает также "Europe/Moscow"
```

Подробности о `set_timezone` — в разделе ниже про Step 0 → «⏰ Часовой пояс».

---

## Step 0a — Обязательный первый вызов в gen.py

С **v5.5** `assert_version()` обязателен. Без него `write_bpm()` и
`write_bpm_multi()` кидают `RuntimeError`. Физически не получится собрать
.bpm пока локальная `bizagi.py` не задекларирована совместимой.

В САМОМ НАЧАЛЕ `gen.py` всегда:

```python
import bizagi
bizagi.assert_version("5.5")     # ← обязательно, без него write_bpm() падает
bizagi.set_timezone(3)           # пояс пользователя (если UTC; число часов от UTC)
```

Что будет при разных ошибках:

| Что произошло | Что увидишь | Что делать |
|---|---|---|
| `bizagi.py` старая (нет `assert_version`) | `AttributeError: module 'bizagi' has no attribute 'assert_version'` | перекопируй свежую из `$SKILL_DIR/scripts/bizagi.py`, удали `__pycache__/` |
| `bizagi.py` версия меньше требуемой | `RuntimeError: bizagi.py версии X.Y устарела, требуется ≥ 5.5` | то же |
| Забыл вызвать `assert_version` | `RuntimeError: write_bpm() требует чтобы ты сначала задекларировал версию` | добавь `bizagi.assert_version("5.5")` в начало `gen.py` |

Бамп версии в новых релизах bizagi.py будет требовать обновить число в
`assert_version("...")` — это явный change-detector: если новая фича требует
новой версии, и ты забыл бампнуть число, build упадёт. Если же забыл
обновить локальную `bizagi.py` — тоже упадёт.

---

## Step 0 - Уточнить требования

| # | Вопрос | Зачем |
|---|--------|-------|
| 1 | Название процесса и описание | Pool label, Scenario description |
| 2 | Количество диаграмм | ПР-5 требует 2 |
| 3 | Lanes per diagram - роли/системы | 2-4 lane рекомендованы |
| 4 | Activities - имена, типы, порядок | Содержимое диаграммы |
| 5 | DataStore - какие данные хранятся | Требование ПР-5 п.6 |
| 6 | SubProcess - какой шаг разворачивается | Требование ПР-5 п.5 |
| 7 | Путь к прикрепляемому файлу | Требование ПР-5 п.2 |
| 8 | BPSim: длительности задач, ресурсы, probability на gateway | Требование ПР-6 |
| **9** | **Windows-логин пользователя** (короткое латинское имя) | глобалы AUTHOR/USER/SCENARIO_AUTHOR |
| **10** | **Часовой пояс пользователя** | ModifiedDate в ModelInfo.xml |

Установите глобалы в начале скрипта. **Используйте одно и то же английское имя в нижнем регистре во всех трёх местах** — без фамилии и отчества. Это совпадает с тем, как Bizagi берёт имя пользователя из Windows (типично `user`, `admin`, `ivanov`, `sergey` — короткое, lowercase, latin):

```python
import bizagi
bizagi.SCENARIO_AUTHOR = "sergey"   # в BPSim Scenario / Author
bizagi.AUTHOR          = "sergey"   # в Package <Author>
bizagi.USER            = "sergey"   # Bizagi username + папка Users/sergey/
```

Дефолтное значение — `"User"` (стандартный пользователь домашнего Windows).
Если пользователь не указал имя — оставь дефолт, имя ФИО кириллицей в эти
поля не пиши: Bizagi их использует как технический идентификатор учётной
записи, а не как «автор для отчёта».

### ⏰ Часовой пояс (timestamp в «Истории обновлений»)

Bizagi показывает «Историю обновлений» по полю `ModifiedDate` в `ModelInfo.xml`.
Скилл по умолчанию берёт **системный пояс машины** через `datetime.now().astimezone()`,
поэтому на ноутбуке пользователя обычно ничего настраивать не нужно.

**Но:** если `bizagi.py` запускается в окружении с поясом UTC (типичные песочницы,
Docker, WSL, CI) — на import печатается WARNING. В этом случае:

**Самый простой способ — передать число (часов от UTC):**

```python
import bizagi
bizagi.set_timezone(3)       # MSK / Минск
bizagi.set_timezone(2)       # Калининград / Берлин
bizagi.set_timezone(5)       # Екатеринбург / Алматы
bizagi.set_timezone(-5)      # New York
bizagi.set_timezone(5.5)     # India
```

Агент САМ выбирает число на основе языка переписки и контекста:
- русский → `3` (MSK), если не указано иное
- казахский → `5` (Алматы)
- украинский → `2` (Киев)
- иврит → `2` (Иерусалим)
- польский → `1` (Варшава)

Если у пользователя есть DST и важна точность с переходом на летнее время —
передай IANA-имя строкой:

```python
bizagi.set_timezone("Europe/Berlin")     # с учётом DST
bizagi.set_timezone("America/New_York")  # с учётом DST
```

Также принимаются `"+03:00"`, `"+0300"`, `"UTC+3"`, `"GMT-5"`.

Если пользователь даже после уточнения не отвечает — поставь число `3` (MSK)
как разумный дефолт для русскоязычной аудитории, но в финальном сообщении
обязательно скажи: «Поставил MSK; если у тебя другой пояс — `bizagi.set_timezone(N)` и пересобери файл».

---

## Step 1 - Layout

### Размеры элементов и стартовая координата

Константы из `bizagi.py`:

```python
from bizagi import SIZE_EVENT, SIZE_TASK, SIZE_GATE, SIZE_SUB, START_X
# SIZE_EVENT = (36, 36)    # StartEvent / EndEvent
# SIZE_TASK  = (150, 70)   # UserTask / ServiceTask
# SIZE_GATE  = (50, 50)    # ExclusiveGateway / ParallelGateway
# SIZE_SUB   = (160, 80)   # SubProcess shell
# START_X    = 150         # безопасный стартовый X
```

Распаковка в `activity()`:
```python
activity(I['se'], "Начало", "StartEvent",
         L_CLI, START_X, y_in_lane(0, H_CLI, 36), *SIZE_EVENT)
activity(I['t1'], "Задача", "UserTask",
         L_CLI, x,       y_in_lane(0, H_CLI, 70), *SIZE_TASK)
```

### Lanes

- Высота lane: **150-220 px** если 1 ряд; **260-320 px** если 2-3 ряда
  (например split-GW + ветки).
- Lanes стекаются вертикально с y=0:
  `lane[0].y=0, lane[1].y=lane[0].h, lane[2].y=lane[0].h+lane[1].h, ...`
- x lane всегда **50** (Bizagi отрисовывает label-колонку слева).
- Ширина lane = ширина pool.

### Helper'ы для координат

- **`y_in_lane(lane_y, lane_h, elem_h)`** - Y по центру рабочей области lane.
  Учитывает `LANE_HEADER=54` (неявная полоса сверху в Bizagi UI). Разные по
  высоте элементы (36/50/70) на одной горизонтальной линии получают разный y,
  но визуально идут ровно.
- **`y_in_subrow(lane_y, lane_h, row_idx, n_rows, elem_h)`** - Y в N-м подряду
  lane'а (0-indexed). Используйте когда внутри одной дорожки нужно 2-3
  горизонтальных ряда (split-GW + ветки Да/Нет). 2 ряда -> h ~ 260-280,
  3 ряда -> h ~ 320-360.
- **`x_after(prev_x, prev_w, gap=30)`** - X следующего элемента с зазором.

### Пример канонической цепочки в одной lane

```python
L1 = g()
LANE_H = 160
I = {k: g() for k in ['se','t1','gw','t2','ee']}

x = START_X                                                  # 150
acts = [
    activity(I['se'], "Начало", "StartEvent",
             L1, x, y_in_lane(0, LANE_H, 36), *SIZE_EVENT),
]
x = x_after(x, SIZE_EVENT[0])                                # 216
acts.append(activity(I['t1'], "Задача 1", "UserTask",
            L1, x, y_in_lane(0, LANE_H, 70), *SIZE_TASK))
x = x_after(x, SIZE_TASK[0])                                 # 396
acts.append(activity(I['gw'], "Условие?", "ExclusiveGateway",
            L1, x, y_in_lane(0, LANE_H, 50), *SIZE_GATE))
x = x_after(x, SIZE_GATE[0])                                 # 476
acts.append(activity(I['t2'], "Задача 2", "ServiceTask",
            L1, x, y_in_lane(0, LANE_H, 70), *SIZE_TASK))
x = x_after(x, SIZE_TASK[0])                                 # 656
acts.append(activity(I['ee'], "Конец", "EndEvent",
            L1, x, y_in_lane(0, LANE_H, 36), *SIZE_EVENT))

POOL_W = x + SIZE_EVENT[0] + 50
```

### Pool

- **Pool height** = сумма высот всех lane.
- **Pool width** = `x_last + 50`.

### Визуальные нюансы

1. **Стартовый X ≥ 120 (`START_X=150` безопасно).** Иначе подпись StartEvent
   наезжает на label-колонку дорожки.
2. **Lane с Gateway = h ≥ 200-240.** Подпись gateway отрисовывается ПОД ромбом,
   при длинной фразе вылезает за границу lane.
3. **Поток между lanes - на одинаковом X** (см. КРИТИЧЕСКОЕ правило выше).

---

## ROOT CAUSES (BPSim) - всё автоматически решается в bizagi.py

| # | Причина | Как избежать |
|---|---------|--------------|
| 1 | `xsi:type` на `<ResultRequest>` | `_rr()` пишет plain без атрибутов |
| 2 | `level="LevelFour"` (нужен Calendar) | `make_bpsim(..., level="LevelThree")` - default |
| 3 | `elementRef="<uuid>"` без `Id_` | `sim_*` сами добавляют `Id_<uuid>` |
| 4 | Internals SubProcess в BPSim | В BPSim только shell-активность |
| 5 | StartEvent с `NumericParameter` | `sim_start()` пишет `TriangularDistribution(mode=40, min=30, max=50)` |
| 6 | `<UnitCost>` в Resource | `sim_resource()` не пишет UnitCost |

### Опровергнутые «эмпирические правила» (удалены)

- «Probability одинаковая в AS-IS/TO-BE» - у канонических файлов разные (работает)
- «Orphan-активность как дифференциатор» - ни у кого нет
- «Shell SubProcess одинаковой длительности» - не наблюдается
- «ParallelGateway нестабилен» - был артефактом LevelFour

---

## API quick reference

### activity(id_, name, atype, lane_id, x, y, w, h, sub_ref="", doc="")

| atype | Назначение |
|-------|------------|
| `StartEvent` | Начало процесса (36x36) |
| `EndEvent` | Конец процесса (36x36) |
| `UserTask` | Действие человека |
| `ServiceTask` | Автоматическое действие |
| `SubProcess` | Раскрывается; передать `sub_ref=SUB_PROC_ID` |
| `ExclusiveGateway` | XOR-развилка (50x50) |
| `ParallelGateway` | AND-развилка (50x50) |

Автоматический XML-escape `name` и `doc` (`<`, `>`, `&`, `"`, `'`).

### transition(id_, from_, to_, name="", pts=None)
Sequence flow. `name` - метка на стрелке. `pts` - waypoints.

### lane(id_, name, pool_id, x, y, w, h, doc="")
Lane внутри пула. x=50 всегда. y = сумма высот предыдущих lanes.

### pool_main(pid, name, proc_id, px, py, pw, ph, lanes_xml)
### pool_empty(pid, proc_id, pw, ph)
`pool_empty` обязателен (скрытый «Основной процесс»), иначе EmptyDocumentException.

### workflow_process(proc_id, name, acts, trans, arts=None, assocs=None)
### empty_workflow_process(proc_id)
### make_package(pkg_id, name, pools_xml, wfs_xml)

### artifact(id_, name, x, y, w=50, h=50, doc="", fields=None, rows=None)
DataStore. `fields` = `[{"name":str, "type":str}]` (Integer/Text/Float/DateTime/Boolean).
`rows` = `[{<field>: <value>}]` - 2-3 строки для ПР-5 п.6.

### association(id_, src, tgt, direction="From")
`direction`: «From» - activity пишет в store; «To» - store читается activity.

### BPSim helpers

- `sim_resource(res_id, qty, cost)` - `res_id` строка «Resource_1», `cost` FixedCost в RUB.
- `sim_start(ref, mode=40, min_=30, max_=50, count=80)` - TriangularDistribution + TriggerCount.
- `sim_task(ref, iso, res=None, wait=None)` - ProcessingTime + опц. WaitTime + ресурс.
- `sim_prob(ref, prob)` - `ref` = UUID **transition** (не gateway), `prob` 0.0-1.0.
- `make_scenario(scen_id, name, elements, desc="", author=None, duration="PT40H")`
- `make_bpsim(scenarios, level="LevelThree", pretty=True)`
- `bpsim_empty()` - минимальный LevelOne для ПР-5.

### Глобалы (установить ДО write_bpm)

- `bizagi.SCENARIO_AUTHOR` - author в `<ns1:Scenario>`.
- `bizagi.AUTHOR` - `<Author>` в Package header.
- `bizagi.USER` - UserName в Modifications и в `Users/<name>/`.

### Запись и проверка

- `write_bpm(filename, diag_xml, bpsim_xml, out_dir, attachments=None, participants=None)`
- `write_bpm_multi(filename, diagrams, out_dir, participants=None)` - список dict'ов с `pkg_id`, `diag_xml`, опц. `bpsim_xml`/`attachments`/`is_selected`.
- `validate_bpm_file(bpm_path)` - 10 проверок BPSim, возвращает `[(level, msg)]`.
- `transplant_bpsim(source_bpm, new_bpsim_xml, output_path)` - редкий резервный путь.

### Layout helpers
- `y_in_lane(lane_y, lane_h, elem_h)`
- `y_in_subrow(lane_y, lane_h, row_idx, n_rows, elem_h)`
- `x_after(prev_x, prev_w, gap=30)`

### Константы
- `SIZE_EVENT = (36, 36)`, `SIZE_TASK = (150, 70)`, `SIZE_GATE = (50, 50)`, `SIZE_SUB = (160, 80)`
- `START_X = 150`
- `LANE_HEADER = 54` (внутренняя, для y_in_lane)

---

## ПР-5 checklist

| п. | Требование | Решение |
|----|------------|---------|
| 1 | 2 диаграммы, 2-3 lane | Способ 1 (2 `write_bpm()`) или Способ 3 (`write_bpm_multi()`) |
| 2 | Прикрепить файл | `attachments=[(path, element_id)]` |
| 3 | Типы Task / Gateway / Event | Правильный `atype` |
| 4 | DataStore | `artifact()` с `fields=` |
| 5 | SubProcess | `activity(..., "SubProcess", sub_ref=...)` + `workflow_process()` |
| 6 | Расширенные атрибуты + 2-3 строки | `artifact(..., fields=[...], rows=[...])` |
| 7 | Скрин валидации | Вручную в Bizagi: Diagram -> Validate |
| 8 | Подсчёт элементов | Вручную в Bizagi: View -> Element Count |

## ПР-6 checklist

| п. | Требование | Решение |
|----|------------|---------|
| 1-4 | Документировать элементы | `doc=` в каждом `activity()`, `lane()`, `artifact()` |
| 5 | Публикация в Word | Вручную: Tools -> Publish -> Word |
| 6-7 | Сценарий «Как есть» + Excel | `make_scenario("Как есть", ...)` |
| 8-9 | Сценарий «Как должно быть» + Excel | `make_scenario("Как должно быть", ...)` |

### Как запустить симуляцию в Bizagi UI

1. Открыть `.bpm` в Bizagi Modeler.
2. Перейти на главную диаграмму с настроенным BPSim.
3. Лента -> Simulation -> «Process Simulation».
4. Level -> «3 Resource Analysis».
5. Выбрать сценарий («Как есть» / «Как должно быть»).
6. Нажать **Start** (зелёный треугольник).
7. Results -> Export to Excel.

**НЕ через What-If Analysis** - это инструмент сравнения, не запуска.

---

## Common mistakes

| Симптом | Причина | Фикс |
|---------|---------|------|
| EmptyDocumentException | XML битый ИЛИ несколько видимых Pool в один .diag | escape автоматический; не делать несколько Pool без `sub_ref` |
| **Наложение дорожек / лишняя lane сверху** | Для SubProcess в Способе 2 создан `pool_main()` (subprocess WF попал в `pools_xml`) | SubProcess WF только в `wf_xml` блок `make_package()`, без своего `pool_main()`. См. ⚠️ блок в Способе 2 |
| SubProcess как лишний pool | SubProcess WF в `<Pools>` | SubProcess только в `<WorkflowProcesses>` |
| Wrong sim_prob ref | Передан UUID шлюза | UUID sequence flow (transition) |
| Симуляция «Невозможно запустить» | Один из ROOT CAUSE 1-6 | `validate_bpm_file()` укажет |
| Зигзаг стрелки между lanes | Разные X у source/target | Одинаковый X (КРИТИЧЕСКОЕ правило) |
| «Начало» наезжает на label | X слишком маленький | Использовать `START_X` (150+) |
| Подпись gateway наезжает | Lane слишком низкая | h≥200 или 2 ряда через `y_in_subrow` |
| Resource_1 в Excel | Не передан `participants=` | Передать `participants=[{"id":..., "name":..., "desc":...}]` |
| Блоки сдвинуты вверх | Не использован helper | Только `y_in_lane`/`y_in_subrow` |
| Имя автора - «User» | Не установлены глобалы | `bizagi.SCENARIO_AUTHOR = "..."` и т.п. |
| **«Улетевшая» стрелка/элемент в углу (X=150,Y=89)** | transition подпроцесса попал в `trans` главной диаграммы (копипаста). Bizagi отрисует sub-элементы по их sub-координатам поверх главной | С v5.5 workflow_process() сам бросает ValueError со списком висячих ID. Если ловишь — проверь что все `transition(...)` в trans главной диаграммы ссылаются ТОЛЬКО на элементы из её `acts` |
| **SubProcess наезжает на следующий блок при ветвлении** | После XOR-split два параллельных пути имеют разную ширину (EndEvent=36 vs SubProcess=160), но X считается единой переменной с `SIZE_EVENT[0]` | Считай X отдельно для каждой ветки. `x_after(prev, SIZE_SUB[0])` после SubProcess, `x_after(prev, SIZE_TASK[0])` после Task, `x_after(prev, SIZE_EVENT[0])` после Event |
| **`write_bpm_multi` не найдена / NameError на helper'е** | Локальная `bizagi.py` старая или обрезанная | Перекопировать из бандла (`cp $SKILL_DIR/scripts/bizagi.py $OUT_DIR/`), удалить `__pycache__`, прогнать grep-валидацию (см. «Bundled library») |
| **EmptyDocumentException при правильной структуре**, XML с каждым символом на новой строке | Helper-функция возвращает уже-склеенную строку: `def acts(): return "\n".join(a)`, а её результат передаётся в `workflow_process(..., acts(), ...)`. Внутри `"\n".join(string)` итерирует строку **по символам** | Возвращайте СПИСОК из helper-функций: `def acts(): return a` (без join). Либо в v4.5+ `workflow_process()` сам распознаёт строку и не ломает её. Если работаете со старой `bizagi.py` - не делайте `join` дважды |

---

## FAQ - тонкие моменты

**Q: Какова zip-структура .bpm?**
A: Outer .bpm-zip содержит `ModelInfo.xml`, `Participants.xml`, `Preferences.bpp`, по одному inner `.diag` (тоже zip) на каждую видимую диаграмму, плюс `Users/<name>/UserPreferences.xml` и др. Каждый inner `.diag` zip содержит `Diagram.xml` (XPDL 2.2), `BPSimData.xml` (BPSim 1.0), `BPSimDataResult.xml`, `Actions.xml`, опционально `Files/<id>/<filename>` для прикреплённых файлов.

**Q: Сколько Pool в одной диаграмме?**
A: Всегда 2: один `pool_empty()` (скрытый «Основной процесс», обязателен) + один `pool_main()` (видимый с lane'ами).

**Q: Разница workflow_process vs empty_workflow_process?**
A: `empty_workflow_process(proc_id)` - для скрытого Pool «Основной процесс» (без активностей). `workflow_process(proc_id, name, acts, trans, arts, assocs)` - для содержательных процессов (с активностями).

**Q: Связь pkg_id и имени inner .diag файла?**
A: Они равны - `write_bpm()` и `write_bpm_multi()` синхронизируют автоматически. Имя inner-файла внутри outer zip = `<pkg_id>.diag`.

**Q: Что если в `sim_task` не передать `res=`?**
A: Задача будет в BPSim без `ResourceParameters` - не привязана к ресурсу. Длительность учитывается, но resource utilization для неё не считается.

**Q: Если у SubProcess shell-активности sub_ref ссылается на UUID диаграммы из write_bpm_multi?**
A: При двойном клике по shell-активности Bizagi переключится на ту закладку (если есть .diag с таким UUID в .bpm).

**Q: Если `author=None` в `make_scenario`?**
A: Берётся `bizagi.SCENARIO_AUTHOR` (глобал). Установите его в начале gen.py.

**Q: Что произойдёт если в `sim_prob` передать UUID gateway вместо transition?**
A: Симуляция Bizagi не запустится корректно (ROOT CAUSE: неверный ref). `validate_bpm_file()` это поймает и подскажет.
