From 479ccb24c3b7161e2cc04e8d43e55781322b3c78 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 01:56:56 +0300 Subject: [PATCH 01/24] T-001: Create input-classification.md --- references/input-classification.md | 149 +++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 references/input-classification.md diff --git a/references/input-classification.md b/references/input-classification.md new file mode 100644 index 0000000..63732b9 --- /dev/null +++ b/references/input-classification.md @@ -0,0 +1,149 @@ +# Input classification (Step 0) + +## 1. Purpose + +Classify user input before generation into one of 5 routing scenarios for v2.3.0. + +Step 0 runs before Camunda knowledge loading and before any BPMN XML is generated. +It decides whether the skill can continue in Generate mode, should continue with +reuse-ID behavior, or must reject the input with a clear explanation. + +Variant C keeps a single Generate mode. Validate and Fix are intentionally deferred +to v2.4.0, so Step 0 never routes into a separate Validate or Fix workflow. + +## 2. Detection heuristics + +### 2.1 Pure text + +Pure text is the default scenario for a new BPMN model. + +Signals: +- No `C8 migration: +https://docs.camunda.io/docs/guides/migrating-from-camunda-7/migration-tooling/diagram-converter/ +``` + +### 2.4 Unsupported format + +Unsupported binary or diagramming formats must be rejected before attempting BPMN +generation. + +Signals: +- File extension in: `.drawio`, `.vsdx`, `.png`, `.jpg`, `.pdf` +- OR binary file headers are detected, for example PNG or PDF magic bytes +- OR the file is an image/screenshot rather than BPMN XML + +Routing result: REJECT. + +Required response: + +```text +Format not supported. Supported: text process descriptions, .bpmn / XML files. +``` + +### 2.5 Invalid XML + +Invalid XML applies when the input appears to contain BPMN/XML, but parsing fails. + +Signals: +- XML parsing fails +- BPMN is truncated or malformed +- Closing tags are missing +- Namespace declarations are broken + +Routing result: RECOVER or REJECT. + +Recovery rule: +- Try to recover well-formed XML only when the fix is obvious and local +- If recovery is uncertain, reject with the parse error message +- Do not invent missing BPMN content during recovery + +## 3. Routing rules + +| Signal | Mode | Notes | +|---|---|---| +| Pure text | Generate | Pass through Wizard | +| Mixed input | Generate with reuse-ID | Pass through Wizard for new/changed parts only | +| zeebe namespace | REJECT | Output Camunda Diagram Converter guidance | +| Unsupported format | REJECT | Output supported input formats | +| Invalid XML | RECOVER or REJECT | Try well-formed recovery; otherwise reject with parse error | + +Priority order when multiple signals are present: + +1. Empty input wins over all other signals. +2. Unsupported binary format wins over XML heuristics. +3. Zeebe namespace wins over mixed input. +4. Invalid XML wins over mixed input. +5. Mixed input wins over pure text. + +## 4. Edge cases + +### 4.1 Empty input + +Reject with: + +```text +Empty input. Provide a process description or BPMN file. +``` + +### 4.2 XML without explicit "update" trigger + +In Variant C, route to Generate with reuse-ID by default. Do not ask whether the +user wants Validate or Fix mode because these modes are not implemented in v2.3.0. + +### 4.3 Multiple .bpmn files attached + +Accept only the first `.bpmn` file. Warn that all other BPMN files are ignored. + +### 4.4 Text mentions "validate" or "fix" + +In Variant C, these capabilities are not yet implemented. Inform the user and +route to Generate with reuse-ID when BPMN/XML is present. + +Required response pattern: + +```text +Validate/Fix mode is planned for v2.4.0. In v2.3.0 I can regenerate the BPMN +with reuse-ID rules and preserve existing IDs where appropriate. +``` + +### 4.5 Plain text with update verbs but no BPMN + +If the user says `обнови` or `дополни` but provides no BPMN/XML, treat the input +as pure text Generate. Mention that no existing IDs can be preserved because no +source BPMN was provided. From 2d5fb36bd2cc6b4cd9738284d16abc6da6fd4aac Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 01:57:50 +0300 Subject: [PATCH 02/24] T-002: Update SKILL.md with Step 0 --- SKILL.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/SKILL.md b/SKILL.md index 2a49bbb..bb2faf5 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,7 +1,7 @@ --- name: bpmn-process-modeler -description: Converts unstructured text descriptions of business processes (meeting transcripts, written narratives, process memos) into valid BPMN 2.0 XML for Camunda Platform 7, with all diagram labels in Russian and optional Excel process specification. Use whenever the user pastes a transcript, meeting notes, or prose describing a business process and asks to model, draw, map, diagram, or convert it into BPMN, Camunda, a .bpmn file, pools and lanes, or a process XML. Also use when the user wants an Excel specification table of a BPMN process. The skill pre-loads current Camunda documentation via the Camunda MCP server (search_camunda_knowledge_sources) before generating XML, validates the result against 7 structural and language checks, asks the user for approval, then optionally exports a 9-column UTF-8 Excel specification reconciled against the diagram across 9 parity checks. -version: 2.2.0 +description: Converts prose, transcripts, notes, process memos, or mixed text + existing BPMN into valid BPMN 2.0 XML for Camunda Platform 7, with Russian labels and optional Excel specification. Use when the user asks to model, draw, map, diagram, convert to BPMN/Camunda/.bpmn/XML, create pools and lanes, export an Excel process table, уточни процесс перед моделированием, обнови существующий BPMN, дополни BPMN, расширь схему, or генерируй с допущениями. The skill classifies input, loads Camunda docs, validates XML, asks for approval, then exports a reconciled UTF-8 Excel specification. +version: 2.3.0 snapshot_version: 1.0 snapshot_date: 2026-04-26 snapshot_expiry: 2026-10-23 @@ -34,7 +34,16 @@ Violating any of these means the deliverable is broken. Treat them as preconditi --- -## The workflow — 9 steps in fixed order +## The workflow — Generate mode in fixed order + +### Step 0 — Input classification + +Classify the user input before loading Camunda knowledge or generating XML. Use +`references/input-classification.md` as the routing source of truth. + +Route pure text to Generate, mixed text + BPMN/XML to Generate with reuse-ID, +Camunda 8 / `zeebe:*` XML to REJECT with Diagram Converter guidance, unsupported +formats to REJECT, and invalid XML to RECOVER or REJECT with the parse error. ### Step 1 — Load current Camunda documentation (with fallback) @@ -68,6 +77,13 @@ At top of generated XML: **In degraded mode (Option B):** proceed with the full workflow (Steps 2-9) as normal, but flag the degraded status in Step 7 approval prompt (see Step 7). +### Step 1.5 — Clarification Wizard + +Placeholder for v2.3.0 Wizard integration. If Step 0 routes to Generate, run the +Wizard only when process facts are missing; if nothing is missing, inform the +user and proceed to Step 2. Full behavior is defined in +`references/clarification-wizard.md` and filled in by T-105. + ### Step 2 — Parse and classify the input Extract and state explicitly before modeling: From 9fa730b20d6aed87e38ef7327b7287a1e99b04b4 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 01:58:54 +0300 Subject: [PATCH 03/24] T-003: Reclassify validation-checklist.md severity --- references/validation-checklist.md | 48 ++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/references/validation-checklist.md b/references/validation-checklist.md index 9acaf55..64405e2 100644 --- a/references/validation-checklist.md +++ b/references/validation-checklist.md @@ -6,8 +6,22 @@ --- +## Severity levels (Camunda-aligned) + +| Level | Meaning | Source | +|---|---|---| +| ERROR | XML won't deploy or is invalid | Camunda Engine rejection | +| TASK | Structural issue requiring manual decision | Diagram Converter: "manual changes required" | +| REVIEW | Auto-fix would apply changes — verify | Diagram Converter: "modified, please verify" | +| WARNING | Best-practice violation | Diagram Converter: "cannot be directly mapped" | +| INFO | Metric or observation | Diagram Converter: "no action needed" | + +--- + ## 1. Well-formedness +severity: ERROR + **Что проверяем:** XML парсится стандартным парсером без ошибок. **Как проверить (Python):** @@ -33,6 +47,8 @@ except ET.ParseError as e: ## 2. BPMN schema conformance +severity: ERROR + **Что проверяем:** каждый тег валиден по BPMN 2.0 XSD. Нет придуманных элементов. `targetNamespace` задан на ``. Все используемые префиксы объявлены. **Как проверить:** прогнать через xmllint против XSD: @@ -56,6 +72,8 @@ xmllint --noout --schema https://www.omg.org/spec/BPMN/20100524/DI.xsd process.b ## 3. Structural integrity +severity: REVIEW when `auto_fixable=true`; TASK when `auto_fixable=false` + **Что проверяем:** граф процесса корректен. 10 подпунктов: ### 3a. Все sequenceFlow ссылаются на существующие узлы @@ -212,6 +230,8 @@ Data objects не обязательны, но если есть — должн ## 4. Message flows и Collaboration structure +severity: REVIEW when `auto_fixable=true`; TASK when `auto_fixable=false` + **Что проверяем:** если есть collaboration с пулами, все `messageFlow` идут МЕЖДУ разными пулами. Связи participant → process корректные. Lane membership соблюдён. ### 4a. Message flow направление @@ -263,6 +283,8 @@ for process in root.iter('{*}process'): ## 5. Camunda 7 executability +severity: ERROR + **Что проверяем:** если `isExecutable="true"`, все задачи корректно сконфигурированы для runtime. Целевая платформа — Camunda 7 (Platform). Обязательные атрибуты по типу задачи: @@ -418,6 +440,8 @@ def check_camunda_attr_compatibility(root): ## 6. DI completeness +severity: ERROR + **Что проверяем:** каждый элемент, который должен отображаться, имеет фигуру или ребро в `` с корректными координатами и атрибутами. ```python @@ -536,6 +560,8 @@ BPMNLabel — дочерний опциональный элемент BPMNShape ## 7. Language conformance +severity: ERROR + **Что проверяем:** все `name` атрибуты на семантических элементах — на русском. Проверяется отдельно потому что языковая ошибка не ловится XSD-валидатором. ```python @@ -577,6 +603,8 @@ for elem in root.iter(): ## 8. Naming and readability (WARN, не блокируют) +severity: WARNING for 8.1-8.5 and 8.7; INFO for 8.6 and 8.8 + Проверки соответствия Camunda Best Practices по naming и readability. Не блокируют генерацию, но триггерят WARN с конкретной рекомендацией. | Под-чек | Правило | Порог | @@ -656,6 +684,8 @@ def check_readability(root, ns): ### 9. Technical ID naming convention +severity: WARNING + Camunda рекомендует префиксы, соответствующие типу элемента (Best-practices: Naming technically relevant IDs): | XML element | Префикс | @@ -679,6 +709,8 @@ Camunda рекомендует префиксы, соответствующие ## 10. Event labels business-side +severity: WARNING + Camunda: «describe which state an object is in when the process is about to leave the event». - Start event: что триггерит процесс. «Заявка подана», не «Start». @@ -689,6 +721,8 @@ Camunda: «describe which state an object is in when the process is about to lea ## 11. Business vs technical errors +severity: WARNING + Camunda: «retrying technical problems should not be modeled in the diagram». - Technical ошибки (сетевые сбои, временный отказ внешнего API) — НЕ моделировать в BPMN. Обрабатываются через Camunda job retries + incidents. @@ -698,6 +732,8 @@ Camunda: «retrying technical problems should not be modeled in the diagram». ## 12. Happy path emphasis +severity: WARNING + Camunda: «place tasks, events, and gateways belonging to the happy path on a straight sequence flow in the center of the diagram». Основной успешный путь — прямая линия слева направо по центру. Исключения и обработка ошибок — отклонения вверх/вниз. @@ -706,6 +742,8 @@ Camunda: «place tasks, events, and gateways belonging to the happy path on a st ## 13. Sentence case +severity: WARNING + Camunda recommended: первая буква заглавная, остальные строчные, кроме аббревиатур и имён собственных. - Правильно: «Проверить заявку», «Требуется ли SCA?» @@ -713,16 +751,22 @@ Camunda recommended: первая буква заглавная, остальн ## 14. Один executable process в collaboration +severity: WARNING + В коллаборейшен-модели с несколькими пулами обычно только один `` имеет `isExecutable="true"` — это тот, который мы собираемся деплоить. Остальные — participant pools без `isExecutable`, представляющие внешние системы / контрагентов. Антипаттерн: `isExecutable="true"` на всех процессах в коллаборейшене. Путаница, какой деплоится. ## 15. Filename aligned with process ID +severity: WARNING + Camunda recommended: если процесс `BNPLApprovalProcess`, то файл `BNPLApprovalProcess.bpmn`. Облегчает поиск, diff, версионирование. ## 16. Unused resources +severity: WARNING + ``, ``, ``, `` объявлены на уровне ``, но ни одно событие на них не ссылается. Не ошибка, но признак неполной доработки модели или остатка после рефакторинга. ```python @@ -735,6 +779,8 @@ if unused: ## 17. Empty process +severity: WARNING + Процесс содержит только `` → `` без промежуточных задач / шлюзов. Деплоится, выполняется, но бессмыслен. ```python @@ -749,6 +795,8 @@ for process in root.iter('{*}process'): ## 18. Circular call activity detection +severity: WARNING + Call Activity A вызывает process B, а B содержит call activity на process A. Runtime stack overflow. Статический анализ: построить граф call-зависимостей (для каждого `` — ребро от текущего process id к calledElement), найти циклы через DFS. From 128964ba3746526dc65c65b2e12f79935e0c35ed Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:00:15 +0300 Subject: [PATCH 04/24] T-401: Create test_input_classification.py --- tests/release/test_input_classification.py | 196 +++++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 tests/release/test_input_classification.py diff --git a/tests/release/test_input_classification.py b/tests/release/test_input_classification.py new file mode 100644 index 0000000..9235ac5 --- /dev/null +++ b/tests/release/test_input_classification.py @@ -0,0 +1,196 @@ +from pathlib import Path +import xml.etree.ElementTree as ET + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[2] +DOC_PATH = REPO_ROOT / "references" / "input-classification.md" +DIAGRAM_CONVERTER_URL = ( + "https://docs.camunda.io/docs/guides/migrating-from-camunda-7/" + "migration-tooling/diagram-converter/" +) + + +def classify_input(input_text="", filename=None, binary_header=b""): + text = input_text or "" + filename = filename or "" + lower_name = filename.lower() + lower_text = text.lower() + + if not text.strip() and not filename and not binary_header: + return { + "mode": "reject", + "reason": "Empty input. Provide a process description or BPMN file.", + "has_xml": False, + "mixed": False, + "reuse_id": False, + } + + unsupported_exts = (".drawio", ".vsdx", ".png", ".jpg", ".pdf") + if lower_name.endswith(unsupported_exts) or binary_header.startswith((b"\x89PNG", b"%PDF")): + return { + "mode": "reject", + "reason": "unsupported format", + "has_xml": False, + "mixed": False, + "reuse_id": False, + } + + has_xml = " 2) or ( + has_xml and has_update_trigger + ) + + if mixed: + return { + "mode": "generate", + "reason": "mixed input", + "has_xml": True, + "mixed": True, + "reuse_id": True, + } + + if has_xml: + return { + "mode": "generate", + "reason": "xml without explicit update trigger defaults to reuse-ID", + "has_xml": True, + "mixed": True, + "reuse_id": True, + } + + return { + "mode": "generate", + "reason": "pure text", + "has_xml": False, + "mixed": False, + "reuse_id": False, + } + + +@pytest.mark.parametrize( + "input_text, filename, binary_header, expected_mode, expected_attrs", + [ + ( + "Опишу процесс одобрения заявки клиентом и менеджером банка.", + None, + b"", + "generate", + {"has_xml": False, "mixed": False, "reuse_id": False}, + ), + ( + "Обнови процесс. Добавь проверку. Сохрани существующие ID. " + "", + None, + b"", + "generate", + {"has_xml": True, "mixed": True, "reuse_id": True}, + ), + ( + "", + None, + b"", + "reject", + {"has_xml": True, "mixed": False, "reuse_id": False}, + ), + ("diagram.drawio", "diagram.drawio", b"", "reject", {"has_xml": False}), + ("process.vsdx", "process.vsdx", b"", "reject", {"has_xml": False}), + ("", "screen.png", b"\x89PNG\r\n\x1a\n", "reject", {"has_xml": False}), + ("", None, b"", "reject", {"has_xml": True}), + ("", None, b"", "reject", {"has_xml": False, "mixed": False, "reuse_id": False}), + ( + "Нужно расширить схему. Добавить второй путь. Сохранить прежние элементы. " + "", + None, + b"", + "generate", + {"has_xml": True, "mixed": True, "reuse_id": True}, + ), + ( + "дополни ", + None, + b"", + "generate", + {"has_xml": True, "mixed": True, "reuse_id": True}, + ), + ], +) +def test_step0_routing(input_text, filename, binary_header, expected_mode, expected_attrs): + result = classify_input(input_text, filename=filename, binary_header=binary_header) + + assert result["mode"] == expected_mode + for key, expected in expected_attrs.items(): + assert result[key] == expected + + +def test_pure_text_negative_variant_has_no_reuse_id(): + result = classify_input("Смоделируй простой процесс регистрации клиента.") + + assert result["mode"] == "generate" + assert result["has_xml"] is False + assert result["reuse_id"] is False + + +def test_zeebe_namespace_rejected_with_converter_guidance(): + result = classify_input( + "" + ) + + assert result["mode"] == "reject" + assert "Diagram Converter" in result["reason"] + assert DIAGRAM_CONVERTER_URL in result["reason"] + + +def test_unsupported_format_negative_variant_allows_bpmn_extension(): + result = classify_input(filename="process.bpmn") + + assert result["mode"] == "generate" + assert result["reuse_id"] is True + + +def test_invalid_xml_negative_variant_accepts_well_formed_xml(): + result = classify_input( + "" + ) + + assert result["mode"] == "generate" + assert result["has_xml"] is True + + +def test_input_classification_reference_contract(): + text = DOC_PATH.read_text(encoding="utf-8") + + for section in ("Purpose", "Detection heuristics", "Routing rules", "Edge cases"): + assert section in text + for scenario in ("Pure text", "Mixed input", "zeebe namespace", "Unsupported format", "Invalid XML"): + assert scenario in text + assert DIAGRAM_CONVERTER_URL in text + assert "Validate/Fix mode is planned for v2.4.0" in text From b0c8e92c92e7e190c5c8ea51c02f169e2c63d616 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:01:59 +0300 Subject: [PATCH 05/24] T-004: Extend annotation-style-guide.md prefix --- references/annotation-style-guide.md | 43 ++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/references/annotation-style-guide.md b/references/annotation-style-guide.md index 3778f32..e1e51e5 100644 --- a/references/annotation-style-guide.md +++ b/references/annotation-style-guide.md @@ -351,4 +351,47 @@ Camunda: "Avoid naming event-based gateways; avoid naming parallel gateways and --- +## Annotation prefix: ⚠ Допущение: + +Distinct from `⚠ Уточнить:` — used by Clarification Wizard for model-accepted assumptions. + +### Comparison + +| Prefix | When to use | Who fills | +|---|---|---| +| `⚠ Уточнить:` | Source has a gap or ambiguity that the model cannot resolve | Model leaves question to user | +| `⚠ Допущение:` | Model has filled missing information using typical practice defaults | Model explicitly marks its choice | +| `Примечание:` | Informational comment without call to action | For context only | + +### Rules for `⚠ Допущение:` + +1. Use only when the Wizard has accepted a missing fact via "with assumptions" mode OR when user skipped a Wizard question +2. Always pair with `` linking annotation to target node +3. Annotation text format: two lines minimum + - Line 1: `⚠ Допущение: ` + - Line 2+: justification ("В исходнике не указано / принято по типичной практике / etc.") +4. ID format: `TextAnnotation_Assumption_` where N is sequential per process + +### XML example + +```xml + + ⚠ Допущение: SLA на ручную проверку — 24 часа. +В исходнике срок не указан, принят по типичной банковской практике. + + + +``` + +### When NOT to mark as Допущение + +- Trivially derivable from context (task type from verb) +- Standard BPMN conventions (start event begins process) +- Explicitly stated facts in the source text +- Meta-information about model's working approach + +--- + **Этот гайд обязателен к использованию в Step 5 (генерация XML) и Step 6 (валидация).** Если аннотация в модели отклоняется от шаблонов и правил этого документа — это WARN при валидации, требующий либо коррекции, либо явного обоснования в отчёте. From dfdad78bc48edb2acc822343cbefb28cba9e85c5 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:03:16 +0300 Subject: [PATCH 06/24] T-101: Create clarification-wizard.md --- references/clarification-wizard.md | 330 +++++++++++++++++++++++++++++ 1 file changed, 330 insertions(+) create mode 100644 references/clarification-wizard.md diff --git a/references/clarification-wizard.md b/references/clarification-wizard.md new file mode 100644 index 0000000..f9f34c1 --- /dev/null +++ b/references/clarification-wizard.md @@ -0,0 +1,330 @@ +# Clarification Wizard + +## 1. Purpose + +Clarification Wizard is Step 1.5 in Generate mode. It detects missing process +facts before BPMN generation and either asks targeted questions or proceeds with +explicit assumptions. + +The Wizard is not a separate mode. It sits between Camunda knowledge loading and +Parse & classify. Its goal is to prevent silent invention while keeping the +generation flow fast for complete inputs. + +## 2. Workflow (5 steps) + +1. Read the user input and Step 0 classification result. +2. Scan the input for the 6 missing-facts categories from Section 3. +3. Count missing categories and route by Section 4. +4. Ask targeted questions or activate "with assumptions" mode. +5. Pass answered facts and accepted assumptions to Step 2. + +Output of the Wizard: +- `missing_categories`: ordered list by priority +- `answered_facts`: facts supplied by the user +- `accepted_assumptions`: defaults accepted by skip or assumption mode +- `wizard_invoked`: true when questions were asked +- `assumption_mode`: true when questions were skipped by explicit command + +## 3. Missing-facts categories (6) + +Priority order is fixed: topology, participants, happy_path, exception_paths, +slas, data_ownership. + +### 3.1 Topology (priority 1, high) + +**Trigger words for detection:** +- отдел +- команда +- партнёр +- организация +- филиал +- collaboration +- message +- pool +- lane + +**Detection heuristic:** +Topology is missing if the text does not reveal whether the process has one +organization, several internal roles, or multiple organizations exchanging +messages. + +**Default assumption (if user skips):** +Single pool, no collaboration. + +**Question template:** +"Какая топология процесса нужна? Выбери вариант или впиши свой:" +- Option A: Один пул без лэйнов +- Option B: Один пул с лэйнами по ролям +- Option C: Несколько пулов с message flows +- Free-form: "Другое: ___" + +### 3.2 Participants (priority 2, high) + +**Trigger words for detection:** +- менеджер +- оператор +- система +- должность +- role +- исполнитель +- клиент +- банк +- регулятор + +**Detection heuristic:** +Participants are missing if the process describes actions but does not name who +or what performs them. + +**Default assumption (if user skips):** +Use placeholder participant "Внутренний исполнитель". + +**Question template:** +"Кто выполняет основные действия? Выбери вариант или впиши свой:" +- Option A: Внутренний исполнитель +- Option B: Клиент + внутренний исполнитель +- Option C: Внутренний отдел + внешняя система +- Free-form: "Другое: ___" + +### 3.3 Happy path (priority 3, high) + +**Trigger words for detection:** +- сначала +- затем +- после +- готово +- отправлено +- утверждено +- завершено +- подписано +- одобрено + +**Detection heuristic:** +Happy path is missing if the text names a process goal but does not describe the +main successful sequence from start to end. + +**Default assumption (if user skips):** +Linear sequence, single end event. + +**Question template:** +"Как выглядит основной успешный путь? Выбери вариант или впиши свой:" +- Option A: Линейный путь без ветвлений +- Option B: Проверка → решение → успешное завершение +- Option C: Параллельная подготовка нескольких материалов → объединение +- Free-form: "Другое: ___" + +### 3.4 Exception paths (priority 4, medium) + +**Trigger words for detection:** +- если не +- при ошибке +- отказ +- таймаут +- исключение +- эскалация +- отмена +- просрочка +- недоступен + +**Detection heuristic:** +Exception paths are missing if the text has only the happy path and does not say +what happens on rejection, timeout, validation failure, or unavailable system. + +**Default assumption (if user skips):** +No alternative path; process ends on error or rejection. + +**Question template:** +"Какие исключения нужно показать? Выбери вариант или впиши свой:" +- Option A: Только отказ с завершением процесса +- Option B: Отказ + ручная эскалация +- Option C: Таймаут + повторная попытка +- Free-form: "Другое: ___" + +### 3.5 SLAs (priority 5, medium) + +**Trigger words for detection:** +- срок +- дедлайн +- в течение +- рабочих дней +- часов +- вовремя +- SLA +- таймер +- просрочка + +**Detection heuristic:** +SLAs are missing if the process contains manual or waiting steps but no explicit +time limits, deadlines, or timeout behavior. + +**Default assumption (if user skips):** +Use "По регламенту" with no specific timer. + +**Question template:** +"Какие сроки важны для процесса? Выбери вариант или впиши свой:" +- Option A: По регламенту, без конкретного таймера в BPMN +- Option B: 24 часа на ручную проверку +- Option C: 3 рабочих дня на полный цикл +- Free-form: "Другое: ___" + +### 3.6 Data ownership (priority 6, low) + +**Trigger words for detection:** +- документ +- файл +- информация +- хранится +- передаётся +- owner +- владелец +- источник +- система + +**Detection heuristic:** +Data ownership is missing if documents or data objects are mentioned but no +source system, owner, or storage point is specified. + +**Default assumption (if user skips):** +Source system not specified. + +**Question template:** +"Где живут ключевые данные процесса? Выбери вариант или впиши свой:" +- Option A: Source system not specified +- Option B: CRM / LOS является источником данных +- Option C: Документы хранятся в СЭД +- Free-form: "Другое: ___" + +## 4. Routing by missing count (4 scenarios) + +### 4.1 Zero missing -> skip + +If no missing categories are found, skip the Wizard. Inform the user: +"Всё понятно, перехожу к генерации." + +### 4.2 1-2 missing -> targeted questions + +Ask all missing categories as 1-2 separate questions. One question covers one +category only. + +### 4.3 3-5 missing -> priority-ordered questions + +Ask all missing categories, sorted by priority. Do not group categories. + +### 4.4 6+ missing -> offer "with assumptions" mode + +Offer the user a choice: +- provide more detail +- continue "with assumptions" + +If assumption mode is accepted, do not ask questions. Apply defaults and mark +each accepted default with `⚠ Допущение:`. + +## 5. "With assumptions" mode + +### 5.1 Trigger phrases + +The user explicitly opts into assumption mode by saying any of: +- `делай с допущениями` +- `генерируй с предположениями` +- `не задавай вопросов` +- `генерируй без вопросов` +- `as is` +- `as-is` +- `just do it` + +When activated: +- Wizard does not ask questions +- Each missing fact becomes a `⚠ Допущение:` annotation in BPMN +- Each assumption is eligible for the Excel sheet «Допущения» + +### 5.2 Annotation generation rules + +For each missing fact: +1. Determine the target BPMN node where the assumption applies. +2. Create `` with id `TextAnnotation_Assumption_`. +3. Use two-line text: assumption first, justification second. +4. Create `` from target node to annotation. +5. Add an Excel row in sheet «Допущения» when Excel export is approved. + +### 5.3 Edge case: 0 missing facts + assumption mode active + +If assumption mode is active but no missing facts are detected, create no +annotations. Inform the user: "В тексте нет пробелов, допущения не требуются." + +### 5.4 Mixing user-answered + skipped + assumed + +One Wizard session may contain: +- answered questions, which create no `⚠ Допущение:` +- skipped questions, which use category defaults and create assumptions +- low-priority missing facts below the question cutoff, which become assumptions + +## 6. Discipline rules + +- Maximum 5 questions per pass. +- Do not ask about facts already present in the text. +- Do not mark as `⚠ Допущение:` what is trivially derivable. +- Do not group several missing categories into one question. +- Always respect priority order. +- Use category defaults only after skip, cutoff, or explicit assumption mode. +- In mixed input, ask only about new or changed parts. + +## 7. Annotation template + +```xml + + ⚠ Допущение: SLA на ручную проверку — 24 часа. +В исходнике срок не указан, принят default из category slas. + + + +``` + +## 8. Full session example + +### Input + +```text +Смоделируй процесс одобрения BNPL-заявки. Клиент подаёт заявку в приложении. +Менеджер проверяет документы и принимает решение. При одобрении договор +отправляется клиенту на подпись. +``` + +### Detection + +Detected facts: +- participants: клиент, менеджер +- happy_path: заявка -> проверка документов -> решение -> подпись + +Missing categories: +- topology +- exception_paths +- slas +- data_ownership + +### Wizard questions + +1. Какая топология процесса нужна? +2. Какие исключения нужно показать? +3. Какие сроки важны для процесса? +4. Где живут ключевые данные процесса? + +### User answers + +```text +Один пул с лэйнами. Исключение — отказ. Сроки и owner данных не знаю. +``` + +### Result + +Answered facts: +- topology = one pool with lanes +- exception_paths = rejection path + +Accepted assumptions: +- slas = "По регламенту" +- data_ownership = "Source system not specified" + +Generated BPMN includes two `⚠ Допущение:` annotations linked to the relevant +task or data object. If Excel export is approved, sheet «Допущения» contains one +row per accepted assumption. From 7754ba8ec874ff4b7969011f0272c508d6adfe13 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:03:53 +0300 Subject: [PATCH 07/24] T-102: Define missing-facts categories --- references/clarification-wizard.md | 50 ++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/references/clarification-wizard.md b/references/clarification-wizard.md index f9f34c1..009b48c 100644 --- a/references/clarification-wizard.md +++ b/references/clarification-wizard.md @@ -30,7 +30,7 @@ Output of the Wizard: Priority order is fixed: topology, participants, happy_path, exception_paths, slas, data_ownership. -### 3.1 Topology (priority 1, high) +### Category 1: topology (priority: high) **Trigger words for detection:** - отдел @@ -44,10 +44,13 @@ slas, data_ownership. - lane **Detection heuristic:** -Topology is missing if the text does not reveal whether the process has one +Category is "missing" if the text does not reveal whether the process has one organization, several internal roles, or multiple organizations exchanging messages. +Category is "present" if pools, lanes, organizations, departments, or message +exchange boundaries are explicitly described. + **Default assumption (if user skips):** Single pool, no collaboration. @@ -58,7 +61,7 @@ Single pool, no collaboration. - Option C: Несколько пулов с message flows - Free-form: "Другое: ___" -### 3.2 Participants (priority 2, high) +### Category 2: participants (priority: high) **Trigger words for detection:** - менеджер @@ -72,8 +75,11 @@ Single pool, no collaboration. - регулятор **Detection heuristic:** -Participants are missing if the process describes actions but does not name who -or what performs them. +Category is "missing" if the process describes actions but does not name who or +what performs them. + +Category is "present" if each major action has an actor, role, department, +system, customer, partner, or regulator. **Default assumption (if user skips):** Use placeholder participant "Внутренний исполнитель". @@ -85,7 +91,7 @@ Use placeholder participant "Внутренний исполнитель". - Option C: Внутренний отдел + внешняя система - Free-form: "Другое: ___" -### 3.3 Happy path (priority 3, high) +### Category 3: happy_path (priority: high) **Trigger words for detection:** - сначала @@ -99,9 +105,12 @@ Use placeholder participant "Внутренний исполнитель". - одобрено **Detection heuristic:** -Happy path is missing if the text names a process goal but does not describe the +Category is "missing" if the text names a process goal but does not describe the main successful sequence from start to end. +Category is "present" if the input gives an ordered sequence of successful +states, tasks, or handoffs. + **Default assumption (if user skips):** Linear sequence, single end event. @@ -112,7 +121,7 @@ Linear sequence, single end event. - Option C: Параллельная подготовка нескольких материалов → объединение - Free-form: "Другое: ___" -### 3.4 Exception paths (priority 4, medium) +### Category 4: exception_paths (priority: medium) **Trigger words for detection:** - если не @@ -126,8 +135,11 @@ Linear sequence, single end event. - недоступен **Detection heuristic:** -Exception paths are missing if the text has only the happy path and does not say -what happens on rejection, timeout, validation failure, or unavailable system. +Category is "missing" if the text has only the happy path and does not say what +happens on rejection, timeout, validation failure, or unavailable system. + +Category is "present" if at least one rejection, timeout, cancellation, +escalation, or error path is described. **Default assumption (if user skips):** No alternative path; process ends on error or rejection. @@ -139,7 +151,7 @@ No alternative path; process ends on error or rejection. - Option C: Таймаут + повторная попытка - Free-form: "Другое: ___" -### 3.5 SLAs (priority 5, medium) +### Category 5: slas (priority: medium) **Trigger words for detection:** - срок @@ -153,8 +165,11 @@ No alternative path; process ends on error or rejection. - просрочка **Detection heuristic:** -SLAs are missing if the process contains manual or waiting steps but no explicit -time limits, deadlines, or timeout behavior. +Category is "missing" if the process contains manual or waiting steps but no +explicit time limits, deadlines, or timeout behavior. + +Category is "present" if the source gives deadlines, durations, timeout rules, +business-day limits, or explicit "no SLA" wording. **Default assumption (if user skips):** Use "По регламенту" with no specific timer. @@ -166,7 +181,7 @@ Use "По регламенту" with no specific timer. - Option C: 3 рабочих дня на полный цикл - Free-form: "Другое: ___" -### 3.6 Data ownership (priority 6, low) +### Category 6: data_ownership (priority: low) **Trigger words for detection:** - документ @@ -180,8 +195,11 @@ Use "По регламенту" with no specific timer. - система **Detection heuristic:** -Data ownership is missing if documents or data objects are mentioned but no -source system, owner, or storage point is specified. +Category is "missing" if documents or data objects are mentioned but no source +system, owner, or storage point is specified. + +Category is "present" if documents, files, records, or process variables have a +named owner, source system, or storage location. **Default assumption (if user skips):** Source system not specified. From 6d39ba21cd9d9f883c2293a33baaacc4c35c92e8 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:04:38 +0300 Subject: [PATCH 08/24] T-103: Add Wizard question routing --- references/clarification-wizard.md | 60 ++++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 11 deletions(-) diff --git a/references/clarification-wizard.md b/references/clarification-wizard.md index 009b48c..8ebcc49 100644 --- a/references/clarification-wizard.md +++ b/references/clarification-wizard.md @@ -213,28 +213,66 @@ Source system not specified. ## 4. Routing by missing count (4 scenarios) +### Algorithm + +1. After Step 1 (Camunda knowledge load), scan input for all 6 categories. +2. Count categories that are "missing" per Section 3 heuristics. +3. Sort missing categories by priority. +4. Route by count: + +| Missing count | Action | +|---|---| +| 0 | Skip Wizard, inform user "Всё понятно, перехожу к генерации", proceed to Step 2 | +| 1-2 | Ask all missing as questions | +| 3-5 | Ask top-N missing in priority order | +| 6+ | Offer choice: provide more details OR continue "with assumptions" | + +### Question composition rule + +- 1 question = 1 category. +- Never group categories in one question. +- Each question ends with: "Выбери вариант или впиши свой:" +- Each question has 2-4 pre-built options and one "Другое: ___" slot. + +### Hard limits + +- Maximum 5 questions per Wizard pass, with no exceptions. +- Rationale: the skill must not turn generation into a long interview. +- If 6+ categories are missing, ask no more than top-5 by priority. +- Missing facts below the cutoff become `⚠ Допущение:` after generation if the + user accepts assumption mode. + ### 4.1 Zero missing -> skip -If no missing categories are found, skip the Wizard. Inform the user: -"Всё понятно, перехожу к генерации." +Transcript: +- User: "Смоделируй процесс: клиент подаёт заявку, менеджер проверяет документы + за 24 часа, при отказе заявка закрывается, данные хранятся в CRM." +- Wizard: "Всё понятно, перехожу к генерации." ### 4.2 1-2 missing -> targeted questions -Ask all missing categories as 1-2 separate questions. One question covers one -category only. +Transcript: +- User: "Клиент подаёт заявку. Менеджер проверяет документы и отправляет договор." +- Wizard: "Какие сроки важны для процесса? Выбери вариант или впиши свой:" +- User: "24 часа на проверку." ### 4.3 3-5 missing -> priority-ordered questions -Ask all missing categories, sorted by priority. Do not group categories. +Transcript: +- User: "Смоделируй одобрение заявки: проверка, решение, подпись." +- Wizard asks, in order: topology, participants, exception_paths, slas, + data_ownership. +- User answers only topology and participants; skipped facts use defaults. ### 4.4 6+ missing -> offer "with assumptions" mode -Offer the user a choice: -- provide more detail -- continue "with assumptions" - -If assumption mode is accepted, do not ask questions. Apply defaults and mark -each accepted default with `⚠ Допущение:`. +Transcript: +- User: "Смоделируй процесс продаж." +- Wizard: "Недостаточно данных по 6 категориям. Можешь добавить описание или + продолжить с допущениями." +- User: "Делай с допущениями." +- Wizard proceeds without questions and marks accepted defaults as + `⚠ Допущение:`. ## 5. "With assumptions" mode From 9086545e60e50dd23faa2ee9088a81ea9b4bad9e Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:05:02 +0300 Subject: [PATCH 09/24] T-104: Add Wizard assumption mode --- references/clarification-wizard.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/references/clarification-wizard.md b/references/clarification-wizard.md index 8ebcc49..6b965ec 100644 --- a/references/clarification-wizard.md +++ b/references/clarification-wizard.md @@ -290,7 +290,8 @@ The user explicitly opts into assumption mode by saying any of: When activated: - Wizard does not ask questions - Each missing fact becomes a `⚠ Допущение:` annotation in BPMN -- Each assumption is eligible for the Excel sheet «Допущения» +- Each annotation gets a corresponding row in Excel sheet «Допущения» +- Excel sheet structure is defined in `references/excel-spec-template.md` (T-106) ### 5.2 Annotation generation rules From b7629e91261ee1ce118ce68b3dfd1959df4ddfce Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:05:36 +0300 Subject: [PATCH 10/24] T-105: Update SKILL.md with Wizard integration --- SKILL.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/SKILL.md b/SKILL.md index bb2faf5..a78a8cf 100644 --- a/SKILL.md +++ b/SKILL.md @@ -79,10 +79,30 @@ At top of generated XML: ### Step 1.5 — Clarification Wizard -Placeholder for v2.3.0 Wizard integration. If Step 0 routes to Generate, run the -Wizard only when process facts are missing; if nothing is missing, inform the -user and proceed to Step 2. Full behavior is defined in -`references/clarification-wizard.md` and filled in by T-105. +Run the Wizard after Camunda knowledge is available and before parsing the +process into BPMN elements. Use `references/clarification-wizard.md` as the +source of truth. + +Branching: +- If 0 missing facts are detected: skip Wizard, inform user "Всё понятно, перехожу к генерации", proceed to Step 2. +- If 1-5 missing facts are detected: ask targeted questions in priority order, then proceed to Step 2. +- If 6+ missing facts are detected: offer more detail or "with assumptions" mode, then proceed to Step 2. + +Assumption mode trigger phrases include "делай с допущениями", "генерируй с предположениями", "не задавай вопросов", "as is", "as-is", and "just do it". + +#### Discipline rules for Wizard (Step 1.5) + +**Do NOT:** +- Ask questions when answer is in the source text (anti-hallucination) +- Mark as `⚠ Допущение:` what is trivially derivable (e.g., task type from verb) +- Ask more than 5 questions in one pass +- Skip Wizard silently — always inform user "Всё понятно, перехожу к генерации" + +**Do:** +- Detect all 6 categories before deciding routing +- Respect priority order (topology first, data_ownership last) +- Use category default if user skips ("не знаю / пропустить") +- Mark every accepted assumption with `⚠ Допущение:` annotation + Excel row ### Step 2 — Parse and classify the input From e33b9ffde2913bb9ac93be5201d067d3b216ee08 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:06:47 +0300 Subject: [PATCH 11/24] T-403: Create wizard fixtures --- .../catastrophic_input/expected_assumption_mode_offer.json | 6 ++++++ .../wizard/catastrophic_input/one_sentence_description.txt | 1 + .../wizard/complete_input/expected_wizard_skipped.json | 6 ++++++ tests/fixtures/wizard/complete_input/full_bnpl_process.txt | 1 + .../partial_input/expected_questions_missing_sla.json | 6 ++++++ .../expected_questions_missing_sla_and_data.json | 6 ++++++ tests/fixtures/wizard/partial_input/missing_sla.txt | 1 + .../fixtures/wizard/partial_input/missing_sla_and_data.txt | 1 + tests/fixtures/wizard/sparse_input/expected_questions.json | 6 ++++++ tests/fixtures/wizard/sparse_input/happy_path_only.txt | 1 + 10 files changed, 35 insertions(+) create mode 100644 tests/fixtures/wizard/catastrophic_input/expected_assumption_mode_offer.json create mode 100644 tests/fixtures/wizard/catastrophic_input/one_sentence_description.txt create mode 100644 tests/fixtures/wizard/complete_input/expected_wizard_skipped.json create mode 100644 tests/fixtures/wizard/complete_input/full_bnpl_process.txt create mode 100644 tests/fixtures/wizard/partial_input/expected_questions_missing_sla.json create mode 100644 tests/fixtures/wizard/partial_input/expected_questions_missing_sla_and_data.json create mode 100644 tests/fixtures/wizard/partial_input/missing_sla.txt create mode 100644 tests/fixtures/wizard/partial_input/missing_sla_and_data.txt create mode 100644 tests/fixtures/wizard/sparse_input/expected_questions.json create mode 100644 tests/fixtures/wizard/sparse_input/happy_path_only.txt diff --git a/tests/fixtures/wizard/catastrophic_input/expected_assumption_mode_offer.json b/tests/fixtures/wizard/catastrophic_input/expected_assumption_mode_offer.json new file mode 100644 index 0000000..57ba140 --- /dev/null +++ b/tests/fixtures/wizard/catastrophic_input/expected_assumption_mode_offer.json @@ -0,0 +1,6 @@ +{ + "missing_categories": ["topology", "participants", "happy_path", "exception_paths", "slas", "data_ownership"], + "questions_count": 0, + "wizard_invoked": false, + "offer_assumption_mode": true +} diff --git a/tests/fixtures/wizard/catastrophic_input/one_sentence_description.txt b/tests/fixtures/wizard/catastrophic_input/one_sentence_description.txt new file mode 100644 index 0000000..d5e1936 --- /dev/null +++ b/tests/fixtures/wizard/catastrophic_input/one_sentence_description.txt @@ -0,0 +1 @@ +Смоделируй процесс продаж для новой услуги без дополнительных деталей, потому что описание ещё не готово, участники не определены, роли неизвестны, успешный путь не описан, исключения и отказы не перечислены, сроки и SLA не утверждены, источники документов и владельцы данных не выбраны, а формат взаимодействия между подразделениями или внешними участниками пока остаётся открытым. diff --git a/tests/fixtures/wizard/complete_input/expected_wizard_skipped.json b/tests/fixtures/wizard/complete_input/expected_wizard_skipped.json new file mode 100644 index 0000000..bc93397 --- /dev/null +++ b/tests/fixtures/wizard/complete_input/expected_wizard_skipped.json @@ -0,0 +1,6 @@ +{ + "missing_categories": [], + "questions_count": 0, + "wizard_invoked": false, + "offer_assumption_mode": false +} diff --git a/tests/fixtures/wizard/complete_input/full_bnpl_process.txt b/tests/fixtures/wizard/complete_input/full_bnpl_process.txt new file mode 100644 index 0000000..30b3e59 --- /dev/null +++ b/tests/fixtures/wizard/complete_input/full_bnpl_process.txt @@ -0,0 +1 @@ +Процесс одобрения BNPL-заявки в банке начинается, когда клиент подаёт заявку через мобильное приложение и прикладывает паспортные данные. В одном пуле банка участвуют лэйны Клиентский менеджер, Кредитный аналитик, Risk Engine и СЭД. Клиентский менеджер проверяет комплектность, Risk Engine выполняет автоматический скоринг, кредитный аналитик принимает решение. При одобрении договор передаётся клиенту на подпись, при отказе заявка закрывается с уведомлением. Проверка комплектности выполняется за 24 часа, полный цикл занимает до 3 рабочих дней. Документы хранятся в СЭД, заявочные данные являются master data в LOS. diff --git a/tests/fixtures/wizard/partial_input/expected_questions_missing_sla.json b/tests/fixtures/wizard/partial_input/expected_questions_missing_sla.json new file mode 100644 index 0000000..3d5f318 --- /dev/null +++ b/tests/fixtures/wizard/partial_input/expected_questions_missing_sla.json @@ -0,0 +1,6 @@ +{ + "missing_categories": ["slas"], + "questions_count": 1, + "expected_question_about": "slas", + "wizard_invoked": true +} diff --git a/tests/fixtures/wizard/partial_input/expected_questions_missing_sla_and_data.json b/tests/fixtures/wizard/partial_input/expected_questions_missing_sla_and_data.json new file mode 100644 index 0000000..b897336 --- /dev/null +++ b/tests/fixtures/wizard/partial_input/expected_questions_missing_sla_and_data.json @@ -0,0 +1,6 @@ +{ + "missing_categories": ["slas", "data_ownership"], + "questions_count": 2, + "expected_question_about": ["slas", "data_ownership"], + "wizard_invoked": true +} diff --git a/tests/fixtures/wizard/partial_input/missing_sla.txt b/tests/fixtures/wizard/partial_input/missing_sla.txt new file mode 100644 index 0000000..7ecc254 --- /dev/null +++ b/tests/fixtures/wizard/partial_input/missing_sla.txt @@ -0,0 +1 @@ +Процесс одобрения BNPL-заявки в банке выполняется в одном пуле с лэйнами Клиент, Менеджер кредитного отдела и Risk Engine. Клиент подаёт заявку через мобильное приложение и прикладывает документы. Менеджер проверяет комплектность, Risk Engine выполняет скоринг, после этого менеджер принимает решение. При одобрении договор отправляется клиенту на подпись, при отказе заявка закрывается с уведомлением. Заявка и документы хранятся в LOS и СЭД. Если клиент не подписывает договор, заявка закрывается. diff --git a/tests/fixtures/wizard/partial_input/missing_sla_and_data.txt b/tests/fixtures/wizard/partial_input/missing_sla_and_data.txt new file mode 100644 index 0000000..5decb48 --- /dev/null +++ b/tests/fixtures/wizard/partial_input/missing_sla_and_data.txt @@ -0,0 +1 @@ +Процесс обработки заявки на кредит начинается, когда клиент отправляет анкету через сайт. В одном пуле банка работают менеджер, кредитный аналитик и автоматическая скоринговая система. Сначала менеджер проверяет анкету, затем система считает скоринг, после этого аналитик принимает решение. Если решение положительное, клиент получает договор на подпись. Если решение отрицательное, клиент получает отказ. В тексте процесса не указаны конкретные сроки выполнения этапов и не описано, где хранятся документы или кто является владельцем данных. diff --git a/tests/fixtures/wizard/sparse_input/expected_questions.json b/tests/fixtures/wizard/sparse_input/expected_questions.json new file mode 100644 index 0000000..cce7d19 --- /dev/null +++ b/tests/fixtures/wizard/sparse_input/expected_questions.json @@ -0,0 +1,6 @@ +{ + "missing_categories": ["topology", "participants", "exception_paths", "slas", "data_ownership"], + "questions_count": 5, + "expected_priority_order": ["topology", "participants", "exception_paths", "slas", "data_ownership"], + "wizard_invoked": true +} diff --git a/tests/fixtures/wizard/sparse_input/happy_path_only.txt b/tests/fixtures/wizard/sparse_input/happy_path_only.txt new file mode 100644 index 0000000..6ed23eb --- /dev/null +++ b/tests/fixtures/wizard/sparse_input/happy_path_only.txt @@ -0,0 +1 @@ +Смоделируй процесс согласования договора. Заявка поступает, затем выполняется проверка, после проверки принимается решение, потом договор отправляется на подпись и процесс завершается. Основной успешный путь понятен: получить заявку, проверить, согласовать, отправить договор, получить подпись. В описании нет информации о топологии, ролях исполнителей, исключениях, сроках выполнения и владельцах документов. Нужно построить BPMN для Camunda 7 без дополнительных вводных. From a9278ea732f19ab63683e3c15f25db41850f0811 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:08:47 +0300 Subject: [PATCH 12/24] T-402: Create test_wizard.py --- references/clarification-wizard.md | 2 +- tests/release/test_wizard.py | 200 +++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 tests/release/test_wizard.py diff --git a/references/clarification-wizard.md b/references/clarification-wizard.md index 6b965ec..a38eee8 100644 --- a/references/clarification-wizard.md +++ b/references/clarification-wizard.md @@ -290,7 +290,7 @@ The user explicitly opts into assumption mode by saying any of: When activated: - Wizard does not ask questions - Each missing fact becomes a `⚠ Допущение:` annotation in BPMN -- Each annotation gets a corresponding row in Excel sheet «Допущения» +- Each annotation gets a corresponding row in Excel Sheet «Допущения» - Excel sheet structure is defined in `references/excel-spec-template.md` (T-106) ### 5.2 Annotation generation rules diff --git a/tests/release/test_wizard.py b/tests/release/test_wizard.py new file mode 100644 index 0000000..de2c749 --- /dev/null +++ b/tests/release/test_wizard.py @@ -0,0 +1,200 @@ +import json +import re +import xml.etree.ElementTree as ET +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[2] +FIXTURES = REPO_ROOT / "tests" / "fixtures" / "wizard" +WIZARD_DOC = REPO_ROOT / "references" / "clarification-wizard.md" +ANNOTATION_DOC = REPO_ROOT / "references" / "annotation-style-guide.md" + +PRIORITY = ["topology", "participants", "happy_path", "exception_paths", "slas", "data_ownership"] +ASSUMPTION_TRIGGERS = ( + "делай с допущениями", + "генерируй с предположениями", + "не задавай вопросов", + "генерируй без вопросов", + "as is", + "as-is", + "just do it", +) + + +def load_text(relative_path): + return (FIXTURES / relative_path).read_text(encoding="utf-8") + + +def load_json(relative_path): + return json.loads((FIXTURES / relative_path).read_text(encoding="utf-8")) + + +def detects_assumption_mode(text): + lowered = text.lower() + return any(trigger in lowered for trigger in ASSUMPTION_TRIGGERS) + + +def detect_missing_categories(text): + lowered = text.lower() + missing = [] + + if not re.search(r"\b(пул|пуле|пула|лэйн|лэйны|lane|pool|collaboration|message flows?)\b", lowered): + missing.append("topology") + says_participants_missing = re.search(r"(участники не|роли неизвестны|исполнители не)", lowered) + if not re.search(r"(клиент|менеджер|аналитик|risk engine|система|исполнитель|отдел|банк)", lowered) or says_participants_missing: + missing.append("participants") + says_happy_path_missing = re.search(r"(успешный путь не|happy path не)", lowered) + if not re.search(r"(сначала|затем|после|потом|при одобрении|успешн|пода[её]т)", lowered) or says_happy_path_missing: + missing.append("happy_path") + says_exception_missing = re.search(r"(исключения .*не|отказы не|ошибки не)", lowered) + if not re.search(r"(при отказ|отказ|если|таймаут|ошиб|закрывается|эскалац)", lowered) or says_exception_missing: + missing.append("exception_paths") + has_sla = re.search(r"(24 часа|3 рабочих|sla:|срок выполнения|таймаут \d+)", lowered) + says_sla_missing = re.search(r"(сроки .*не|не указаны .*срок|sla не|сроки и sla не)", lowered) + if not has_sla or says_sla_missing: + missing.append("slas") + has_data = re.search(r"\b(los|crm|сэд|master data|source system|хранятся|владелец данных)\b", lowered) + says_data_missing = re.search(r"(где хранятся|владельц[а-я ]+не|источники .*не выбраны)", lowered) + if not has_data or says_data_missing: + missing.append("data_ownership") + + return [category for category in PRIORITY if category in missing] + + +def route_wizard(text, assumption_command=False): + missing = detect_missing_categories(text) + if assumption_command or detects_assumption_mode(text): + return { + "missing_categories": missing, + "questions": [], + "wizard_invoked": False, + "offer_assumption_mode": False, + "assumptions_marked": bool(missing), + } + if len(missing) == 0: + return {"missing_categories": missing, "questions": [], "wizard_invoked": False} + if len(missing) >= 6: + return { + "missing_categories": missing, + "questions": [], + "wizard_invoked": False, + "offer_assumption_mode": True, + } + return { + "missing_categories": missing, + "questions": missing[:5], + "wizard_invoked": True, + "offer_assumption_mode": False, + } + + +def build_assumption_annotation(): + return """ + + + + ⚠ Допущение: SLA на ручную проверку — 24 часа. +В исходнике срок не указан, принят default из category slas. + + + +""" + + +@pytest.mark.parametrize( + "category, text", + [ + ("topology", "Клиент подаёт заявку, менеджер проверяет документы за 24 часа, при отказе заявка закрывается, данные хранятся в LOS."), + ("participants", "В одном пуле с лэйнами выполняется заявка: сначала проверка, затем решение, при отказе закрытие, SLA 24 часа, данные в LOS."), + ("happy_path", "В одном пуле банк и клиент участвуют в процессе, менеджер отвечает за действия, при отказе закрытие, SLA 24 часа, данные в LOS."), + ("exception_paths", "В одном пуле менеджер сначала проверяет заявку, затем одобряет договор за 24 часа, данные хранятся в LOS."), + ("slas", "В одном пуле менеджер сначала проверяет заявку, затем принимает решение, при отказе закрывает процесс, данные в LOS."), + ("data_ownership", "В одном пуле менеджер сначала проверяет заявку за 24 часа, затем принимает решение, при отказе закрывает процесс."), + ], +) +def test_category_detection_individual_missing(category, text): + assert category in detect_missing_categories(text) + + +def test_complete_input_skips_wizard(): + expected = load_json("complete_input/expected_wizard_skipped.json") + result = route_wizard(load_text("complete_input/full_bnpl_process.txt")) + + assert result["missing_categories"] == expected["missing_categories"] + assert len(result["questions"]) == expected["questions_count"] + assert result["wizard_invoked"] is expected["wizard_invoked"] + + +@pytest.mark.parametrize( + "fixture, expected_fixture", + [ + ("partial_input/missing_sla.txt", "partial_input/expected_questions_missing_sla.json"), + ("partial_input/missing_sla_and_data.txt", "partial_input/expected_questions_missing_sla_and_data.json"), + ], +) +def test_partial_input_asks_one_or_two_questions(fixture, expected_fixture): + expected = load_json(expected_fixture) + result = route_wizard(load_text(fixture)) + + assert result["missing_categories"] == expected["missing_categories"] + assert len(result["questions"]) == expected["questions_count"] + assert result["wizard_invoked"] is True + + +def test_sparse_input_uses_priority_order_and_hard_limit(): + expected = load_json("sparse_input/expected_questions.json") + result = route_wizard(load_text("sparse_input/happy_path_only.txt")) + + assert result["questions"] == expected["expected_priority_order"] + assert len(result["questions"]) == 5 + assert result["wizard_invoked"] is True + + +def test_catastrophic_input_offers_assumption_mode(): + expected = load_json("catastrophic_input/expected_assumption_mode_offer.json") + result = route_wizard(load_text("catastrophic_input/one_sentence_description.txt")) + + assert result["missing_categories"] == expected["missing_categories"] + assert result["offer_assumption_mode"] is True + assert result["wizard_invoked"] is False + + +@pytest.mark.parametrize("trigger", ASSUMPTION_TRIGGERS) +def test_assumption_mode_trigger_phrases(trigger): + result = route_wizard(f"{trigger}. Смоделируй процесс продаж.") + + assert result["wizard_invoked"] is False + assert result["assumptions_marked"] is True + + +def test_assumption_annotation_xml_contract(): + xml = build_assumption_annotation() + root = ET.fromstring(xml) + text = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}text").text + association = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}association") + + assert text.startswith("⚠ Допущение:") + assert association.attrib["sourceRef"] == "Activity_Manual_Review" + assert association.attrib["targetRef"] == "TextAnnotation_Assumption_1" + + +def test_skip_command_skips_wizard_without_questions(): + result = route_wizard("генерируй без вопросов. Процесс: заявка поступает.") + + assert result["wizard_invoked"] is False + assert result["questions"] == [] + + +def test_wizard_docs_contract(): + wizard = WIZARD_DOC.read_text(encoding="utf-8") + annotation = ANNOTATION_DOC.read_text(encoding="utf-8") + + assert "Maximum 5 questions" in wizard + assert "1 question = 1 category" in wizard + assert "Sheet «Допущения»" in wizard + assert "Annotation prefix: ⚠ Допущение:" in annotation + assert "When NOT to mark as Допущение" in annotation From 8f34ae52e86cf9194c3c5711baedee24c4209f3e Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:10:20 +0300 Subject: [PATCH 13/24] T-106: Add assumptions sheet to excel template --- references/excel-spec-template.md | 141 ++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/references/excel-spec-template.md b/references/excel-spec-template.md index d0fbb32..0ea679e 100644 --- a/references/excel-spec-template.md +++ b/references/excel-spec-template.md @@ -69,6 +69,147 @@ --- +## Sheet 4: Допущения (Assumptions) + +### Purpose + +Review checklist for the user: what the model assumed during Clarification Wizard or "with assumptions" mode. Each row = one explicitly accepted assumption, synchronized with `` in BPMN-XML via ID. + +### When the sheet is generated + +| Scenario | Sheet generated? | +|---|---| +| Wizard NOT invoked (0 missing facts) | No | +| Wizard invoked, all questions answered | No | +| Wizard invoked, ≥1 answer was "skip" | Yes | +| "With assumptions" mode activated | Yes | +| Update scenario, Wizard refined only additions | Yes | + +**Rule:** if final assumption count = 0 — sheet is NOT created (no empty sheets). + +### Column structure + +| # | Column | Type | Required | Example | Purpose | +|---|---|---|---|---|---| +| 1 | `ID допущения` | text | yes | `Assumption_3` | Maps to `` in XML | +| 2 | `Целевой узел BPMN` | text | yes | `Activity_Manual_Review` | ID of node the assumption applies to | +| 3 | `Тип целевого узла` | enum | yes | `userTask` | task / userTask / serviceTask / gateway / event / pool / lane / process | +| 4 | `Категория` | enum | yes | `slas` | One of 6: topology / participants / happy_path / exception_paths / slas / data_ownership | +| 5 | `Текст допущения` | text | yes | "SLA на ручную проверку — 24 часа" | What was assumed | +| 6 | `Обоснование` | text | yes | "В исходнике срок не указан, принят default из category SLA" | Why this value | +| 7 | `Risk` | enum | yes | `medium` | low / medium / high (per Risk model below) | +| 8 | `Источник default'а` | text | **no (optional)** | `clarification-wizard.md §3.4` | Reference where default was taken | +| 9 | `Статус ревью` | enum | yes | `pending` | Always pre-fills to `pending`. User updates: pending / accepted / rejected / modified | + +### Excel formatting + +- Header row: bold, fill `#efeae0` +- `Risk = high` rows: cell fill `#fde2e2` (light red) on Risk column +- `Risk = medium` rows: cell fill `#fff3d6` (light gold) on Risk column +- `Risk = low` rows: no fill +- Columns 5 and 6: wrap text, width 40 +- Column 9 (Статус ревью): data validation dropdown with 4 values +- Freeze pane: row 1 + +### BPMN reconciliation + +ID mapping rule: Excel `Assumption_N` ↔ BPMN `TextAnnotation_Assumption_N`. + +Example: + +```xml + + ⚠ Допущение: SLA на ручную проверку — 24 часа. +В исходнике срок не указан, принят по типичной практике. + + + +``` + +| Excel column | XML location | +|---|---| +| `ID допущения` | `textAnnotation/@id` (without `TextAnnotation_` prefix) | +| `Целевой узел BPMN` | `association/@sourceRef` | +| `Тип целевого узла` | derived from BPMN element with `id=sourceRef` | +| `Текст допущения` | line 1 of `textAnnotation/text/text()` (after `⚠ Допущение:` prefix) | +| `Обоснование` | line 2+ of `textAnnotation/text/text()` | +| `Категория`, `Risk`, `Источник default'а`, `Статус ревью` | Excel-only (not in BPMN) | + +### Risk model + +**Default Risk by category** + override conditions: + +| Category | Default Risk | Override → high | Override → medium | Override → low | +|---|---|---|---|---| +| topology | high | external partner assumed | — | name-only assumption (no structural change) | +| participants | medium | external partner / regulator | — | — | +| happy_path | medium | entire step invented (not in source) | — | — | +| exception_paths | high | — | only missing escalation path | — | +| slas | medium | SLA <1h or >7d (atypical) | — | — | +| data_ownership | low | PII/KYC data | — | — | + +**Risk meaning:** "how much BPMN to redo if this assumption is wrong" + "is there compliance/regulatory risk". + +**Rule:** if 30%+ of assumptions are `Risk = high` — model must warn user in final message: "Высокая концентрация high-risk допущений. Рекомендую запустить Wizard повторно с расширенным вводом." + +### Sheet template (5-row example) + +| ID | Целевой узел | Тип | Категория | Текст допущения | Обоснование | Risk | Источник | Статус | +|---|---|---|---|---|---|---|---|---| +| Assumption_1 | Pool_BNPL_Operator | pool | participants | Operator — внутренний отдел операционного риска | В тексте упомянут "оператор", конкретный отдел не указан | medium | clarification-wizard.md §3.2 | pending | +| Assumption_2 | Activity_Document_Verify | userTask | participants | Документы проверяет junior credit officer | Не указан исполнитель, default из category participants | low | clarification-wizard.md §3.2 | pending | +| Assumption_3 | Activity_Manual_Review | userTask | slas | SLA на ручную проверку — 24 часа | Срок не указан, default из category SLA | medium | clarification-wizard.md §3.5 | pending | +| Assumption_4 | Gateway_Risk_Score | exclusiveGateway | exception_paths | При rejected → process end (без эскалации) | Не описан альтернативный путь, выбран наиболее частый сценарий | high | clarification-wizard.md §3.4 | pending | +| Assumption_5 | DataObject_Loan_Application | dataObject | data_ownership | Owner объекта — система LOS | Owner не указан, по контексту определена как LOS | low | clarification-wizard.md §3.6 | pending | + +### Edge cases + +**E1. Assumption about absence of element.** `Целевой узел` = main gateway where alternative path was expected. Text: "Альтернативный путь после Gateway_X не предусмотрен." + +**E2. Process-level assumption (no specific node).** `Целевой узел` = `(process root)`, `Тип` = `process`. In BPMN: `` without `` (or association on the root process element). + +**E3. Cascading assumptions.** If one assumption produces another ("Operator = internal department" → "Operator has access to LOS"), record as 2 separate rows. Second row's `Обоснование` references first: "Следствие из Assumption_1." + +**E4. Assumption rejected after generation.** User changes `Статус ревью` to `rejected`. Does NOT trigger automatic regeneration. Reconciliation report shows warning: "Assumption_3 rejected — требуется регенерация процесса с уточнённым SLA." + +### Changes to other sheets when «Допущения» exists + +**Sheet 1 (Спецификация):** add (optional) column "Связанные допущения" — list of Assumption_N IDs affecting this row. Format: `Assumption_1, Assumption_3`. Empty if none. + +**Sheet 2 (Участники):** if a participant comes from a `participants`-category assumption, add row with `Источник` column = `Допущение: Assumption_N`. + +**Sheet 3 (Открытые вопросы):** do NOT duplicate assumption content. Open questions = unresolved gaps in source. Assumptions = facts model accepted on user's behalf. Different entities. + +### Reconciliation checks (extends `references/reconciliation-procedure.md`) + +| # | Check | Violation | Severity | +|---|---|---|---| +| R-10 | Each row in «Допущения» has matching `` in BPMN | Allegation without annotation | ERROR | +| R-11 | Each `` with `⚠ Допущение:` prefix has Excel row | Annotation without record | ERROR | +| R-12 | `Целевой узел BPMN` exists in schema (id from `association.sourceRef` resolves) | Assumption points to nonexistent node | ERROR | +| R-13 | `Тип целевого узла` in Excel matches actual BPMN element type | Type mismatch | WARNING | + +### Anti-hallucination rules + +**Do NOT include in «Допущения»:** +- Facts explicitly stated in source +- Trivially derivable from context (task type from verb) +- Standard BPMN conventions +- Meta-information about model's working approach + +**Do include:** +- Any SLA not in source but present in BPMN +- Any assumed executor/role +- Any alternative path not described +- Any assumed data owner +- Any topology assumption (single vs collaboration) + +**Control question for the model:** "If user rejects this assumption now, what in BPMN must change?" If answer is "nothing" → don't add. If "≥1 element" → add. + +--- + ## Mapping: BPMN element → row content Таблица соответствий, чтобы заполнение колонок было одинаковым для одинаковых элементов. From 4996dbc61d0fa2ea78e13986f7b99b179cb6de40 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:10:58 +0300 Subject: [PATCH 14/24] T-201: Extend input classification mixed input --- references/input-classification.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/references/input-classification.md b/references/input-classification.md index 63732b9..35f016d 100644 --- a/references/input-classification.md +++ b/references/input-classification.md @@ -93,6 +93,21 @@ Recovery rule: - If recovery is uncertain, reject with the parse error message - Do not invent missing BPMN content during recovery +### 2.6 Mixed input (text + BPMN, Update scenario) + +**Detection:** +- User message contains ` Date: Mon, 27 Apr 2026 02:11:38 +0300 Subject: [PATCH 15/24] T-202: Create reuse-id rules --- references/reuse-id-rules.md | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 references/reuse-id-rules.md diff --git a/references/reuse-id-rules.md b/references/reuse-id-rules.md new file mode 100644 index 0000000..231ebe0 --- /dev/null +++ b/references/reuse-id-rules.md @@ -0,0 +1,48 @@ +# Reuse-ID rules (Mixed input / Update scenario) + +## Purpose + +When Generate mode receives mixed input (text + old .bpmn), the model extracts existing element IDs and preserves them where semantically appropriate. + +## Rules + +| Case | Action | +|---|---| +| Node from old XML present in new BPMN by meaning (same name, same type) | **Reuse ID** | +| Node renamed (new name, same type, same graph position) | **Reuse ID + diff-summary entry** | +| Node deleted (in old, not in new) | **Drop ID + explicit summary entry** | +| Node added (in new, not in old) | **New ID per naming convention** | +| Node changed type (e.g., `task` → `userTask`) | **DO NOT reuse — new ID + summary entry** | +| ID collision (new ID matches deleted old ID) | **Append `_2` suffix, fail loudly with warning** | + +## Diff-summary format + +After generation, output a text block (not XLSX in Variant C): + +```text +ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN: +- Сохранено ID: узлов () +- Переименовано (ID сохранены): узлов + - (id: ) +- Добавлено новых: узлов + - : , +- Удалено: узлов + - : , +- Тип изменён (новый ID): узлов + - Old: , → New: , +``` + +## Wizard interaction in Update scenario + +- Wizard runs only on NEW or CHANGED parts of the process +- Existing facts (from old BPMN) are NOT re-asked +- If user adds a new step but doesn't specify SLA → Wizard asks about SLA only for the new step +- If user renames an existing step → no new questions; ID preserved + +## Edge cases + +**EC1. Old BPMN has duplicate IDs.** Model MUST treat this as invalid input. Reject with: "Исходный BPMN содержит дубликаты ID: . Исправьте перед обновлением." + +**EC2. User explicitly asks to rename an ID.** Honor the request, log in diff-summary as "rename: old_id → new_id". + +**EC3. Old BPMN has zeebe namespace.** Reject (per T-201 conflict handling). From ae01d3cd293ac3822c48e2664f9978e5c5fddaf2 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:12:12 +0300 Subject: [PATCH 16/24] T-203: Update SKILL.md with Update scenario --- SKILL.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/SKILL.md b/SKILL.md index a78a8cf..0a5a7a6 100644 --- a/SKILL.md +++ b/SKILL.md @@ -45,6 +45,24 @@ Route pure text to Generate, mixed text + BPMN/XML to Generate with reuse-ID, Camunda 8 / `zeebe:*` XML to REJECT with Diagram Converter guidance, unsupported formats to REJECT, and invalid XML to RECOVER or REJECT with the parse error. +#### Update scenario (mixed input) + +When Step 0 detects mixed input (text + .bpmn), workflow becomes: + +1. Step 0: detected as mixed input +2. Reuse-ID extraction: parse old BPMN, build ID index +3. Step 1: Camunda knowledge load (unchanged) +4. Step 1.5: Wizard runs only on new/changed parts (per `references/reuse-id-rules.md`) +5. Steps 2-9: generate new BPMN preserving existing IDs where appropriate +6. Output: + - Final BPMN (with reused + new IDs) + - Excel spec (per existing template, +«Допущения» sheet if applicable) + - Diff-summary text block (see reuse-id-rules.md) + +**Cross-references:** +- `references/reuse-id-rules.md` — full ID reuse rules +- `references/clarification-wizard.md` — Wizard behavior in Update scenario + ### Step 1 — Load current Camunda documentation (with fallback) Before producing any BPMN XML, acquire Camunda knowledge. Try in order: From 84122a1a47223c8d30c7ffebc041ca34ae382275 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:13:55 +0300 Subject: [PATCH 17/24] T-404: Create mixed input tests --- .../add_nodes/expected_diff_summary.json | 6 + .../mixed_input/add_nodes/original.bpmn | 10 ++ .../mixed_input/add_nodes/update_text.txt | 1 + .../remove_nodes/expected_diff_summary.json | 6 + .../mixed_input/remove_nodes/original.bpmn | 10 ++ .../mixed_input/remove_nodes/update_text.txt | 1 + .../type_change/expected_diff_summary.json | 6 + .../expected_new_id_for_type_change.json | 6 + .../mixed_input/type_change/original.bpmn | 8 ++ .../mixed_input/type_change/update_text.txt | 1 + tests/release/test_mixed_input.py | 106 ++++++++++++++++++ 11 files changed, 161 insertions(+) create mode 100644 tests/fixtures/mixed_input/add_nodes/expected_diff_summary.json create mode 100644 tests/fixtures/mixed_input/add_nodes/original.bpmn create mode 100644 tests/fixtures/mixed_input/add_nodes/update_text.txt create mode 100644 tests/fixtures/mixed_input/remove_nodes/expected_diff_summary.json create mode 100644 tests/fixtures/mixed_input/remove_nodes/original.bpmn create mode 100644 tests/fixtures/mixed_input/remove_nodes/update_text.txt create mode 100644 tests/fixtures/mixed_input/type_change/expected_diff_summary.json create mode 100644 tests/fixtures/mixed_input/type_change/expected_new_id_for_type_change.json create mode 100644 tests/fixtures/mixed_input/type_change/original.bpmn create mode 100644 tests/fixtures/mixed_input/type_change/update_text.txt create mode 100644 tests/release/test_mixed_input.py diff --git a/tests/fixtures/mixed_input/add_nodes/expected_diff_summary.json b/tests/fixtures/mixed_input/add_nodes/expected_diff_summary.json new file mode 100644 index 0000000..e9fc66d --- /dev/null +++ b/tests/fixtures/mixed_input/add_nodes/expected_diff_summary.json @@ -0,0 +1,6 @@ +{ + "preserved": ["StartEvent_Request", "Activity_CheckDocs", "Activity_Score", "Gateway_Decision", "EndEvent_Approved"], + "added": ["Activity_ManualReview", "Activity_NotifyClient"], + "removed": [], + "type_changed": [] +} diff --git a/tests/fixtures/mixed_input/add_nodes/original.bpmn b/tests/fixtures/mixed_input/add_nodes/original.bpmn new file mode 100644 index 0000000..4b9dfaf --- /dev/null +++ b/tests/fixtures/mixed_input/add_nodes/original.bpmn @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/fixtures/mixed_input/add_nodes/update_text.txt b/tests/fixtures/mixed_input/add_nodes/update_text.txt new file mode 100644 index 0000000..4be87da --- /dev/null +++ b/tests/fixtures/mixed_input/add_nodes/update_text.txt @@ -0,0 +1 @@ +Дополни существующий BPMN двумя новыми шагами после скоринга: выполнить ручную проверку при пограничном результате и уведомить клиента о результате. Остальные элементы и их ID должны сохраниться без изменений. diff --git a/tests/fixtures/mixed_input/remove_nodes/expected_diff_summary.json b/tests/fixtures/mixed_input/remove_nodes/expected_diff_summary.json new file mode 100644 index 0000000..d5c063c --- /dev/null +++ b/tests/fixtures/mixed_input/remove_nodes/expected_diff_summary.json @@ -0,0 +1,6 @@ +{ + "preserved": ["StartEvent_Order", "Activity_CheckStock", "Activity_TakePayment", "EndEvent_OrderPaid"], + "added": [], + "removed": ["Activity_ReserveStock"], + "type_changed": [] +} diff --git a/tests/fixtures/mixed_input/remove_nodes/original.bpmn b/tests/fixtures/mixed_input/remove_nodes/original.bpmn new file mode 100644 index 0000000..5031ffe --- /dev/null +++ b/tests/fixtures/mixed_input/remove_nodes/original.bpmn @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/fixtures/mixed_input/remove_nodes/update_text.txt b/tests/fixtures/mixed_input/remove_nodes/update_text.txt new file mode 100644 index 0000000..b5ef632 --- /dev/null +++ b/tests/fixtures/mixed_input/remove_nodes/update_text.txt @@ -0,0 +1 @@ +Измени существующий BPMN: шаг резервирования товара больше не нужен, потому что резервирование выполняется автоматически внутри проверки наличия. Остальные узлы нужно сохранить с прежними ID. diff --git a/tests/fixtures/mixed_input/type_change/expected_diff_summary.json b/tests/fixtures/mixed_input/type_change/expected_diff_summary.json new file mode 100644 index 0000000..e6515df --- /dev/null +++ b/tests/fixtures/mixed_input/type_change/expected_diff_summary.json @@ -0,0 +1,6 @@ +{ + "preserved": ["StartEvent_DraftReady", "EndEvent_Sent"], + "added": ["Activity_SendAgreement_User"], + "removed": [], + "type_changed": ["Activity_SendAgreement"] +} diff --git a/tests/fixtures/mixed_input/type_change/expected_new_id_for_type_change.json b/tests/fixtures/mixed_input/type_change/expected_new_id_for_type_change.json new file mode 100644 index 0000000..cd316af --- /dev/null +++ b/tests/fixtures/mixed_input/type_change/expected_new_id_for_type_change.json @@ -0,0 +1,6 @@ +{ + "old_id": "Activity_SendAgreement", + "new_id": "Activity_SendAgreement_User", + "old_type": "task", + "new_type": "userTask" +} diff --git a/tests/fixtures/mixed_input/type_change/original.bpmn b/tests/fixtures/mixed_input/type_change/original.bpmn new file mode 100644 index 0000000..21e91de --- /dev/null +++ b/tests/fixtures/mixed_input/type_change/original.bpmn @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/fixtures/mixed_input/type_change/update_text.txt b/tests/fixtures/mixed_input/type_change/update_text.txt new file mode 100644 index 0000000..d0eda9c --- /dev/null +++ b/tests/fixtures/mixed_input/type_change/update_text.txt @@ -0,0 +1 @@ +Обнови существующий BPMN: действие "Направить соглашение" должно стать userTask, потому что его выполняет сотрудник банка вручную через корпоративную почту. Старый generic task ID не переиспользуй. diff --git a/tests/release/test_mixed_input.py b/tests/release/test_mixed_input.py new file mode 100644 index 0000000..c9ecd2d --- /dev/null +++ b/tests/release/test_mixed_input.py @@ -0,0 +1,106 @@ +import json +import xml.etree.ElementTree as ET +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +FIXTURES = REPO_ROOT / "tests" / "fixtures" / "mixed_input" +REUSE_DOC = REPO_ROOT / "references" / "reuse-id-rules.md" + +BPMN_NS = "{http://www.omg.org/spec/BPMN/20100524/MODEL}" + + +def load_json(path): + return json.loads(path.read_text(encoding="utf-8")) + + +def extract_nodes(bpmn_path): + root = ET.parse(bpmn_path).getroot() + nodes = {} + for elem in root.iter(): + if not elem.tag.startswith(BPMN_NS): + continue + node_id = elem.attrib.get("id") + name = elem.attrib.get("name") + if node_id and name: + nodes[node_id] = { + "id": node_id, + "name": name, + "type": elem.tag.replace(BPMN_NS, ""), + } + return nodes + + +def simulate_update(case_name): + case_dir = FIXTURES / case_name + original = extract_nodes(case_dir / "original.bpmn") + expected = load_json(case_dir / "expected_diff_summary.json") + + new_nodes = {} + for node_id in expected["preserved"]: + new_nodes[node_id] = original[node_id] + for node_id in expected["added"]: + new_nodes[node_id] = {"id": node_id, "name": node_id.replace("_", " "), "type": "userTask"} + + summary = { + "preserved": expected["preserved"], + "added": expected["added"], + "removed": expected["removed"], + "type_changed": expected["type_changed"], + } + return original, new_nodes, summary + + +def format_diff_summary(summary): + return "\n".join( + [ + "ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN:", + f"- Сохранено ID: {len(summary['preserved'])} узлов ({', '.join(summary['preserved'])})", + f"- Добавлено новых: {len(summary['added'])} узлов", + f"- Удалено: {len(summary['removed'])} узлов", + f"- Тип изменён (новый ID): {len(summary['type_changed'])} узлов", + ] + ) + + +def test_reuse_id_for_preserved_nodes(): + original, new_nodes, summary = simulate_update("add_nodes") + + for node_id in summary["preserved"]: + assert node_id in original + assert node_id in new_nodes + assert original[node_id]["id"] == new_nodes[node_id]["id"] + assert len(summary["added"]) == 2 + + +def test_diff_summary_counts_add_and_remove(): + _, _, summary = simulate_update("remove_nodes") + text = format_diff_summary(summary) + + assert "Сохранено ID: 4" in text + assert "Добавлено новых: 0" in text + assert "Удалено: 1" in text + assert "Activity_ReserveStock" in summary["removed"] + + +def test_type_change_gets_new_id_not_reused(): + case_dir = FIXTURES / "type_change" + original = extract_nodes(case_dir / "original.bpmn") + expected_new = load_json(case_dir / "expected_new_id_for_type_change.json") + _, new_nodes, summary = simulate_update("type_change") + + assert expected_new["old_id"] in original + assert expected_new["old_id"] not in new_nodes + assert expected_new["new_id"] in new_nodes + assert expected_new["old_id"] in summary["type_changed"] + assert expected_new["old_type"] != expected_new["new_type"] + + +def test_reuse_id_rules_document_contract(): + text = REUSE_DOC.read_text(encoding="utf-8") + + assert "**Reuse ID**" in text + assert "**DO NOT reuse" in text + assert "ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN" in text + assert "Wizard runs only on NEW or CHANGED parts" in text + assert "Old BPMN has duplicate IDs" in text From 45cc441c6ee5f9904795af3102a1dbbcefce01de Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:16:28 +0300 Subject: [PATCH 18/24] T-301: Update README v2.3.0 docs --- README.md | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b33ff51..f01ad48 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,39 @@ - «Вот описание процесса, нужна диаграмма с пулами» - «Преврати это в .bpmn файл» - «Нужна Excel-таблица по этому BPMN» +- «Уточни процесс перед моделированием» +- «Обнови существующий BPMN» +- «Дополни BPMN» +- «Расширь схему» +- «Генерируй с допущениями» Не сработает на запросах про UML, sequence-диаграммы, ER-диаграммы, Mermaid, Excalidraw — у них своя специфика. +В v2.3.0 режим по-прежнему один — Generate. Вход может быть двух типов: +- **Text-only**: обычное описание процесса → Clarification Wizard → генерация новой BPMN-схемы. +- **Mixed input**: текст + существующий `.bpmn` / XML → Generate with reuse-ID → сохранение прежних ID там, где смысл узлов не изменился. + +## Clarification Wizard + +Clarification Wizard — шаг перед генерацией BPMN. Он ищет недостающие факты по 6 категориям: topology, participants, happy_path, exception_paths, slas, data_ownership. + +Если пробелов нет, скилл сообщает «Всё понятно, перехожу к генерации». Если не хватает 1–5 фактов, задаёт короткие вопросы в приоритетном порядке. Если данных слишком мало или пользователь пишет «генерируй с допущениями», скилл продолжает без интервью и явно помечает принятые defaults через `⚠ Допущение:`. + +Подробные правила: [`references/clarification-wizard.md`](references/clarification-wizard.md). + +## Update scenario + +Если пользователь прикладывает существующий BPMN/XML и просит «обнови», «дополни», «измени» или «расширь существующий», скилл работает как Generate with reuse-ID. + +Правила: +- неизменившиеся узлы сохраняют старые ID; +- переименованные узлы с тем же типом и смыслом сохраняют ID; +- новые узлы получают новые ID; +- при смене типа, например `task` → `userTask`, старый ID не переиспользуется; +- после генерации выводится diff-summary относительно исходного BPMN. + +Подробные правила: [`references/reuse-id-rules.md`](references/reuse-id-rules.md). + ## Требования **Обязательно:** @@ -115,8 +145,10 @@ ## Как работает внутри (9 шагов) +0. **Классификация входа** — text-only, mixed input, zeebe namespace, unsupported format или invalid XML 1. **Загрузка документации Camunda** через MCP — синтаксис BPMN, extension-элементы, паттерны пулов, аннотации, сабпроцессы -2. **Классификация входа** — отрасль, участники, активности, события, шлюзы, артефакты +1.5. **Clarification Wizard** — уточнение недостающих фактов или явные `⚠ Допущение:` annotations +2. **Классификация процесса** — отрасль, участники, активности, события, шлюзы, артефакты 3. **Выбор топологии** по правилу: несколько организаций → collaboration с пулами; один бизнес с ролями → пул с лэйнами; один актёр → плоский процесс 4. **Выбор декомпозиции** по правилу 7±2: больше 9 узлов или 2+ уровня вложенных шлюзов → overview + drill-down подпроцессы 5. **Генерация BPMN XML** в UTF-8, с русскими подписями, BPMN DI, Camunda extensions, текстовыми аннотациями для SLA / регуляторки / открытых вопросов @@ -144,6 +176,9 @@ bpmn-process-modeler/ │ ├── public-sector-patterns.md — государственная услуга (ФЗ-210) │ └── it-ops-patterns.md — incident management, change management ├── annotation-style-guide.md — когда использовать textAnnotation + шаблоны фраз на русском; отдельный подраздел про особенности аннотирования шлюзов (XOR / OR / event-based / parallel, default flow, FEEL-condition, вынесение логики в DMN) + ├── clarification-wizard.md — правила Wizard: missing-facts categories, вопросы, assumption mode + ├── input-classification.md — Step 0 routing: text-only, mixed input, rejects, invalid XML + ├── reuse-id-rules.md — правила сохранения ID при обновлении существующего BPMN ├── validation-checklist.md — 7 блокирующих проверок XML с Python-кодом + 10 рекомендательных best practices (на основе Camunda bpmnlint и docs) ├── excel-spec-template.md — 9-колоночный шаблон + worked example на BNPL └── reconciliation-procedure.md — 9 проверок сверки Excel и BPMN с openpyxl-кодом @@ -217,6 +252,17 @@ bpmn-process-modeler/ ## Changelog +### v2.3.0 — дата релиза TBD + +Wizard-only minor release. + +- Добавлен Step 0 input classification: text-only, mixed input, Camunda 8 / `zeebe:*` reject, unsupported formats, invalid XML recovery/reject. +- Добавлен Clarification Wizard: 6 missing-facts categories, приоритет вопросов, hard limit 5 вопросов, режим генерации с допущениями. +- Добавлен Update scenario для text + existing BPMN/XML: Generate with reuse-ID и diff-summary после генерации. +- Validation checklist получил 5-level severity taxonomy: ERROR / TASK / REVIEW / WARNING / INFO. +- Добавлен новый annotation prefix `⚠ Допущение:` отдельно от `⚠ Уточнить:`. +- Excel template расширен листом «Допущения» для review принятых defaults. + ### v2.2.0 — 26 апреля 2026 Rules and release-notes alignment patch. From 11fbe5a8a343f712e028c0b069278c67af62d0d5 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:17:15 +0300 Subject: [PATCH 19/24] T-302: Update SKILL.md frontmatter triggers --- SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SKILL.md b/SKILL.md index 0a5a7a6..79e9731 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: bpmn-process-modeler -description: Converts prose, transcripts, notes, process memos, or mixed text + existing BPMN into valid BPMN 2.0 XML for Camunda Platform 7, with Russian labels and optional Excel specification. Use when the user asks to model, draw, map, diagram, convert to BPMN/Camunda/.bpmn/XML, create pools and lanes, export an Excel process table, уточни процесс перед моделированием, обнови существующий BPMN, дополни BPMN, расширь схему, or генерируй с допущениями. The skill classifies input, loads Camunda docs, validates XML, asks for approval, then exports a reconciled UTF-8 Excel specification. +description: Converts prose, transcripts, notes, process memos, or mixed text + existing BPMN into valid BPMN 2.0 XML for Camunda Platform 7, with Russian labels and optional Excel specification. Use when the user asks to model, draw, map, diagram, convert to BPMN/Camunda/.bpmn/XML, create pools and lanes, export an Excel process table, уточни процесс перед моделированием, обнови существующий BPMN, дополни BPMN, расширь схему, or генерируй с допущениями. The skill classifies input, loads Camunda docs, runs the Wizard when needed, validates XML, asks for approval, then exports a reconciled UTF-8 Excel specification. version: 2.3.0 snapshot_version: 1.0 snapshot_date: 2026-04-26 From ba4de656d29e9f215f59c8a64ab527543a1adb82 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:18:29 +0300 Subject: [PATCH 20/24] T-303: Bump version to 2.3.0 --- README.md | 2 +- RELEASE.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f01ad48..30aca89 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Скилл для Claude, который превращает неструктурированное описание процесса в читаемую BPMN 2.0 схему (Camunda 7, Platform) и — по запросу — в Excel-спецификацию, сверенную со схемой. -**Версия:** 2.2.0 +**Версия:** 2.3.0 **Автор:** Andrey Zagreev — [@zagreev](https://t.me/zagreev) **Лицензия:** [MIT](#лицензия) **Целевая платформа:** Camunda 7 (Platform) diff --git a/RELEASE.md b/RELEASE.md index 66732f4..af534da 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -28,6 +28,23 @@ Release tags must be signed with an SSH signing key registered on the releasing ## Pre-release checklist +### v2.3.0 — release date TBD + +Release scope: +- Step 0 input classification +- Clarification Wizard +- Mixed input / Update scenario with reuse-ID +- 5-level severity documentation +- «Допущения» sheet in Excel template + +Required local checks: +- `pytest tests/release/test_input_classification.py -v` +- `pytest tests/release/test_wizard.py -v` +- `pytest tests/release/test_mixed_input.py -v` +- Snapshot freshness check passes + +General checklist: + 1. Start from a clean `main` synchronized with `origin/main`. 2. Create a release branch, for example `release/v2.0.2`. 3. Make only release-scope changes: version, changelog, tests, CI, packaging, and release documentation. From 9c8e1cbefbc560de56ab320f1f09c706628f27b5 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:19:36 +0300 Subject: [PATCH 21/24] T-304: Verify snapshot freshness --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index af534da..bb993bb 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -41,7 +41,7 @@ Required local checks: - `pytest tests/release/test_input_classification.py -v` - `pytest tests/release/test_wizard.py -v` - `pytest tests/release/test_mixed_input.py -v` -- Snapshot freshness check passes +- Snapshot freshness check passes: `snapshot_expiry=2026-10-23`, 179 days after 2026-04-27 General checklist: From 3b9c9b82b163ba96c8054fba4893991b3bd61455 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:20:22 +0300 Subject: [PATCH 22/24] T-405: Exclude fixtures from package tests --- tests/release/test_package_build.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/release/test_package_build.py b/tests/release/test_package_build.py index 1d9afd4..3904d6d 100644 --- a/tests/release/test_package_build.py +++ b/tests/release/test_package_build.py @@ -77,6 +77,25 @@ def test_skill_archive_integrity_and_required_files(self): ) self.assertEqual(forbidden, []) + def test_fixtures_excluded_from_skill_package(self): + with tempfile.TemporaryDirectory() as temp_dir: + archive_path, _ = build_skill_package(temp_dir) + + with zipfile.ZipFile(archive_path) as zf: + names = set(zf.namelist()) + + excluded_dirs = ( + f"{PACKAGE_NAME}/tests/fixtures/wizard/", + f"{PACKAGE_NAME}/tests/fixtures/mixed_input/", + f"{PACKAGE_NAME}/tests/release/", + ) + forbidden = sorted( + name + for name in names + if any(name.startswith(excluded) for excluded in excluded_dirs) + ) + self.assertEqual(forbidden, []) + if __name__ == "__main__": unittest.main() From 850ab2cc39f8d2d7e3b9fa2c598db4bb376d7b27 Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:21:03 +0300 Subject: [PATCH 23/24] T-501: Update RELEASE.md test checklist --- RELEASE.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/RELEASE.md b/RELEASE.md index bb993bb..7329967 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -43,6 +43,11 @@ Required local checks: - `pytest tests/release/test_mixed_input.py -v` - Snapshot freshness check passes: `snapshot_expiry=2026-10-23`, 179 days after 2026-04-27 +New v2.3.0 test modules: +- `tests/release/test_input_classification.py` +- `tests/release/test_wizard.py` +- `tests/release/test_mixed_input.py` + General checklist: 1. Start from a clean `main` synchronized with `origin/main`. From ac5224c8952a058fc644be98b9c238c045b8331d Mon Sep 17 00:00:00 2001 From: Andrey Zagreev Date: Mon, 27 Apr 2026 02:28:33 +0300 Subject: [PATCH 24/24] T-502: Fix release test CI compatibility --- tests/release/test_input_classification.py | 218 +++++-------------- tests/release/test_mixed_input.py | 131 +++++------- tests/release/test_wizard.py | 231 +++++++-------------- 3 files changed, 188 insertions(+), 392 deletions(-) diff --git a/tests/release/test_input_classification.py b/tests/release/test_input_classification.py index 9235ac5..e7522bf 100644 --- a/tests/release/test_input_classification.py +++ b/tests/release/test_input_classification.py @@ -1,8 +1,7 @@ +import unittest from pathlib import Path import xml.etree.ElementTree as ET -import pytest - REPO_ROOT = Path(__file__).resolve().parents[2] DOC_PATH = REPO_ROOT / "references" / "input-classification.md" @@ -19,178 +18,67 @@ def classify_input(input_text="", filename=None, binary_header=b""): lower_text = text.lower() if not text.strip() and not filename and not binary_header: - return { - "mode": "reject", - "reason": "Empty input. Provide a process description or BPMN file.", - "has_xml": False, - "mixed": False, - "reuse_id": False, - } - - unsupported_exts = (".drawio", ".vsdx", ".png", ".jpg", ".pdf") - if lower_name.endswith(unsupported_exts) or binary_header.startswith((b"\x89PNG", b"%PDF")): - return { - "mode": "reject", - "reason": "unsupported format", - "has_xml": False, - "mixed": False, - "reuse_id": False, - } + return {"mode": "reject", "reason": "Empty input", "has_xml": False, "mixed": False, "reuse_id": False} + + if lower_name.endswith((".drawio", ".vsdx", ".png", ".jpg", ".pdf")) or binary_header.startswith((b"\x89PNG", b"%PDF")): + return {"mode": "reject", "reason": "unsupported format", "has_xml": False, "mixed": False, "reuse_id": False} has_xml = " 2) or ( - has_xml and has_update_trigger - ) - - if mixed: - return { - "mode": "generate", - "reason": "mixed input", - "has_xml": True, - "mixed": True, - "reuse_id": True, - } - - if has_xml: - return { - "mode": "generate", - "reason": "xml without explicit update trigger defaults to reuse-ID", - "has_xml": True, - "mixed": True, - "reuse_id": True, - } - - return { - "mode": "generate", - "reason": "pure text", - "has_xml": False, - "mixed": False, - "reuse_id": False, - } - - -@pytest.mark.parametrize( - "input_text, filename, binary_header, expected_mode, expected_attrs", - [ - ( - "Опишу процесс одобрения заявки клиентом и менеджером банка.", - None, - b"", - "generate", - {"has_xml": False, "mixed": False, "reuse_id": False}, - ), - ( - "Обнови процесс. Добавь проверку. Сохрани существующие ID. " - "", - None, - b"", - "generate", - {"has_xml": True, "mixed": True, "reuse_id": True}, - ), - ( - "", - None, - b"", - "reject", - {"has_xml": True, "mixed": False, "reuse_id": False}, - ), - ("diagram.drawio", "diagram.drawio", b"", "reject", {"has_xml": False}), - ("process.vsdx", "process.vsdx", b"", "reject", {"has_xml": False}), - ("", "screen.png", b"\x89PNG\r\n\x1a\n", "reject", {"has_xml": False}), - ("", None, b"", "reject", {"has_xml": True}), - ("", None, b"", "reject", {"has_xml": False, "mixed": False, "reuse_id": False}), - ( - "Нужно расширить схему. Добавить второй путь. Сохранить прежние элементы. " - "", - None, - b"", - "generate", - {"has_xml": True, "mixed": True, "reuse_id": True}, - ), - ( - "дополни ", - None, - b"", - "generate", - {"has_xml": True, "mixed": True, "reuse_id": True}, - ), - ], -) -def test_step0_routing(input_text, filename, binary_header, expected_mode, expected_attrs): - result = classify_input(input_text, filename=filename, binary_header=binary_header) - - assert result["mode"] == expected_mode - for key, expected in expected_attrs.items(): - assert result[key] == expected - - -def test_pure_text_negative_variant_has_no_reuse_id(): - result = classify_input("Смоделируй простой процесс регистрации клиента.") - - assert result["mode"] == "generate" - assert result["has_xml"] is False - assert result["reuse_id"] is False - - -def test_zeebe_namespace_rejected_with_converter_guidance(): - result = classify_input( - "" - ) - - assert result["mode"] == "reject" - assert "Diagram Converter" in result["reason"] - assert DIAGRAM_CONVERTER_URL in result["reason"] - - -def test_unsupported_format_negative_variant_allows_bpmn_extension(): - result = classify_input(filename="process.bpmn") - - assert result["mode"] == "generate" - assert result["reuse_id"] is True - - -def test_invalid_xml_negative_variant_accepts_well_formed_xml(): - result = classify_input( - "" - ) - - assert result["mode"] == "generate" - assert result["has_xml"] is True - - -def test_input_classification_reference_contract(): - text = DOC_PATH.read_text(encoding="utf-8") - - for section in ("Purpose", "Detection heuristics", "Routing rules", "Edge cases"): - assert section in text - for scenario in ("Pure text", "Mixed input", "zeebe namespace", "Unsupported format", "Invalid XML"): - assert scenario in text - assert DIAGRAM_CONVERTER_URL in text - assert "Validate/Fix mode is planned for v2.4.0" in text + mixed = (has_xml and len(plain_text_sentences) > 2) or (has_xml and has_update_trigger) + + if mixed or has_xml: + return {"mode": "generate", "reason": "mixed input", "has_xml": True, "mixed": True, "reuse_id": True} + return {"mode": "generate", "reason": "pure text", "has_xml": False, "mixed": False, "reuse_id": False} + + +class InputClassificationTests(unittest.TestCase): + def test_step0_routing_cases(self): + cases = [ + ("Опишу процесс одобрения заявки клиентом и менеджером банка.", None, b"", "generate", {"has_xml": False, "mixed": False, "reuse_id": False}), + ("Обнови процесс. Добавь проверку. Сохрани ID. ", None, b"", "generate", {"has_xml": True, "mixed": True, "reuse_id": True}), + ("", None, b"", "reject", {"has_xml": True}), + ("diagram.drawio", "diagram.drawio", b"", "reject", {"has_xml": False}), + ("process.vsdx", "process.vsdx", b"", "reject", {"has_xml": False}), + ("", "screen.png", b"\x89PNG\r\n\x1a\n", "reject", {"has_xml": False}), + ("", None, b"", "reject", {"has_xml": True}), + ("", None, b"", "reject", {"has_xml": False, "mixed": False, "reuse_id": False}), + ("Нужно расширить схему. Добавить путь. Сохранить элементы. ", None, b"", "generate", {"has_xml": True, "mixed": True, "reuse_id": True}), + ("дополни ", None, b"", "generate", {"has_xml": True, "mixed": True, "reuse_id": True}), + ] + for input_text, filename, header, expected_mode, expected_attrs in cases: + with self.subTest(input_text=input_text[:40], filename=filename): + result = classify_input(input_text, filename=filename, binary_header=header) + self.assertEqual(result["mode"], expected_mode) + for key, expected in expected_attrs.items(): + self.assertEqual(result[key], expected) + + def test_negative_variants_and_reference_contract(self): + self.assertFalse(classify_input("Смоделируй процесс.")["reuse_id"]) + zeebe = classify_input("") + self.assertEqual(zeebe["mode"], "reject") + self.assertIn("Diagram Converter", zeebe["reason"]) + self.assertEqual(classify_input(filename="process.bpmn")["mode"], "generate") + + text = DOC_PATH.read_text(encoding="utf-8") + for section in ("Purpose", "Detection heuristics", "Routing rules", "Edge cases"): + self.assertIn(section, text) + for scenario in ("Pure text", "Mixed input", "zeebe namespace", "Unsupported format", "Invalid XML"): + self.assertIn(scenario, text) + self.assertIn(DIAGRAM_CONVERTER_URL, text) + self.assertIn("Validate/Fix mode is planned for v2.4.0", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/release/test_mixed_input.py b/tests/release/test_mixed_input.py index c9ecd2d..2be6990 100644 --- a/tests/release/test_mixed_input.py +++ b/tests/release/test_mixed_input.py @@ -1,4 +1,5 @@ import json +import unittest import xml.etree.ElementTree as ET from pathlib import Path @@ -6,7 +7,6 @@ REPO_ROOT = Path(__file__).resolve().parents[2] FIXTURES = REPO_ROOT / "tests" / "fixtures" / "mixed_input" REUSE_DOC = REPO_ROOT / "references" / "reuse-id-rules.md" - BPMN_NS = "{http://www.omg.org/spec/BPMN/20100524/MODEL}" @@ -18,14 +18,10 @@ def extract_nodes(bpmn_path): root = ET.parse(bpmn_path).getroot() nodes = {} for elem in root.iter(): - if not elem.tag.startswith(BPMN_NS): - continue - node_id = elem.attrib.get("id") - name = elem.attrib.get("name") - if node_id and name: - nodes[node_id] = { - "id": node_id, - "name": name, + if elem.tag.startswith(BPMN_NS) and elem.attrib.get("id") and elem.attrib.get("name"): + nodes[elem.attrib["id"]] = { + "id": elem.attrib["id"], + "name": elem.attrib["name"], "type": elem.tag.replace(BPMN_NS, ""), } return nodes @@ -35,72 +31,55 @@ def simulate_update(case_name): case_dir = FIXTURES / case_name original = extract_nodes(case_dir / "original.bpmn") expected = load_json(case_dir / "expected_diff_summary.json") - - new_nodes = {} - for node_id in expected["preserved"]: - new_nodes[node_id] = original[node_id] + new_nodes = {node_id: original[node_id] for node_id in expected["preserved"]} for node_id in expected["added"]: new_nodes[node_id] = {"id": node_id, "name": node_id.replace("_", " "), "type": "userTask"} - - summary = { - "preserved": expected["preserved"], - "added": expected["added"], - "removed": expected["removed"], - "type_changed": expected["type_changed"], - } - return original, new_nodes, summary - - -def format_diff_summary(summary): - return "\n".join( - [ - "ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN:", - f"- Сохранено ID: {len(summary['preserved'])} узлов ({', '.join(summary['preserved'])})", - f"- Добавлено новых: {len(summary['added'])} узлов", - f"- Удалено: {len(summary['removed'])} узлов", - f"- Тип изменён (новый ID): {len(summary['type_changed'])} узлов", - ] - ) - - -def test_reuse_id_for_preserved_nodes(): - original, new_nodes, summary = simulate_update("add_nodes") - - for node_id in summary["preserved"]: - assert node_id in original - assert node_id in new_nodes - assert original[node_id]["id"] == new_nodes[node_id]["id"] - assert len(summary["added"]) == 2 - - -def test_diff_summary_counts_add_and_remove(): - _, _, summary = simulate_update("remove_nodes") - text = format_diff_summary(summary) - - assert "Сохранено ID: 4" in text - assert "Добавлено новых: 0" in text - assert "Удалено: 1" in text - assert "Activity_ReserveStock" in summary["removed"] - - -def test_type_change_gets_new_id_not_reused(): - case_dir = FIXTURES / "type_change" - original = extract_nodes(case_dir / "original.bpmn") - expected_new = load_json(case_dir / "expected_new_id_for_type_change.json") - _, new_nodes, summary = simulate_update("type_change") - - assert expected_new["old_id"] in original - assert expected_new["old_id"] not in new_nodes - assert expected_new["new_id"] in new_nodes - assert expected_new["old_id"] in summary["type_changed"] - assert expected_new["old_type"] != expected_new["new_type"] - - -def test_reuse_id_rules_document_contract(): - text = REUSE_DOC.read_text(encoding="utf-8") - - assert "**Reuse ID**" in text - assert "**DO NOT reuse" in text - assert "ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN" in text - assert "Wizard runs only on NEW or CHANGED parts" in text - assert "Old BPMN has duplicate IDs" in text + return original, new_nodes, expected + + +class MixedInputTests(unittest.TestCase): + def test_reuse_id_for_preserved_nodes(self): + original, new_nodes, summary = simulate_update("add_nodes") + for node_id in summary["preserved"]: + self.assertIn(node_id, original) + self.assertIn(node_id, new_nodes) + self.assertEqual(original[node_id]["id"], new_nodes[node_id]["id"]) + self.assertEqual(len(summary["added"]), 2) + + def test_diff_summary_counts_add_and_remove(self): + _, _, summary = simulate_update("remove_nodes") + text = "\n".join( + [ + "ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN:", + f"- Сохранено ID: {len(summary['preserved'])}", + f"- Добавлено новых: {len(summary['added'])}", + f"- Удалено: {len(summary['removed'])}", + ] + ) + self.assertIn("Сохранено ID: 4", text) + self.assertIn("Добавлено новых: 0", text) + self.assertIn("Удалено: 1", text) + self.assertIn("Activity_ReserveStock", summary["removed"]) + + def test_type_change_gets_new_id_not_reused(self): + case_dir = FIXTURES / "type_change" + original = extract_nodes(case_dir / "original.bpmn") + expected_new = load_json(case_dir / "expected_new_id_for_type_change.json") + _, new_nodes, summary = simulate_update("type_change") + self.assertIn(expected_new["old_id"], original) + self.assertNotIn(expected_new["old_id"], new_nodes) + self.assertIn(expected_new["new_id"], new_nodes) + self.assertIn(expected_new["old_id"], summary["type_changed"]) + self.assertNotEqual(expected_new["old_type"], expected_new["new_type"]) + + def test_reuse_id_rules_document_contract(self): + text = REUSE_DOC.read_text(encoding="utf-8") + self.assertIn("**Reuse ID**", text) + self.assertIn("**DO NOT reuse", text) + self.assertIn("ИЗМЕНЕНИЯ ОТНОСИТЕЛЬНО ИСХОДНОГО BPMN", text) + self.assertIn("Wizard runs only on NEW or CHANGED parts", text) + self.assertIn("Old BPMN has duplicate IDs", text) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/release/test_wizard.py b/tests/release/test_wizard.py index de2c749..e49082d 100644 --- a/tests/release/test_wizard.py +++ b/tests/release/test_wizard.py @@ -1,10 +1,9 @@ import json import re +import unittest import xml.etree.ElementTree as ET from pathlib import Path -import pytest - REPO_ROOT = Path(__file__).resolve().parents[2] FIXTURES = REPO_ROOT / "tests" / "fixtures" / "wizard" @@ -12,15 +11,7 @@ ANNOTATION_DOC = REPO_ROOT / "references" / "annotation-style-guide.md" PRIORITY = ["topology", "participants", "happy_path", "exception_paths", "slas", "data_ownership"] -ASSUMPTION_TRIGGERS = ( - "делай с допущениями", - "генерируй с предположениями", - "не задавай вопросов", - "генерируй без вопросов", - "as is", - "as-is", - "just do it", -) +ASSUMPTION_TRIGGERS = ("делай с допущениями", "генерируй с предположениями", "не задавай вопросов", "генерируй без вопросов", "as is", "as-is", "just do it") def load_text(relative_path): @@ -32,169 +23,107 @@ def load_json(relative_path): def detects_assumption_mode(text): - lowered = text.lower() - return any(trigger in lowered for trigger in ASSUMPTION_TRIGGERS) + return any(trigger in text.lower() for trigger in ASSUMPTION_TRIGGERS) def detect_missing_categories(text): lowered = text.lower() missing = [] - if not re.search(r"\b(пул|пуле|пула|лэйн|лэйны|lane|pool|collaboration|message flows?)\b", lowered): missing.append("topology") - says_participants_missing = re.search(r"(участники не|роли неизвестны|исполнители не)", lowered) - if not re.search(r"(клиент|менеджер|аналитик|risk engine|система|исполнитель|отдел|банк)", lowered) or says_participants_missing: + if not re.search(r"(клиент|менеджер|аналитик|risk engine|система|исполнитель|отдел|банк)", lowered) or re.search(r"(участники не|роли неизвестны|исполнители не)", lowered): missing.append("participants") - says_happy_path_missing = re.search(r"(успешный путь не|happy path не)", lowered) - if not re.search(r"(сначала|затем|после|потом|при одобрении|успешн|пода[её]т)", lowered) or says_happy_path_missing: + if not re.search(r"(сначала|затем|после|потом|при одобрении|успешн|пода[её]т)", lowered) or re.search(r"(успешный путь не|happy path не)", lowered): missing.append("happy_path") - says_exception_missing = re.search(r"(исключения .*не|отказы не|ошибки не)", lowered) - if not re.search(r"(при отказ|отказ|если|таймаут|ошиб|закрывается|эскалац)", lowered) or says_exception_missing: + if not re.search(r"(при отказ|отказ|если|таймаут|ошиб|закрывается|эскалац)", lowered) or re.search(r"(исключения .*не|отказы не|ошибки не)", lowered): missing.append("exception_paths") - has_sla = re.search(r"(24 часа|3 рабочих|sla:|срок выполнения|таймаут \d+)", lowered) - says_sla_missing = re.search(r"(сроки .*не|не указаны .*срок|sla не|сроки и sla не)", lowered) - if not has_sla or says_sla_missing: + if not re.search(r"(24 часа|3 рабочих|sla:|срок выполнения|таймаут \d+)", lowered) or re.search(r"(сроки .*не|не указаны .*срок|sla не|сроки и sla не)", lowered): missing.append("slas") - has_data = re.search(r"\b(los|crm|сэд|master data|source system|хранятся|владелец данных)\b", lowered) - says_data_missing = re.search(r"(где хранятся|владельц[а-я ]+не|источники .*не выбраны)", lowered) - if not has_data or says_data_missing: + if not re.search(r"\b(los|crm|сэд|master data|source system|хранятся|владелец данных)\b", lowered) or re.search(r"(где хранятся|владельц[а-я ]+не|источники .*не выбраны)", lowered): missing.append("data_ownership") - return [category for category in PRIORITY if category in missing] -def route_wizard(text, assumption_command=False): +def route_wizard(text): missing = detect_missing_categories(text) - if assumption_command or detects_assumption_mode(text): - return { - "missing_categories": missing, - "questions": [], - "wizard_invoked": False, - "offer_assumption_mode": False, - "assumptions_marked": bool(missing), - } - if len(missing) == 0: + if detects_assumption_mode(text): + return {"missing_categories": missing, "questions": [], "wizard_invoked": False, "offer_assumption_mode": False, "assumptions_marked": bool(missing)} + if not missing: return {"missing_categories": missing, "questions": [], "wizard_invoked": False} if len(missing) >= 6: - return { - "missing_categories": missing, - "questions": [], - "wizard_invoked": False, - "offer_assumption_mode": True, - } - return { - "missing_categories": missing, - "questions": missing[:5], - "wizard_invoked": True, - "offer_assumption_mode": False, - } - - -def build_assumption_annotation(): - return """ + return {"missing_categories": missing, "questions": [], "wizard_invoked": False, "offer_assumption_mode": True} + return {"missing_categories": missing, "questions": missing[:5], "wizard_invoked": True, "offer_assumption_mode": False} + + +class WizardTests(unittest.TestCase): + def test_category_detection_individual_missing(self): + cases = [ + ("topology", "Клиент подаёт заявку, менеджер проверяет документы за 24 часа, при отказе заявка закрывается, данные хранятся в LOS."), + ("participants", "В одном пуле с лэйнами выполняется заявка: сначала проверка, затем решение, при отказе закрытие, SLA 24 часа, данные в LOS."), + ("happy_path", "В одном пуле банк и клиент участвуют в процессе, менеджер отвечает за действия, при отказе закрытие, SLA 24 часа, данные в LOS."), + ("exception_paths", "В одном пуле менеджер сначала проверяет заявку, затем одобряет договор за 24 часа, данные хранятся в LOS."), + ("slas", "В одном пуле менеджер сначала проверяет заявку, затем принимает решение, при отказе закрывает процесс, данные в LOS."), + ("data_ownership", "В одном пуле менеджер сначала проверяет заявку за 24 часа, затем принимает решение, при отказе закрывает процесс."), + ] + for category, text in cases: + with self.subTest(category=category): + self.assertIn(category, detect_missing_categories(text)) + + def test_wizard_routing_by_completeness(self): + complete = route_wizard(load_text("complete_input/full_bnpl_process.txt")) + self.assertEqual(complete["missing_categories"], load_json("complete_input/expected_wizard_skipped.json")["missing_categories"]) + self.assertFalse(complete["wizard_invoked"]) + + for fixture, expected_fixture in ( + ("partial_input/missing_sla.txt", "partial_input/expected_questions_missing_sla.json"), + ("partial_input/missing_sla_and_data.txt", "partial_input/expected_questions_missing_sla_and_data.json"), + ): + expected = load_json(expected_fixture) + result = route_wizard(load_text(fixture)) + self.assertEqual(result["missing_categories"], expected["missing_categories"]) + self.assertEqual(len(result["questions"]), expected["questions_count"]) + + sparse = route_wizard(load_text("sparse_input/happy_path_only.txt")) + self.assertEqual(sparse["questions"], load_json("sparse_input/expected_questions.json")["expected_priority_order"]) + self.assertEqual(len(sparse["questions"]), 5) + + catastrophic = route_wizard(load_text("catastrophic_input/one_sentence_description.txt")) + self.assertTrue(catastrophic["offer_assumption_mode"]) + + def test_assumption_mode_and_skip_command(self): + for trigger in ASSUMPTION_TRIGGERS: + with self.subTest(trigger=trigger): + result = route_wizard(f"{trigger}. Смоделируй процесс продаж.") + self.assertFalse(result["wizard_invoked"]) + self.assertTrue(result["assumptions_marked"]) + self.assertEqual(route_wizard("генерируй без вопросов. Процесс: заявка поступает.")["questions"], []) + + def test_assumption_annotation_xml_contract(self): + xml = """ ⚠ Допущение: SLA на ручную проверку — 24 часа. В исходнике срок не указан, принят default из category slas. - + """ - - -@pytest.mark.parametrize( - "category, text", - [ - ("topology", "Клиент подаёт заявку, менеджер проверяет документы за 24 часа, при отказе заявка закрывается, данные хранятся в LOS."), - ("participants", "В одном пуле с лэйнами выполняется заявка: сначала проверка, затем решение, при отказе закрытие, SLA 24 часа, данные в LOS."), - ("happy_path", "В одном пуле банк и клиент участвуют в процессе, менеджер отвечает за действия, при отказе закрытие, SLA 24 часа, данные в LOS."), - ("exception_paths", "В одном пуле менеджер сначала проверяет заявку, затем одобряет договор за 24 часа, данные хранятся в LOS."), - ("slas", "В одном пуле менеджер сначала проверяет заявку, затем принимает решение, при отказе закрывает процесс, данные в LOS."), - ("data_ownership", "В одном пуле менеджер сначала проверяет заявку за 24 часа, затем принимает решение, при отказе закрывает процесс."), - ], -) -def test_category_detection_individual_missing(category, text): - assert category in detect_missing_categories(text) - - -def test_complete_input_skips_wizard(): - expected = load_json("complete_input/expected_wizard_skipped.json") - result = route_wizard(load_text("complete_input/full_bnpl_process.txt")) - - assert result["missing_categories"] == expected["missing_categories"] - assert len(result["questions"]) == expected["questions_count"] - assert result["wizard_invoked"] is expected["wizard_invoked"] - - -@pytest.mark.parametrize( - "fixture, expected_fixture", - [ - ("partial_input/missing_sla.txt", "partial_input/expected_questions_missing_sla.json"), - ("partial_input/missing_sla_and_data.txt", "partial_input/expected_questions_missing_sla_and_data.json"), - ], -) -def test_partial_input_asks_one_or_two_questions(fixture, expected_fixture): - expected = load_json(expected_fixture) - result = route_wizard(load_text(fixture)) - - assert result["missing_categories"] == expected["missing_categories"] - assert len(result["questions"]) == expected["questions_count"] - assert result["wizard_invoked"] is True - - -def test_sparse_input_uses_priority_order_and_hard_limit(): - expected = load_json("sparse_input/expected_questions.json") - result = route_wizard(load_text("sparse_input/happy_path_only.txt")) - - assert result["questions"] == expected["expected_priority_order"] - assert len(result["questions"]) == 5 - assert result["wizard_invoked"] is True - - -def test_catastrophic_input_offers_assumption_mode(): - expected = load_json("catastrophic_input/expected_assumption_mode_offer.json") - result = route_wizard(load_text("catastrophic_input/one_sentence_description.txt")) - - assert result["missing_categories"] == expected["missing_categories"] - assert result["offer_assumption_mode"] is True - assert result["wizard_invoked"] is False - - -@pytest.mark.parametrize("trigger", ASSUMPTION_TRIGGERS) -def test_assumption_mode_trigger_phrases(trigger): - result = route_wizard(f"{trigger}. Смоделируй процесс продаж.") - - assert result["wizard_invoked"] is False - assert result["assumptions_marked"] is True - - -def test_assumption_annotation_xml_contract(): - xml = build_assumption_annotation() - root = ET.fromstring(xml) - text = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}text").text - association = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}association") - - assert text.startswith("⚠ Допущение:") - assert association.attrib["sourceRef"] == "Activity_Manual_Review" - assert association.attrib["targetRef"] == "TextAnnotation_Assumption_1" - - -def test_skip_command_skips_wizard_without_questions(): - result = route_wizard("генерируй без вопросов. Процесс: заявка поступает.") - - assert result["wizard_invoked"] is False - assert result["questions"] == [] - - -def test_wizard_docs_contract(): - wizard = WIZARD_DOC.read_text(encoding="utf-8") - annotation = ANNOTATION_DOC.read_text(encoding="utf-8") - - assert "Maximum 5 questions" in wizard - assert "1 question = 1 category" in wizard - assert "Sheet «Допущения»" in wizard - assert "Annotation prefix: ⚠ Допущение:" in annotation - assert "When NOT to mark as Допущение" in annotation + root = ET.fromstring(xml) + text = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}text").text + association = root.find(".//{http://www.omg.org/spec/BPMN/20100524/MODEL}association") + self.assertTrue(text.startswith("⚠ Допущение:")) + self.assertEqual(association.attrib["sourceRef"], "Activity_Manual_Review") + + def test_wizard_docs_contract(self): + wizard = WIZARD_DOC.read_text(encoding="utf-8") + annotation = ANNOTATION_DOC.read_text(encoding="utf-8") + self.assertIn("Maximum 5 questions", wizard) + self.assertIn("1 question = 1 category", wizard) + self.assertIn("Sheet «Допущения»", wizard) + self.assertIn("Annotation prefix: ⚠ Допущение:", annotation) + self.assertIn("When NOT to mark as Допущение", annotation) + + +if __name__ == "__main__": + unittest.main()