diff --git a/_process/04. Analitycs artefacts - documentation.md b/_process/04. Analitycs artefacts - documentation.md new file mode 100644 index 0000000..714c098 --- /dev/null +++ b/_process/04. Analitycs artefacts - documentation.md @@ -0,0 +1,289 @@ +## 1. Формат ведения технической документации агентом + +## 1.1. Общие принципы + +Техническая документация, формируемая агентом, должна строиться как система атомарных, не пересекающихся по смыслу документов, связанных между собой явными ссылками. + +Ключевые принципы: +- один документ описывает одну сущность или один устойчивый технический аспект; +- документ не должен дублировать соседние документы; +- общая система знаний должна собираться через ссылки, а не через копипасту; +- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG. + +## 1.2. Требования к заголовкам + +- Заголовок должен отражать только суть раздела. +- Заголовок не должен содержать метаданные (`id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`). +- Метаданные указываются отдельными строками в теле раздела или в YAML frontmatter. + +Пример: +- правильно: `## 6.2 Метод UFS получения списка заказов` +- неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)` + +## 1.3. Базовые типы документных единиц + +Базовые типы: +- `ui_page` +- `api_method` +- `logic_block` + +Дополнительно могут использоваться: +- `architecture_overview` +- `integration_doc` +- `domain_entity` +- `glossary_item` +- `index_page` + +## 1.4. Принцип декомпозиции страниц / файлов + +### Один устойчивый объект - один документ +Если объект можно переиспользовать или на него могут ссылаться другие документы, его нужно выносить в отдельный файл. + +### Документы не должны пересекаться по смыслу +Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него. + +### Use case и детали живут раздельно +Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания. + +## 1.5. Иерархическая организация документации + +Документация должна быть организована как иерархическое дерево каталогов и файлов. + +Пример: + +```text +docs/ + ui/ + api/ + logic/ + domains/ + integrations/ + architecture/ + glossary/ + errors/ +``` + +## 1.6. Учет связей между документами + +Связи должны быть явными. + +Примеры: +- UI-страница ссылается на вызываемые API; +- API-документ ссылается на используемые блоки логики; +- логический блок ссылается на интеграции; +- документ по коду ссылается на системную аналитику, инициировавшую изменения. + +## 1.7. Формат markdown-документов + +Каждый документ состоит из: +1. YAML frontmatter; +2. Markdown body. + +## 1.8. YAML frontmatter + +### Обязательные поля +- `id` +- `title` +- `doc_type` +- `status` +- `domain` +- `sub_domain` +- `related_docs` + +### Рекомендуемые поля +- `owner` +- `entities` +- `tags` +- `feature` +- `system_analytics_refs` +- `source_of_truth` +- `related_code` + +### Допустимые значения `doc_type` +- `ui_page` +- `api_method` +- `logic_block` +- `architecture_overview` +- `integration_doc` +- `domain_entity` +- `glossary_item` +- `index_page` + +### Допустимые значения `status` +- `draft` +- `in_review` +- `approved` +- `outdated` +- `generated` +- `active` + +## 1.9. Синхронизация с системной аналитикой + +Техническая документация строится на основе системной аналитики (features). + +Обязательно учитывать: +- концептуальный уровень аналитики; +- детализацию технической документации; +- согласованность терминов, ролей и интеграционных цепочек. + +Если атрибуты или детали отсутствуют в аналитике: +- определить их из текста аналитики; +- дополнить данными из репозитория (код, контракты, существующие документы); +- зафиксировать итог в документации как явные метаданные и требования. + +## 1.10. Формат body-разделов для блока изменений + +Для секции изменений (по аналогии с разделом `6` в аналитике) использовать единый формат. + +Под корнем секции изменений указывать общие атрибуты: +- `domain` +- `sub_domain` + +Для каждого подраздела `X.Y` указывать метаданные строками сразу после заголовка: +- `id` +- `doc_type` +- `application` +- `platform` + +## 1.11. Различие аналитики и документации + +- Аналитика - концептуальный уровень, упрощенный use case. +- Документация - детальный инженерный уровень. + +Для документации: +- технический use case должен быть детализированным; +- функциональные требования расширяют use case и описывают детали интеграций, логики и поведения; +- функциональные требования не должны копировать шаги сценария без добавления новой информации. + +Источник правил: +- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md` +- `src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md` + +## 1.12. Требования к `ui_page` + +Обязательная структура: +- `### Технический use case` +- `### Требования к UI` +- `### Функциональные требования` +- `### Нефункциональные требования` + +### Требования к UI +Внутри обязательно отдельно описывать каждую форму UI: +- табличное представление; +- пустой список (empty state); +- ошибка (error state). + +Обязательные правила: +- если есть интеграция, обязательно описать показ ошибки; +- если показывается список, обязательно описать показ отсутствия данных. + +### UI-элементы +UI-поля и элементы в документации описываются строго в таблицах. + +Обязательные колонки (заполнять там, где применимо): +- `Код элемента` +- `Название и описание` +- `Данные` +- `Поведение` +- `Валидация` + +## 1.13. Пользовательская аналитика для `ui_page` + +События пользовательской аналитики оформляются таблицей: +- `Название события` +- `Описание` +- `Точка вызова` +- `Payload` + +## 1.14. Требования к `api_method` + +Обязательная структура: +- `### Технический use case` +- `### Функциональные требования` +- `### Нефункциональные требования` +- `### Контракт` + +### Технический use case +Оформляется детально по правилам `tech-use-case.md`. + +Обязательные части: +- название +- предусловия +- триггер +- основной сценарий +- альтернативный сценарий +- обработка ошибок +- постусловие + +### Функциональные требования +Оформляются по правилам `fr.md`: +- формат `FR.<номер>. <Название>`; +- FR расширяют use case; +- FR не дублируют шаги сценария без дополнительной ценности; +- для интеграционных шагов FR обязательны. + +## 1.15. Нефункциональные требования для `api_method` + +Разделять на подразделы: +- `#### Аудит` (если применимо) +- `#### Мониторинг` + +### Мониторинг +Оформлять таблицей: +- `Метрика` +- `Описание` +- `Условие срабатывания` + +Правила: +- в условиях указывать, при каких состояниях фиксируется событие; +- не использовать формулировку вида «точка измерения = метод»; +- базово закладывать метрики: + - `_SUCCESS` + - `_FAIL` + - `_BUSINESS_ERROR` + +## 1.16. Распределение ответственности по слоям + +- Проверка ролевой модели пользователя обычно выполняется в `ufs`. +- Для `pprb` аудит может не фиксироваться, если это согласовано правилами домена. +- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в use case `pprb`. + +## 1.17. Контракты API + +Контракт может быть: +- в markdown-таблицах; +- в OpenAPI; +- в отдельном контрактном файле. + +Для markdown-контракта минимум: +- endpoint/method; +- request fields; +- required/optional; +- constraints; +- response; +- errors; +- auth; +- retry; +- timeout; +- idempotency. + +## 1.18. Integrations-блок + +Если у документа есть интеграции, выделять отдельный `## Integrations`. + +Рекомендуемые атрибуты интеграции: +- `target` +- `target_type` +- `direction` +- `interaction` +- `via` +- `purpose` +- `details` + +## 1.19. Общие требования к markdown body + +- В документе должен быть один `H1`, совпадающий с `title`. +- Основные разделы - `H2`, подразделы - `H3`. +- Не допускать хаотичной вложенности заголовков. +- Вместо дублирования использовать ссылки на связанные документы. +- Сценарии, правила, ограничения и кодовые привязки держать раздельно. diff --git a/_process/04. Analitycs artefacts - features.md b/_process/04. Analitycs artefacts - features.md new file mode 100644 index 0000000..2e41b24 --- /dev/null +++ b/_process/04. Analitycs artefacts - features.md @@ -0,0 +1,185 @@ +# Системная аналитика + +## Общее описание + +Документ описывает изменения в автоматизированной системе. Пишется системными аналитиками для разработчиков и тестировщиков и проходит согласование с экспертами по архитектуре, безопасности и сопровождению. + +Документ может описывать как новый процесс, так и инкремент доработки существующей функциональности. + +## Требования к заголовкам + +- Заголовок должен отражать суть раздела. +- Заголовок не должен содержать лишнюю информацию, которая относится к метаданным (id, doc_type, platform, application и т.д.). +- Метаданные указываются отдельными строками в теле раздела. + +## Состав документа + +Каждый раздел верхнего уровня оформляется заголовком уровня `#`. + +### 1. Цели + +- Коротко описать, какую проблему и для кого решаем. +- 1-2 предложения. +- Не дублировать критерии приемки. + +### 2. Процесс AS IS и TO BE + +- Фокус на пользовательских и бизнес-изменениях. +- Не указывать технические детали (платформы, API, внутренние интеграции). + +### 3. Ограничения + +- Ограничения и допущения в техническом и бизнесовом плане. + +### 4. Критерии приемки + +- Описывать с точки зрения пользователя. +- Не добавлять технические детали (платформы, API, внутренние компоненты). + +### 5. Архитектура + +Нужно указать: + +- схему контейнеров, +- таблицу интеграций, +- сквозные интеграционные сценарии. + +Слои: + +- `ui` - web-приложение, клиент. +- `ufs` - BFF: аутентификация/авторизация, агрегация и маппинг данных. +- `pprb` - backend: API, БД, логика жизненного цикла сущностей. + +#### Диаграмма + +Mermaid-диаграмма должна содержать: + +- основные контейнеры, +- названия приложений и платформ, +- интеграции между приложениями, +- названия вызываемых endpoint или топиков. + +#### Таблица интеграций + +Обязательные колонки: + +- Код +- Название endpoint/топика +- Источник данных +- Потребитель данных +- Инициатор вызова +- Передаваемые данные + +#### Сквозной интеграционный сценарий + +- Нумерованный список вызовов вида: «Компонент 1 вызывает endpoint в Компонент 2». +- Только интеграционная цепочка, без детального разбора логики. + +### 6. Описание изменений + +Раздел состоит из подразделов уровня `##` (например, `6.1`, `6.2`, `6.3`). + +Под корнем раздела `# 6` указываются общие метаданные: + +- `domain` +- `sub_domain` + +Для каждого раздела `6.x` обязательно указывать метаданные строками сразу после заголовка: + +- `id` +- `doc_type` +- `application` +- `platform` + +#### 6.x для `ui_page` + +Обязательная структура: + +- `### Технический use case (тезисно)` +- `### Требования к UI` +- `### Функциональные требования` +- `### Нефункциональные требования` + +Требования к разделу `### Требования к UI`: + +- Внутри нужно отдельно описывать каждую UI-форму. +- Если есть интеграция, обязательно описать, как показывается ошибка. +- Если показываем список, обязательно описать, как показывается отсутствие данных. + +Рекомендуемая детализация UI-форм: + +- табличное представление, +- пустой список (empty state), +- ошибка (error state). + +Правила описания UI-полей: + +- Поля описывать списком (не таблицей). +- Общие правила (например, read-only, поведение при пустом значении) выносить в общий блок, не дублировать для каждого поля. + +Нефункциональные требования для `ui_page`: + +- пользовательская аналитика оформляется таблицей с колонками: + - `Название события` + - `Описание` + - `Точка вызова` + - `Payload` + +#### 6.x для `api_method` + +Обязательная структура: + +- `### Технический use case (тезисно)` +- `### Функциональные требования` +- `### Нефункциональные требования` +- `### Контракт метода` + +Правило для функциональных требований: + +- Если дополнительных требований нет (дублируют сценарий), писать: `Не выявлены`. + +Нефункциональные требования: + +- Разделять на подразделы: + - `#### Аудит` (если применимо) + - `#### Мониторинг` + +Для `Мониторинг` использовать таблицу с колонками: + +- `Метрика` +- `Описание` +- `Условие срабатывания` + +Важно: + +- В мониторинге описывать условия срабатывания, а не «точку измерения = метод». +- Базово закладывать 3 метрики: + - `_SUCCESS` + - `_FAIL` + - `_BUSINESS_ERROR` + +Контракт метода: + +- Для запроса: таблица параметров (`header/query/path`) с колонками: название, тип параметра, тип данных, обязательность, описание, пример. +- Для тела JSON (если есть): структура отдельной таблицей. +- Для ответа JSON: таблица с колонками: название, тип данных, обязательность, описание, заполнение, пример. + +#### 6.x для `logic_block` + +Обязательная структура: + +- `### Технический use case (тезисно)` +- `### Функциональные требования` +- `### Нефункциональные требования` + +## Дополнительные правила по слоям + +- Проверка ролевой модели пользователя обычно выполняется на уровне `ufs`. +- Для `pprb` аудит может не фиксироваться, если это правило принято для конкретной фичи/домена. +- Если проверка ролей вынесена в `ufs`, не дублировать этот шаг в сценарии `pprb`. + +## Термины + +- Аудит: события, которые фиксируют действия пользователя и позволяют ответить на вопрос «кто, что, когда сделал». +- Мониторинг: технические события/метрики для контроля стабильности и поиска сбоев. + diff --git a/_process/04. Analitycs artefacts.md b/_process/04. Analitycs artefacts.md deleted file mode 100644 index 2415f32..0000000 --- a/_process/04. Analitycs artefacts.md +++ /dev/null @@ -1,852 +0,0 @@ -## 1. Формат ведения технической документации агентом - -## 1.1. Общие принципы - -Техническая документация, формируемая агентом, должна строиться как **система атомарных, не пересекающихся по смыслу документов**, связанных между собой явными ссылками. - -Ключевые принципы: -- один документ описывает одну сущность или один устойчивый технический аспект; -- документ не должен дублировать соседние документы; -- общая система знаний должна собираться через ссылки, а не через копипасту; -- структура документации должна быть пригодна как для чтения человеком, так и для индексирования в RAG. - -## 1.2. Базовые типы документных единиц - -На первом этапе логично сохранить текущую семантику типов документов, но перенести ее в файловую модель. - -Базовые типы: -- `ui_page` -- `api_method` -- `logic_block` - -Позже могут добавиться: -- `architecture_overview` -- `integration_doc` -- `domain_entity` -- `glossary_item` -- `index_page` - -## 1.3. Принцип декомпозиции страниц / файлов - -### Один устойчивый объект — один документ -Если объект можно переиспользовать или на него могут ссылаться другие документы, его надо выносить в отдельный файл. - -Примеры: -- отдельная UI-страница; -- отдельный API endpoint; -- отдельный блок логики; -- отдельный интеграционный сценарий. - -### Документы не должны пересекаться по смыслу -Если описание повторяется в нескольких местах, нужно выделять общий документ и ссылаться на него. - -Примеры: -- фронтальная страница не должна заново описывать логику API; -- документ по API не должен заново раскрывать общую логику переиспользуемого блока; -- вместо дублирования должен быть переход по ссылке. - -### Use case и детальные правила живут раздельно -Сценарий описывает поток работы, а детали выносятся в функциональные требования, отдельные блоки логики или контрактные описания. - -Это важно и для RAG-индексации: -- сценарии индексируются как workflows; -- отдельные правила — как facts; -- сущности и блоки — как entities. - -## 1.4. Иерархическая организация документации - -Документация должна быть организована как иерархическое дерево каталогов и файлов, а не как набор неструктурированных страниц. - -Пример верхнего уровня: - -```text -docs/ - ui/ - api/ - logic/ - domains/ - integrations/ - architecture/ - glossary/ - errors/ -``` - -Пример организации: - -```text -docs/ - ui/ - order-create-page.md - order-edit-page.md - api/ - orders-create.md - orders-get.md - logic/ - order-validation.md - order-enrichment.md - architecture/ - system-overview.md - integration-landscape.md - errors/ - catalog.yaml -``` - -## 1.5. Учет связей между документами - -Связи должны быть **явными и поддерживаемыми агентом**. - -Примеры: -- UI-страница ссылается на API, который она вызывает; -- API-документ ссылается на переиспользуемые логические блоки; -- логический блок ссылается на связанные интеграции; -- архитектурный обзор ссылается на набор конкретных модулей и документов; -- документ по коду может ссылаться на системную аналитику, которая инициировала изменение. - -Именно эта сеть ссылок затем индексируется в слоях: -- `D1_DOCUMENT_CATALOG` -- `D3_ENTITY_CATALOG` -- `D4_WORKFLOW_INDEX` -- `D5_REFERENCE_GRAPH` -- `D6_DOC_CODE_LINKS` - -## 1.6. Формат markdown-документов - -Основной формат технической документации — `Markdown`. - -Каждый документ состоит из двух частей: -1. **YAML frontmatter** — структурные метаданные; -2. **Markdown body** — основное содержимое по шаблону. - -## 3.7. YAML frontmatter - -Frontmatter нужен для: -- определения типа документа; -- идентификации документа; -- определения его места в иерархии; -- фиксации связей с кодом и другими документами; -- выделения сущностей и тегов; -- упрощения построения слоев `D1`, `D3`, `D5`, `D6`. - -### Базовый frontmatter - -```yaml ---- -id: ui-order-create-page -title: Страница создания заказа -doc_type: ui_page -domain: orders -status: draft -related_docs: - - api-orders-create - - logic-order-validation -entities: - - Order - - CreateOrder -tags: - - ui - - orders - - creation -#owner: system-analyst -#source_of_truth: code -#related_code: -# - src/orders/ui/create_page.tsx -# - src/orders/api/orders_controller.py ---- -``` - -### Обязательные поля - -- `id` — стабильный уникальный идентификатор документа; -- `title` — человекочитаемый заголовок; -- `doc_type` — тип документа; -- `related_docs` — ссылки на связанные документы; -- `status` — статус документа; -- `domain` - домен фичи (Карточка клиента, Задачи, Сделки, Предложения) -- `sub_domain` - поддомен внутри основной сущности (Счета, ЗДА, ECM) - -### Рекомендуемые поля -- `owner` -- `entities` -- `tags` -- `parent` -- `children` -- `feature` -- `system_analytics_refs` -- `business_refs` -- `updated_from` -- `reviewers` -- `source_of_truth` -- `related_code` - -### Допустимые значения `doc_type` -- `ui_page` -- `api_method` -- `logic_block` -- `architecture_overview` -- `integration_doc` -- `domain_entity` -- `glossary_item` -- `index_page` - -### Допустимые значения `status` -- `draft` -- `in_review` -- `approved` -- `outdated` -- `generated` -- `active` - -### Допустимые значения `source_of_truth` -- `code` -- `doc` -- `system_analysis` -- `business_requirements` -- `mixed` - -## 1.8. Typed frontmatter для разных типов документов - -У каждого типа документа есть: -- **общие поля**; -- **тип-специфичные поля**. - -### Пример для `api_method` - -```yaml ---- -id: api.create_invoice -doc_type: api_method -domain: billing -title: Создание инвойса - -endpoint: POST /api/v1/invoices -auth: USER -idempotent: false -timeout_ms: 3000 - -links: - called_by: - - ui.invoice_form - uses_logic: - - logic.invoice_validation - writes_db: - - db.invoices - - db.invoice_items - integrates_with: - - int.crm_sync - -related_docs: - - ui.invoice_form - - logic.invoice_validation -related_code: - - services/billing/api/create_invoice.py -entities: - - Invoice - - CreateInvoice - -tags: - - invoice - - create - - billing -status: active -version: 1.3 -source_of_truth: code ---- -``` - -### Для `api_method` полезно поддерживать - -- `endpoint` -- `sup_parameters` -- `role_model_actions` -- `monitoring_actions` -- `audit_actions` -- `idempotent` -- `timeout_ms` -- `links.called_by` -- `links.uses_logic` -- `links.writes_db` -- `links.integrates_with` - -### Для `ui_page` позже полезно поддерживать -- `calls_api` -- `user_analitycs_actions` -- `sup_parameters` -- `role_model_actions` -- `entry_points` -- `uses_logic` - -### Для `logic_block` полезно поддерживать - -- `called_from` -- `uses_logic` -- `reads_db` -- `writes_db` -- `integrates_with` - -## 1.9. Двухслойная структура документа: `Summary` + `Details` - -LLM не должна каждый раз тонуть в полном документе. Поэтому каждый документ должен содержать два уровня представления. - -### `Summary` -Короткая, строго структурированная спецификация. Это слой **быстрого контекста**. - -Рекомендуемый объем: -- примерно 30–60 строк; -- без длинных пояснений; -- только ключевые факты. - -Пример: - -```md -## Summary -- Purpose: создание инвойса из формы -- Actor: пользователь -- Trigger: Submit -- Main API: POST /api/v1/invoices (api.create_invoice) -- Validation: required fields, amount > 0, date <= today -- Errors: 400(field errors), 409(duplicate), 503(retryable) -- Analytics: event invoice_submit, invoice_error -``` - -### `Details` -Полное раскрытие объекта: -- use case; -- функциональные требования; -- UI; -- API; -- integrations; -- ошибки; -- НФТ; -- связи; -- кодовые привязки. - -### Блок `## Integrations` - -Если у объекта есть интеграции, они должны быть выделены в отдельный блок `## Integrations`. -Интеграции не нужно дублировать во frontmatter. -Основное описание хранится в body документа. - -Ожидаемый принцип: -- одна интеграция = одна отдельная запись внутри блока; -- у интеграции есть краткое имя; -- у интеграции есть структурированные атрибуты; -- дополнительные детали допускаются в свободной форме через вложенный словарь. - -Рекомендуемые атрибуты интеграции: -- `target` -- `target_type` -- `direction` -- `interaction` -- `via` -- `purpose` -- `details` - -Где: -- `target` - идентификатор или имя целевого объекта; -- `target_type` - тип цели: `api`, `ui`, `entity`, `service`, `queue`, `db`, `external_system`; -- `direction` - направление: `inbound`, `outbound`, `bidirectional`; -- `interaction` - тип взаимодействия: `calls`, `reads`, `writes`, `emits`, `consumes`, `depends_on`; -- `via` - технический канал интеграции; -- `purpose` - зачем нужна интеграция; -- `details` - словарь с гибкой структурой под дополнительные параметры. - -Пример: - -```md -## Integrations - -### Orders API -- target: api.orders.create -- target_type: api -- direction: outbound -- interaction: calls -- via: POST /api/orders -- purpose: создание заказа -- details: - - auth: service-token - - retry: true - -### Order Entity -- target: domain.order -- target_type: entity -- direction: outbound -- interaction: writes -- via: repository -- purpose: сохранение состояния заказа -- details: - - transaction: required -``` - -Этот блок должен быть пригоден и для чтения человеком, и для последующего извлечения в отдельный RAG-слой интеграций. - -## 1.10. Общие требования к markdown body - -1. В документе должен быть один `H1`, совпадающий с `title`. -2. Основные разделы используют `H2`. -3. Подразделы внутри разделов используют `H3`. -4. Не должно быть хаотической вложенности заголовков. -5. Один раздел должен описывать одну смысловую часть. -6. Текст не должен дублировать соседние документы. -7. Вместо дублирования должны использоваться явные ссылки на связанные документы. -8. Сценарии, правила, ограничения и ссылки на код должны быть отделены друг от друга. - -## 1.11. Базовый каркас markdown-документа - -```md ---- -id: api-orders-create -title: Метод создания заказа -doc_type: api_method -domain: orders -status: draft -source_of_truth: code -related_docs: - - logic-order-validation - - ui-order-create-page -related_code: - - src/orders/api/create_order.py -entities: - - Order - - CreateOrder -tags: - - api - - orders ---- - -# Метод создания заказа - -## Summary -- Purpose: создание заказа -- Actor: пользователь -- Trigger: submit формы -- Main API: POST /orders - -## Details -### Описание -### Технический use case -### Функциональные требования -### Нефункциональные требования -### Контракт - - -## 3.13. Специализированные шаблоны документов - -### UI Page - -```md -# <Название страницы> - -## Summary -## Назначение -## Контекст -## Технический use case -## Описание UI -## UI Elements -## Функциональные требования -## Нефункциональные требования -## Связанные API -## Связанные блоки логики -## Связанный код -## Связанные документы -## История изменений -``` - -#### Требования к разделу `Описание UI` -Для каждого элемента желательно описывать: -- тип элемента; -- назначение; -- источник данных; -- default / placeholder; -- правила активации; -- поведение при взаимодействии; -- валидацию. - -#### Требования к разделу `UI Elements` -UI-элементы должны храниться в **табличном** или **полуструктурированном** виде. - -Пример: - -```md -## UI Elements - -| id | type | label | data_source | validation | behavior | -|--------|--------|---------|------------|------------|----------| -| amount | input | Amount | local | >0 | enables submit | -| submit | button | Create | - | - | calls api.create_invoice | -``` - -Если модель UI сложная, допустим sidecar-файл `ui_elements.yaml` или `ui_elements.json` рядом с основным документом. - -### API Method - -```md -# <Название API метода> - -## Summary -## Назначение -## Контекст -## Технический use case -## Функциональные требования -## Contract -## Integrations -## Errors -## Нефункциональные требования -## Связанные блоки логики -## Связанный код -## Связанные документы -## История изменений -``` - -#### Требования к разделу `Contract` -Контракт может: -- быть кратко описан прямо в документе; -- ссылаться на OpenAPI; -- ссылаться на отдельный контрактный файл. - -Для REST API целевым источником истины должен становиться `OpenAPI`. - -### Reusable Logic Block - -```md -# <Название блока логики> - -## Summary -## Назначение -## Контекст -## Технический use case -## Функциональные требования -## Integrations -## Ограничения и условия вызова -## Нефункциональные требования -## Связанные API / UI / integration points -## Связанный код -## Связанные документы -## История изменений -``` - -## 3.14. Машинно-читаемые API-контракты - -Для API контрактов **источником истины** должен становиться: -- `OpenAPI` — предпочтительно; -- либо временно строгий markdown/yaml-контракт, если OpenAPI еще нет. - -Минимальный набор для API-контракта: -- `endpoint` -- `method` -- `request fields` -- `required / optional` -- `constraints` -- `response` -- `errors` -- `idempotency` -- `retry` -- `timeout` -- `auth` - -## 3.15. Каталог ошибок - -Ошибки, HTTP-коды, retry-правила и клиентское поведение не должны размазываться по разным документам. - -Нужен единый каталог ошибок, например `docs/errors/catalog.yaml`. - -Пример: - -```yaml -errors: - - error_id: invoice_validation_failed - http_code: 400 - internal_code: BILLING_400_01 - when: invalid request fields - client_behavior: show field errors - retry: false - owner: billing - - - error_id: invoice_duplicate - http_code: 409 - internal_code: BILLING_409_01 - when: duplicate invoice detected - client_behavior: show duplicate warning - retry: false - owner: billing - - - error_id: crm_sync_unavailable - http_code: 503 - internal_code: BILLING_503_02 - when: downstream CRM unavailable - client_behavior: retry later - retry: true - owner: billing -``` - -В API- и logic-документах лучше ссылаться на `error_id`, а не заново подробно описывать каждую ошибку. - -## 3.16. Требования к качеству документа для RAG - -1. **Явные заголовки** — не использовать безымянные блоки текста. -2. **Атомарные утверждения** — не смешивать несколько правил в одном пункте, если их можно разделить. -3. **Явные сущности** — использовать стабильные названия компонентов, API, модулей, страниц. -4. **Явные ссылки** — не писать «этот метод», если можно указать конкретную ссылку или идентификатор. -5. **Минимум дублирования** — повторяющийся контент должен заменяться ссылками. -6. **Привязка к коду** — если документ описывает кодовый объект, это должно быть явно указано. -7. **Разделение сценариев и правил** — workflow и fact-like требования должны быть отделены. - -## 3.17. Как структура markdown помогает RAG - -- `frontmatter` + заголовки → `D1_DOCUMENT_CATALOG` -- `entities`, `tags`, устойчивые термины → `D3_ENTITY_CATALOG` -- атомарные функциональные и нефункциональные требования → `D2_FACT_INDEX` -- `Технический use case` → `D4_WORKFLOW_INDEX` -- `related_docs`, явные ссылки → `D5_REFERENCE_GRAPH` -- `related_code`, упоминания symbols и файлов → `D6_DOC_CODE_LINKS` -- `Summary` → быстрый retrieval и short-form context для LLM - -## 3.18. Принципы генерации документации агентом - -Когда документ пишет агент, он должен: -- сначала извлечь evidence из кода, системной аналитики и существующих документов; -- определить тип документа; -- заполнить frontmatter; -- построить markdown body по шаблону; -- явно указать связи с кодом и другими документами; -- не дублировать уже существующее описание, если можно сослаться на него. - ---- - - -## 4.4. Layered RAG - -RAG строится как система специализированных слоев для двух основных доменов: -- `CODE RAG` -- `DOCS RAG` - -Каждый graph извлекает контекст не из одного общего индекса, а из нужного набора слоев в зависимости от intent. - -## 4.5. Evidence gate - -Перед синтезом ответа или документа агент должен проверять, хватает ли опоры. - -Примеры: -- найден ли symbol; -- найдено ли достаточное количество code chunks; -- есть ли supporting relations; -- есть ли document evidence; -- есть ли docs ↔ code mapping. - -Если опоры недостаточно, агент должен: -- деградировать в упрощенный режим; -- честно фиксировать неполноту ответа; -- при необходимости уходить в fallback. - -## 4.6. Synthesis layer - -На этом этапе LLM: -- агрегирует найденные артефакты; -- формирует объяснение; -- пишет документ; -- структурирует результат под нужный шаблон. - -LLM не должна быть основным источником фактов. Фактическая основа должна приходить из RAG и диагностируемого pipeline. - -## 4.7. Diagnostics - -Система должна сохранять диагностический след: -- какой graph был выбран; -- какие слои использовались; -- что было найдено; -- где retrieval был слабым; -- почему был выбран fallback; -- какие evidence стали основой ответа. - -## 4.8. Сценарии: Target Architecture vs MVP-now - -### 4.8.1. Target Architecture - -#### CODE -- `OPEN_FILE` — открыть конкретный файл; -- `OPEN_SYMBOL` — открыть класс / функцию / метод; -- `EXPLAIN` — объяснить, как работает сущность или фрагмент; -- `FIND_TESTS` — найти релевантные тесты; -- `FIND_ENTRYPOINTS` — найти основные точки входа; -- `RELATED_CODE` — найти связанные сущности и ближайший контекст. - -#### DOCS -- `DOC_SEARCH` — найти релевантный фрагмент документации; -- `DOC_EXPLAIN` — кратко объяснить, что сказано в документации по теме; -- `DOC_ENTITY_LOOKUP` — найти разделы, связанные с сущностью или компонентом; -- `GENERATE_DOCS_FROM_CODE` — сформировать документацию по коду с нуля для модуля, класса, функции, компонента или сценария. - -#### CROSS-DOMAIN -- `FIND_IMPLEMENTATION_BY_DOC` — найти реализацию по описанию; -- `FIND_DOC_BY_CODE` — найти документацию по коду; -- `COMPARE_DOCS_AND_CODE` — базовое сопоставление документации и реализации. - -#### GENERAL / FALLBACK -- `GENERAL_QA` — общий сценарий ответа на вопрос, если домен или интент не удалось определить уверенно. - -### 4.8.2. MVP-now - -В текущем цикле фокус на сценариях: - -- `OPEN_FILE` -- `EXPLAIN` -- `FIND_TESTS` -- `FIND_ENTRYPOINTS` -- `GENERAL_QA` - -DOCS и CROSS_DOMAIN остаются частью target architecture; в текущем цикле они не являются обязательной частью test-first MVP. - ---- - -## 5. Структура слоев RAG - -## 5.1. CODE RAG - -### C0 — Source Chunks -**Назначение:** базовые фрагменты исходного кода. -**Единица:** chunk кода. -**Как формируется:** исходные файлы обходятся и режутся на chunk’и с учетом структурных границ. -**Статус в MVP:** да. - -### C1 — Symbol Catalog -**Назначение:** каталог модулей, классов, функций, методов и других значимых сущностей. -**Единица:** symbol. -**Как формируется:** из AST и синтаксического разбора кода. -**Статус в MVP:** да. - -### C2 — Symbol Relations -**Назначение:** связи между symbols. -**Единица:** relation. -**Как формируется:** вторым проходом по AST и структурным зависимостям. -**Статус в MVP:** да, в ограниченном виде. - -### C3 — Entrypoints -**Назначение:** каталог точек входа системы. -**Единица:** entrypoint. -**Как формируется:** специализированными детекторами entrypoint-паттернов. -**Статус в MVP:** да, минимально. - -### C4 — Execution Paths -**Назначение:** типовые пути исполнения. -**Единица:** path. -**Как формируется:** поверх `C2` и `C3` через производную трассировку. -**Статус в MVP:** нет. - -### C5 — Test Mappings -**Назначение:** связи production code ↔ tests. -**Единица:** mapping. -**Как формируется:** по путям, именам, импортам и конвенциям проекта. -**Статус в MVP:** да, минимально. - -### C6 — Code Facts -**Назначение:** нормализованные факты из кода. -**Единица:** fact. -**Как формируется:** поверх `C1–C3` как производный слой. -**Статус в MVP:** нет. - -## 5.2. DOCS RAG - -### D0 — Document Chunks -**Назначение:** базовые фрагменты документации. -**Единица:** document chunk. -**Как формируется:** документы нормализуются и режутся на chunk’и с сохранением `section path`. -**Статус в MVP:** да. - -### D1 — Document Catalog -**Назначение:** каталог документов и разделов. -**Единица:** `document node / section node`. -**Как формируется:** из структуры документов и их заголовков. -**Статус в MVP:** да. - -### D2 — Fact Index -**Назначение:** атомарные факты из документации. -**Единица:** fact. -**Как формируется:** из `D0/D1` через правила, шаблоны и при необходимости LLM extraction с валидацией. -**Статус в MVP:** частично. - -### D3 — Entity Catalog -**Назначение:** каталог сущностей и понятий документации. -**Единица:** entity / concept. -**Как формируется:** из устойчивых терминов, заголовков, словарей и нормализации повторяющихся сущностей. -**Статус в MVP:** да, минимально. - -### D4 — Workflow Index -**Назначение:** процедуры, сценарии, последовательности шагов. -**Единица:** workflow. -**Как формируется:** из use case, процессных разделов и последовательных описаний шагов. -**Статус в MVP:** нет. - -### D5 — Reference Graph -**Назначение:** граф ссылок между документами, секциями, сущностями и фактами. -**Единица:** reference link. -**Как формируется:** из явных и неявных cross-links между документами. -**Статус в MVP:** нет. - -### D6 — Doc-Code Links -**Назначение:** мост между документацией и кодом. -**Единица:** `doc artifact ↔ code artifact link`. -**Как формируется:** из имен, aliases, путей, устойчивых терминов и других надежных соответствий. -**Статус в MVP:** да, минимально. - -## 5.3. Layer scope: Target Architecture vs MVP-now - -### 5.3.1. Target Architecture - -Полная карта слоёв: - -- **CODE:** C0–C6 (Source Chunks, Symbol Catalog, Symbol Relations, Entrypoints, Execution Paths, Test Mappings, Code Facts) -- **DOCS:** D0–D6 (Document Chunks, Document Catalog, Fact Index, Entity Catalog, Workflow Index, Reference Graph, Doc-Code Links) - -### 5.3.2. MVP-now - -**Обязательные сейчас:** - -- `C0_SOURCE_CHUNKS` -- `C1_SYMBOL_CATALOG` -- `C2_SYMBOL_RELATIONS` -- `C3_ENTRYPOINTS` - -**В облегчённом виде:** - -- `C5_TEST_MAPPINGS` или `C5-lite` - -**Не блокируют текущий этап:** - -- `C4_EXECUTION_PATHS` -- `C6_CODE_FACTS` -- весь docs runtime (слои D0–D6 в исполнении runtime) - -Слои документации остаются частью target architecture; docs retrieval пока не обязателен для текущего code-first milestone. - ---- - -## 6. Итоговая рамка MVP-now - -Сейчас система должна стабильно работать в **test-first** режиме. - -**Фокус:** - -- CODE_QA; -- через тесты настраиваются: - - intent routing (IntentRouterV2); - - layered retrieval; - - evidence sufficiency; - - answer quality; - - diagnostics. - -**Не входят в текущий milestone:** - -- UI-интеграция; -- docs runtime; -- полная интеграция orchestration переносится на следующий этап после стабилизации test pipeline. - -В целевой архитектуре по-прежнему заложены: -- уверенная работа с кодом, symbols, entrypoints, тестами; -- ответ по документации и мост docs ↔ code; -- генерация документации по коду; -- fallback при неуверенном роутинге. - -В MVP-now сознательно **не включаются** самые дорогие части: -- полноценные execution paths для всей системы; -- богатые fact-индексы по всем доменам; -- полный reference graph документации; -- глубокая автоматизация подготовки системной аналитики. diff --git a/_process/05. MVP - process_v2.1.md b/_process/05. MVP - process_v2.1.md new file mode 100644 index 0000000..5676206 --- /dev/null +++ b/_process/05. MVP - process_v2.1.md @@ -0,0 +1,33 @@ +Нужно реализовать 2 вещи + +Создать процесс внесения изменений в файл документации +Создать контекст этого процесса + +Контекст наполнять атрибутами +что-то явно задано, фоллбэк через ллм + + + +Написать тестовую аналитику - круд над сущностью +фронт, ефс, ппрб +Все в своей БД +Атрибуты сущности задать в требованиях + + + + +Аналитика имеет структуру +Внутри модули - один модуль на правку одного файла. + + +Модуль извлекается из аналитики парсером и из него формируется задача на редактирование файла +если парсер не сработал - фоллбэк ан ллм + + + +Процесс редактирования работает стандартно + + + + + diff --git a/_process/doc_rules_v3/README.md b/_process/doc_rules_v3/README.md new file mode 100644 index 0000000..ba167b4 --- /dev/null +++ b/_process/doc_rules_v3/README.md @@ -0,0 +1,37 @@ +# Documentation Rules V3 + +Этот каталог содержит правила генерации технической документации из системной аналитики. + +## Цель +- синхронизировать требования к документации с требованиями к аналитике (`04. Analitycs artefacts - features.md`); +- сохранить детальность техдокументации по сравнению с аналитикой; +- убрать дублирование структуры и manifest-слоя между разными файлами; +- собирать итоговый промпт из модулей: глобальные правила + template с manifest + блоки. + +## Структура +- `documentation-rules.md` — верхнеуровневый регламент и порядок сборки. +- `global/` — общие правила (заголовки, frontmatter, слой ответственности, мост аналитика->документация). +- `common-elements/` — правила для общих блоков (`summary`, `details`, `use case`, `FR`, `NFR`, `UI`, `Contract`). +- `templates/` — единственный источник истины для структуры итоговой страницы и manifest-метаданных типа документа. + +## Принцип сборки +Для конкретного документа агент собирает единый набор правил из: +1. `documentation-rules.md` +2. `global/*.md` +3. `templates/.template.md` +4. `common-elements/*.md`, указанных в frontmatter template + +## Правило без дублирования +- `templates/` отвечают за структуру документа, порядок разделов и manifest-метаданные типа. +- `common-elements/` отвечают только за правила написания конкретного раздела. +- отдельный слой `types/` не нужен, если для типа документа используется один основной template. + +## Формат template-manifest +Manifest оформляется в YAML frontmatter самого template. + +Обязательные поля manifest: +- `doc_type` +- `required_common_elements` + +Рекомендуемые поля: +- `special_rules` diff --git a/_process/doc_rules_v3/common-elements/api-contract.md b/_process/doc_rules_v3/common-elements/api-contract.md new file mode 100644 index 0000000..2070782 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/api-contract.md @@ -0,0 +1,21 @@ +# API Contract Rules + +## Обязательные части +- request parameters (`header/query/path`) +- request body (если применимо) +- response body +- errors +- auth +- timeout +- retry/idempotency (если применимо) + +## Табличный формат +Для request/response таблицы должны содержать: +- название +- тип данных +- обязательность +- описание +- пример + +Для response дополнительно: +- заполнение (mapping/логика источника данных) diff --git a/_process/doc_rules_v3/common-elements/db-columns.md b/_process/doc_rules_v3/common-elements/db-columns.md new file mode 100644 index 0000000..5fb7f0a --- /dev/null +++ b/_process/doc_rules_v3/common-elements/db-columns.md @@ -0,0 +1,17 @@ +# DB Columns Rules + +## Формат +Структура таблицы оформляется таблицей. + +## Обязательные колонки +- `Поле` +- `Тип` +- `Nullable` +- `Описание` +- `Источник заполнения` +- `Использование` + +## Правила +- перечислять все ключевые поля таблицы; +- для служебных полей (`id`, `created_at`, `updated_at`, `deleted_at`) явно описывать назначение; +- если тип или nullable не заданы в аналитике, допускается инженерное предположение с рабочим вариантом. diff --git a/_process/doc_rules_v3/common-elements/db-constraints.md b/_process/doc_rules_v3/common-elements/db-constraints.md new file mode 100644 index 0000000..9cacbf0 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/db-constraints.md @@ -0,0 +1,16 @@ +# DB Constraints Rules + +## Что включать +- primary key; +- unique constraints; +- foreign keys; +- важные индексы; +- бизнес-ограничения на уровне БД. + +## Формат +- списком или таблицей; +- для каждого индекса и ограничения писать, зачем оно нужно. + +## Правила +- если индекс нужен для сценария чтения/пагинации, это должно быть явно сказано; +- если точные названия индексов неизвестны, можно использовать осмысленные проектные названия. diff --git a/_process/doc_rules_v3/common-elements/db-purpose.md b/_process/doc_rules_v3/common-elements/db-purpose.md new file mode 100644 index 0000000..c965a3b --- /dev/null +++ b/_process/doc_rules_v3/common-elements/db-purpose.md @@ -0,0 +1,12 @@ +# DB Table Purpose Rules + +## Что описывать +- назначение таблицы; +- в каком сценарии она используется; +- кто является владельцем данных; +- является ли таблица источником истины или производным хранилищем. + +## Формат +- 1-3 абзаца без воды; +- явно указывать доменную сущность, которую хранит таблица; +- если сделаны допущения по БД, фиксировать их отдельной фразой. diff --git a/_process/doc_rules_v3/common-elements/db-usage.md b/_process/doc_rules_v3/common-elements/db-usage.md new file mode 100644 index 0000000..6d7ab2e --- /dev/null +++ b/_process/doc_rules_v3/common-elements/db-usage.md @@ -0,0 +1,11 @@ +# DB Usage Rules + +## Что описывать +- какие API / logic block / batch job используют таблицу; +- какие операции выполняются: read / insert / update / delete; +- как таблица участвует в пользовательском сценарии. + +## Правила +- ссылки на связанные документы давать по `doc_id` или path; +- не дублировать полный use case, а показывать роль таблицы в сценарии; +- если таблица используется для пагинации, фильтрации или сортировки, это нужно отметить явно. diff --git a/_process/doc_rules_v3/common-elements/details.md b/_process/doc_rules_v3/common-elements/details.md new file mode 100644 index 0000000..329f069 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/details.md @@ -0,0 +1,10 @@ +# Details Rules + +## Назначение +Этот файл задает общие правила для секции `## Details`. + +## Правила +- `Details` оформляется как `## Details`. +- Внутри `Details` используются заголовки уровня `###` и ниже. +- Структура `Details` определяется template типа документа. +- В `Details` не нужно дублировать навигацию и связи, если они уже есть во frontmatter. diff --git a/_process/doc_rules_v3/common-elements/fr.md b/_process/doc_rules_v3/common-elements/fr.md new file mode 100644 index 0000000..0e3e206 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/fr.md @@ -0,0 +1,31 @@ +# Functional Requirements Rules + +## Формат +- `FR.<номер>. <Название>` +- Нумерация инкрементальная внутри документа. + +## Правила +- FR расширяют шаги сценария. +- FR не копируют шаги сценария без добавления новой информации. +- Для интеграционных шагов FR обязательны. +- Если в сценарии есть вызов внешнего API / сервиса / БД, нужен отдельный FR на интеграцию. + +## FR для интеграционных шагов +Для интеграционного FR обязательно раскрывать: +- как формируется запрос; +- откуда берется каждый значимый атрибут запроса; +- какой downstream вызывается; +- какой ответ считается успешным; +- какие ответы и ситуации считаются бизнес-ошибкой; +- какие ситуации считаются технической ошибкой; +- как downstream-ответ маппится в контракт текущего слоя. + +## FR для шагов доступа к БД +Если шаг читает или пишет БД, FR должен по возможности включать: +- таблицу или набор таблиц; +- логику фильтрации; +- логику сортировки; +- логику пагинации; +- пример SQL или близкий к рабочему псевдо-SQL. + +Если СУБД и диалект не заданы, допускается сделать рабочее предположение и явно зафиксировать его. diff --git a/_process/doc_rules_v3/common-elements/nfr.md b/_process/doc_rules_v3/common-elements/nfr.md new file mode 100644 index 0000000..32ab9cd --- /dev/null +++ b/_process/doc_rules_v3/common-elements/nfr.md @@ -0,0 +1,20 @@ +# Non-Functional Requirements Rules + +## Для api_method +- Подразделы: + - `#### Аудит` (если применимо) + - `#### Мониторинг` + +## Мониторинг +Оформлять таблицей: +- `Метрика` +- `Описание` +- `Условие срабатывания` + +Запрещено: +- использовать «точка измерения = метод» вместо условий срабатывания. + +Базовые суффиксы метрик: +- `_SUCCESS` +- `_FAIL` +- `_BUSINESS_ERROR` diff --git a/_process/doc_rules_v3/common-elements/sql-example.md b/_process/doc_rules_v3/common-elements/sql-example.md new file mode 100644 index 0000000..5325f23 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/sql-example.md @@ -0,0 +1,15 @@ +# SQL Example Rules + +## Назначение +Секция показывает пример рабочего SQL для основного сценария использования таблицы. + +## Правила +- SQL должен быть близок к рабочему, а не абстрактным псевдокодом; +- если диалект БД не указан, допускается выбрать наиболее вероятный вариант и явно зафиксировать допущение; +- пример должен отражать реальный сценарий документа: чтение, вставка, обновление или агрегация; +- для read-сценариев по возможности показывать фильтрацию, сортировку и пагинацию; +- если есть join, нужно кратко пояснить, зачем он нужен. + +## Формат +- fenced code block с указанием `sql`; +- под кодом 1-3 поясняющих bullets о ключевых условиях, индексах и параметрах. diff --git a/_process/doc_rules_v3/common-elements/summary.md b/_process/doc_rules_v3/common-elements/summary.md new file mode 100644 index 0000000..a70a141 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/summary.md @@ -0,0 +1,10 @@ +# Summary Rules + +## Назначение +Этот файл задает правила для секции `## Summary`. + +## Правила +- `Summary` должен быть коротким слоем быстрого контекста. +- `Summary` должен объяснять суть документа без длинных деталей. +- Предпочтительный формат: краткий список ключевых фактов. +- `Summary` не должен дублировать `Details`. diff --git a/_process/doc_rules_v3/common-elements/tech-use-case.md b/_process/doc_rules_v3/common-elements/tech-use-case.md new file mode 100644 index 0000000..9ae3b64 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/tech-use-case.md @@ -0,0 +1,16 @@ +# Tech Use Case Rules + +## Обязательные части +- название +- предусловия +- триггер +- основной сценарий +- альтернативный сценарий +- обработка ошибок +- постусловие + +## Правила шага +- Один шаг = одно предложение до 15-20 слов. +- Формат шага: смысловое действие + техническая реализация (endpoint/топик/операция). +- Длинные технические детали выносить в FR и ссылаться на FR из шага. +- Для интеграционных шагов описание обработки ошибок обязательно. diff --git a/_process/doc_rules_v3/common-elements/ui-requirements.md b/_process/doc_rules_v3/common-elements/ui-requirements.md new file mode 100644 index 0000000..0de6134 --- /dev/null +++ b/_process/doc_rules_v3/common-elements/ui-requirements.md @@ -0,0 +1,22 @@ +# UI Requirements Rules + +## Структура блока +- `### Требования к UI` +- Внутри обязательно отдельные формы: + - табличное представление + - пустой список (empty state) + - ошибка (error state) + +## Обязательные правила +- Если есть интеграция, обязательно описывать показ ошибки. +- Если есть список, обязательно описывать показ отсутствия данных. + +## Описание UI-элементов +UI-элементы описываются строго в таблице. + +Обязательные колонки (где применимо): +- `Код элемента` +- `Название и описание` +- `Данные` +- `Поведение` +- `Валидация` diff --git a/_process/doc_rules_v3/common-elements/user-analytics.md b/_process/doc_rules_v3/common-elements/user-analytics.md new file mode 100644 index 0000000..f9fee1e --- /dev/null +++ b/_process/doc_rules_v3/common-elements/user-analytics.md @@ -0,0 +1,7 @@ +# User Analytics Rules + +События пользовательской аналитики оформлять таблицей: +- `Название события` +- `Описание` +- `Точка вызова` +- `Payload` diff --git a/_process/doc_rules_v3/documentation-rules.md b/_process/doc_rules_v3/documentation-rules.md new file mode 100644 index 0000000..9d0b6d9 --- /dev/null +++ b/_process/doc_rules_v3/documentation-rules.md @@ -0,0 +1,45 @@ +# Documentation Rules V3 + +## 1. Общий контракт +- Документация строится на основе системной аналитики, но на более детальном уровне. +- Заголовки отражают только суть раздела; метаданные в заголовках запрещены. +- Метаданные указываются во frontmatter и/или отдельными строками в body. +- Структура документа определяется только template соответствующего типа. +- Правила написания конкретного раздела определяются только соответствующим `common-elements` файлом. +- Manifest типа документа хранится во frontmatter соответствующего template. + +## 2. Источники требований +При генерации документа учитывать: +- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - documentation.md` +- `/Users/alex/Dev_projects_v2/ai driven app process/v2/agent/_process/04. Analitycs artefacts - features.md` +- правила v2 из `src/app/core/agent/processes/v2/doc_rules_v2` + +## 3. Разрыв аналитика vs документация +- Аналитика: концептуальная, укрупненная. +- Документация: технически детальная. +- Технический use case в документации не копирует аналитический 1-в-1, а детализирует его. +- Функциональные требования расширяют сценарий и не дублируют шаги без новой информации. + +## 4. Заполнение пробелов +Если атрибуты/детали отсутствуют в аналитике: +1. восстановить из формулировок аналитики; +2. уточнить по репозиторию (код, контракты, существующие документы); +3. зафиксировать в документации явно. + +## 5. Сборка итогового промпта +1. Загрузить global-правила. +2. Загрузить template типа документа. +3. Прочитать YAML frontmatter template как manifest. +4. Загрузить общие блоки, указанные в manifest. +5. Применить body template как единственный источник структуры. +5. Проверить чек-лист совместимости с аналитикой (domain/sub_domain, роли слоев, интеграции, ошибки). + +## 6. Формат manifest типа документа +Manifest типа документа хранится во frontmatter `templates/.template.md`. + +Минимальная схема: +- `doc_type` +- `required_common_elements` + +Дополнительно можно указывать: +- `special_rules` diff --git a/_process/doc_rules_v3/global/analytics-to-doc.md b/_process/doc_rules_v3/global/analytics-to-doc.md new file mode 100644 index 0000000..d495ace --- /dev/null +++ b/_process/doc_rules_v3/global/analytics-to-doc.md @@ -0,0 +1,10 @@ +# Analytics to Documentation Mapping + +## Принцип +- Системная аналитика задает «что». +- Документация детализирует «как». + +## Маппинг +- Из раздела архитектуры аналитики переносить контейнеры, интеграции и цепочки вызовов. +- Из раздела изменений аналитики строить отдельные технические страницы (`ui_page`, `api_method`, `logic_block`). +- Если в аналитике упрощенный use case, в документации раскрывать полный технический сценарий по правилам `tech-use-case.md`. diff --git a/_process/doc_rules_v3/global/filepaths.md b/_process/doc_rules_v3/global/filepaths.md new file mode 100644 index 0000000..f90d43f --- /dev/null +++ b/_process/doc_rules_v3/global/filepaths.md @@ -0,0 +1,67 @@ +# Правила определения путей файлов + +Текущая happy-path реализация строит путь документа по фиксированному шаблону: + +`docs////.md` + +Пример: + +`docs/orders/pprb/ui_page/orders.ui.list.md` + +## Источники атрибутов + +Для построения пути используются четыре основных атрибута: + +- `domain` +- `application` +- `platform` +- `doc_type` +- `id` как `doc_id` + +Если атрибуты явно указаны в подразделе `6.x`, нужно использовать их. +Если атрибут не указан, он может быть взят из общих метаданных аналитики или определен fallback-логикой. + +## Нормализация сегментов + +Каждый сегмент пути нормализуется одинаково: + +- значение переводится в lowercase; +- все символы, кроме `a-z`, `0-9`, `.`, `_`, `-`, заменяются на `-`; +- ведущие и хвостовые `.` и `-` удаляются. + +Примеры нормализации: + +- `Payment Status` -> `payment-status` +- `UFS Orders` -> `ufs-orders` +- `crm.mobile` -> `crm.mobile` + +## Значения по умолчанию + +Если после нормализации сегмент пустой, используются fallback-значения: + +- корневая папка: `domain`, иначе `application`, иначе `common` +- `platform` -> `web` +- `doc_type` -> `misc` +- `doc_id` -> `untitled` + +## Что важно в текущей версии + +- для корневой папки сначала используется `domain`; +- если `domain` не задан, используется `application`; +- `sub_domain` сейчас не участвует в построении пути; +- операции `create`, `update`, `delete` работают с одним и тем же правилом вычисления пути; +- специальных исключений для разных типов документов пока нет; +- отдельные каталоги для `pprb`, `ufs`, `web` задаются только через значение `platform`. + +## Практическое правило для агента + +Если нужно предложить или определить путь новой страницы, агент должен: + +1. определить `application`; +2. определить `domain`; +3. определить `platform`; +4. определить `doc_type`; +5. определить стабильный `doc_id`; +6. взять корневую папку как `domain`, а если он пустой, то `application`; +7. нормализовать все сегменты; +8. собрать путь по шаблону `docs////.md`. diff --git a/_process/doc_rules_v3/global/frontmatter.md b/_process/doc_rules_v3/global/frontmatter.md new file mode 100644 index 0000000..b0e94e1 --- /dev/null +++ b/_process/doc_rules_v3/global/frontmatter.md @@ -0,0 +1,32 @@ +# Frontmatter Rules + +## Обязательные поля +```yaml +id: string +title: string +doc_type: string +domain: string +sub_domain: string +related_docs: [] +status: string +``` + +## Рекомендуемые поля +```yaml +tags: [] +entities: [] +source_of_truth: string +related_code: [] +system_analytics_refs: [] +``` + +## Body-метаданные для секции изменений +Под корнем секции изменений указывать: +- `domain` +- `sub_domain` + +Для каждого подраздела `X.Y` указывать строками: +- `id` +- `doc_type` +- `application` +- `platform` diff --git a/_process/doc_rules_v3/global/header-rules.md b/_process/doc_rules_v3/global/header-rules.md new file mode 100644 index 0000000..1c6e075 --- /dev/null +++ b/_process/doc_rules_v3/global/header-rules.md @@ -0,0 +1,10 @@ +# Header Rules + +## Правила +- Заголовок описывает только смысл раздела. +- Не включать в заголовок: `id`, `doc_type`, `application`, `platform`, `domain`, `sub_domain`. +- Метаданные указываются отдельными строками ниже заголовка или во frontmatter. + +## Пример +- Правильно: `## 6.2 Метод UFS получения списка заказов` +- Неправильно: `## 6.2 Блок api_method (id=..., platform=ufs)` diff --git a/_process/doc_rules_v3/global/layer-responsibility.md b/_process/doc_rules_v3/global/layer-responsibility.md new file mode 100644 index 0000000..cb09845 --- /dev/null +++ b/_process/doc_rules_v3/global/layer-responsibility.md @@ -0,0 +1,10 @@ +# Layer Responsibility + +- `ui`: отображение, UX, запуск пользовательских сценариев. +- `ufs`: авторизация/аутентификация, агрегация, маппинг, оркестрация вызовов. +- `pprb`: API, БД, доменная логика backend. + +## Правила согласованности +- Проверка ролевой модели пользователя обычно фиксируется на уровне `ufs`. +- Если проверка роли вынесена в `ufs`, в `pprb`-сценарии не дублировать этот шаг. +- Аудит для `pprb` может отсутствовать, если это явно принято для домена/фичи. diff --git a/_process/doc_rules_v3/templates/api_method.template.md b/_process/doc_rules_v3/templates/api_method.template.md new file mode 100644 index 0000000..fb79085 --- /dev/null +++ b/_process/doc_rules_v3/templates/api_method.template.md @@ -0,0 +1,34 @@ +--- +doc_type: api_method +required_common_elements: + - common-elements/summary.md + - common-elements/details.md + - common-elements/tech-use-case.md + - common-elements/fr.md + - common-elements/nfr.md + - common-elements/api-contract.md +special_rules: + - Технический use case детализируется по `common-elements/tech-use-case.md`. + - FR расширяют use case и не дублируют шаги сценария без новой информации. + - Для интеграционных шагов FR обязательны. +--- + +# + +## Summary +Правила оформления: `../common-elements/summary.md` + +## Details +Правила оформления: `../common-elements/details.md` + +### Технический use case +Правила оформления: `../common-elements/tech-use-case.md` + +### Функциональные требования +Правила оформления: `../common-elements/fr.md` + +### Нефункциональные требования +Правила оформления: `../common-elements/nfr.md` + +### Контракт +Правила оформления: `../common-elements/api-contract.md` diff --git a/_process/doc_rules_v3/templates/db_table.template.md b/_process/doc_rules_v3/templates/db_table.template.md new file mode 100644 index 0000000..ce72352 --- /dev/null +++ b/_process/doc_rules_v3/templates/db_table.template.md @@ -0,0 +1,38 @@ +--- +doc_type: db_table +required_common_elements: + - common-elements/summary.md + - common-elements/details.md + - common-elements/db-purpose.md + - common-elements/db-columns.md + - common-elements/db-constraints.md + - common-elements/db-usage.md + - common-elements/sql-example.md +special_rules: + - Документ описывает одну физическую таблицу БД или materialized view. + - Нужно фиксировать назначение таблицы, поля, ограничения, индексы, связи и сценарии использования. + - Если точные детали БД не заданы, допустимо сделать рабочие инженерные допущения и явно записать их в документ. +--- + +# <title> + +## Summary +Правила оформления: `../common-elements/summary.md` + +## Details +Правила оформления: `../common-elements/details.md` + +### Назначение таблицы +Правила оформления: `../common-elements/db-purpose.md` + +### Структура таблицы +Правила оформления: `../common-elements/db-columns.md` + +### Ограничения и индексы +Правила оформления: `../common-elements/db-constraints.md` + +### Использование в сценариях +Правила оформления: `../common-elements/db-usage.md` + +### Пример SQL +Правила оформления: `../common-elements/sql-example.md` diff --git a/_process/doc_rules_v3/templates/logic_block.template.md b/_process/doc_rules_v3/templates/logic_block.template.md new file mode 100644 index 0000000..99b03d9 --- /dev/null +++ b/_process/doc_rules_v3/templates/logic_block.template.md @@ -0,0 +1,28 @@ +--- +doc_type: logic_block +required_common_elements: + - common-elements/summary.md + - common-elements/details.md + - common-elements/tech-use-case.md + - common-elements/fr.md + - common-elements/nfr.md +special_rules: + - Logic block описывает переиспользуемую логику без дублирования UI/API деталей. +--- + +# <title> + +## Summary +Правила оформления: `../common-elements/summary.md` + +## Details +Правила оформления: `../common-elements/details.md` + +### Технический use case +Правила оформления: `../common-elements/tech-use-case.md` + +### Функциональные требования +Правила оформления: `../common-elements/fr.md` + +### Нефункциональные требования +Правила оформления: `../common-elements/nfr.md` diff --git a/_process/doc_rules_v3/templates/ui_page.template.md b/_process/doc_rules_v3/templates/ui_page.template.md new file mode 100644 index 0000000..fb1daf5 --- /dev/null +++ b/_process/doc_rules_v3/templates/ui_page.template.md @@ -0,0 +1,33 @@ +--- +doc_type: ui_page +required_common_elements: + - common-elements/summary.md + - common-elements/details.md + - common-elements/tech-use-case.md + - common-elements/ui-requirements.md + - common-elements/fr.md + - common-elements/user-analytics.md +special_rules: + - Для списочных страниц обязательно описывать табличное представление, empty state и error state. + - UI-элементы описываются в таблицах по правилам `common-elements/ui-requirements.md`. +--- + +# <title> + +## Summary +Правила оформления: `../common-elements/summary.md` + +## Details +Правила оформления: `../common-elements/details.md` + +### Технический use case +Правила оформления: `../common-elements/tech-use-case.md` + +### Требования к UI +Правила оформления: `../common-elements/ui-requirements.md` + +### Функциональные требования +Правила оформления: `../common-elements/fr.md` + +### Нефункциональные требования +Правила оформления: `../common-elements/user-analytics.md` diff --git a/src/app/core/agent/__init__.py b/src/app/core/agent/__init__.py index 465c646..b3e1458 100644 --- a/src/app/core/agent/__init__.py +++ b/src/app/core/agent/__init__.py @@ -1,3 +1,9 @@ -from app.core.agent.runtime import AgentRuntime - __all__ = ["AgentRuntime"] + + +def __getattr__(name: str): + if name == "AgentRuntime": + from app.core.agent.runtime import AgentRuntime + + return AgentRuntime + raise AttributeError(name) diff --git a/src/app/core/agent/processes/__init__.py b/src/app/core/agent/processes/__init__.py index 510c092..d3ce2f2 100644 --- a/src/app/core/agent/processes/__init__.py +++ b/src/app/core/agent/processes/__init__.py @@ -1,10 +1,22 @@ -from app.core.agent.processes.base import AgentProcess, ProcessResult -from app.core.agent.processes.v1.process import V1Process -from app.core.agent.processes.v2.v2_process import V2Process - __all__ = [ "AgentProcess", "ProcessResult", "V1Process", "V2Process", ] + + +def __getattr__(name: str): + if name in {"AgentProcess", "ProcessResult"}: + from app.core.agent.processes.base import AgentProcess, ProcessResult + + return {"AgentProcess": AgentProcess, "ProcessResult": ProcessResult}[name] + if name == "V1Process": + from app.core.agent.processes.v1.process import V1Process + + return V1Process + if name == "V2Process": + from app.core.agent.processes.v2.v2_process import V2Process + + return V2Process + raise AttributeError(name) diff --git a/src/app/core/agent/processes/v2/__init__.py b/src/app/core/agent/processes/v2/__init__.py index b2633c3..921e4e5 100644 --- a/src/app/core/agent/processes/v2/__init__.py +++ b/src/app/core/agent/processes/v2/__init__.py @@ -1,9 +1,11 @@ -from app.core.agent.processes.v2.intent_router.router import V2IntentRouter - __all__ = ["V2IntentRouter", "V2Process"] def __getattr__(name: str): + if name == "V2IntentRouter": + from app.core.agent.processes.v2.intent_router.router import V2IntentRouter + + return V2IntentRouter if name == "V2Process": from app.core.agent.processes.v2.v2_process import V2Process diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/README.md b/src/app/core/agent/processes/v2/doc_rules_v2/README.md deleted file mode 100644 index 1a3ed4b..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Documentation Rules V2 - -Этот каталог — новая структура правил для DOC_UPDATE/FROM_FEATURE. - -## Цель - -Разделить инструкции на 2 независимых блока: - -1. `types/` — инструкции по структуре конкретных типов документов (`ui_page`, `api_method`, и т.д.). -2. `common-elements/` — инструкции по структуре общих элементов страницы (`tech-use-case`, `fr`, и др.). - -## Структура каталога - -- `documentation-rules.md` — верхнеуровневые правила. -- `global/` — общие правила оформления и frontmatter. -- `types/` — правила по типам документов (вместо `artifact-types/`). -- `common-elements/` — правила по общим секциям (вместо `sections/`). -- `templates/` — шаблоны документов. - -## Переключение профиля - -Загрузчик поддерживает переключение через env: - -- `DOC_RULES_PROFILE=v2` (по умолчанию) — использовать `doc_rules_v2`. -- `DOC_RULES_PROFILE=legacy` — использовать старый каталог `doc_rules`. - -Если `v2` не содержит валидных пар `type + template`, загрузчик автоматически fallback на `doc_rules`. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/api-contract.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/api-contract.md deleted file mode 100644 index fc313ea..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/api-contract.md +++ /dev/null @@ -1,24 +0,0 @@ -# API Contract Rules - -## Назначение - -Этот файл описывает, как оформлять подраздел `## Контракт` в API-документах. - -## Что должно быть описано - -- входные параметры -- выходные параметры -- JSON-структуры запросов и ответов -- обязательность полей -- типы полей -- ограничения -- описание назначения полей -- примеры данных -- auth -- idempotency -- timeout -- ошибки и их HTTP-коды - -## Правило качества - -Контракт должен быть достаточно формальным, чтобы по нему можно было собрать OpenAPI-спецификацию. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/details.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/details.md deleted file mode 100644 index 33ed824..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/details.md +++ /dev/null @@ -1,13 +0,0 @@ -# Details Section Rules - -## Назначение - -Этот файл задает общие правила для секции `## Details`. - -## Правила - -- `Details` оформляется как `## Details`. -- Внутри `Details` используются заголовки уровня `###` и ниже. -- Структура Details зависит от типа документа. -- В Details не нужно повторно дублировать навигацию и связи, если они уже есть во frontmatter. -- Интеграции, ошибки и кодовые привязки должны быть выделены в отдельные подразделы, если они существенны для понимания документа. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md deleted file mode 100644 index f212330..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/fr.md +++ /dev/null @@ -1,37 +0,0 @@ -# Functional requrements rules - -## Назначение - -Этот файл описывает, как оформлять функциональные требования в подраздел `### Функциональные требования` в документах. - -## Правила -- Функциональное требование (FR) расширяет и дополняет шаги, описанные в сценарии. -- Функциональное требование (FR) не должно копировать шаг сценария не неся дополнительной информации. -- Название функционального требования формируется следующим образом - "FR.<номер>. <Название>", где - - <номер> идет инкрементально внутри конкретного документа, начинается с 1. - - <Название> - кратко описывает что делает требование, суть действий (от 3 до 7 слов) - - - -## Пример целевого описания сценария - -### Примеры названия FR - - Получение данных клиента из АС ЕПК - - Проверка уровня доступа - - Сценарий построения списка связанных предложений - - - ### Примеры описания FR -FR.1. Получение данных клиента из АС ЕПК -1. Сформировать запрос к эндпоинту POST /api/v1/path/to/resourse в АС ЕПК - - Заголовки - - <тут идет описание заголовков и того как они формируются> - - Параметры запроса - - <тут идет описание параметров и того как они формируются> - - Тело запроса - - <тут идет описание структуры объекта JSON или payload в другмо формате так как это задано требованиями> - -2. Обработать ответ от АС ЕПК - Успешный ответ - <взять из описания вызываеого api критерии успешного ответа > - Ничего не найдено - <взять из описания вызываеого api критерии успешного овтета, опционально (если применимо)> - Ошибка - <взять из описания вызываеого api критерии успешного ответа > \ No newline at end of file diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/requirements-format.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/requirements-format.md deleted file mode 100644 index 14eb0c3..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/requirements-format.md +++ /dev/null @@ -1,16 +0,0 @@ -# Requirements Format Rules - -## Назначение - -Этот файл задает формат для функциональных и нефункциональных требований. - -## Функциональные требования - -- Использовать коды `FR-1`, `FR-2`, `FR-3` и так далее. -- Каждое требование должно описывать отдельный обязательный аспект поведения. -- Идентификаторы локальны в пределах одного документа. - -## Нефункциональные требования - -- Использовать коды `NFR-1`, `NFR-2`, `NFR-3` и так далее. -- Требования должны описывать характеристики качества, ограничения и эксплуатационные свойства. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/summary.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/summary.md deleted file mode 100644 index 7f7fa7b..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/summary.md +++ /dev/null @@ -1,13 +0,0 @@ -# Summary Section Rules - -## Назначение - -Этот файл задает правила для секции `## Summary`. - -## Правила - -- Summary должен быть коротким explain-слоем быстрого контекста. -- Summary должен объяснять суть документа без лишних деталей. -- Summary должен быть пригоден для explain и быстрого чтения. -- Предпочтительный формат: список ключевых фактов `Purpose`, `Actor`, `Trigger`, `Errors`, `Related ...` и т.д. -- Для крупных документов допустим более длинный summary, если он остается структурированным. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md b/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md deleted file mode 100644 index 572d52f..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/common-elements/tech-use-case.md +++ /dev/null @@ -1,66 +0,0 @@ -# Scenario Rules - -## Назначение - -Этот файл описывает, как оформлять технический USE CASE в подраздел `### Сценарий` в документах. - -## Обязательные части - -- название -- предусловия -- триггер -- основной сценарий -- альтернативный сценарий -- обработка ошибок -- постусловие - -## Правила -- Основной и альтернативные сценарии состоят из шагов. - -- Каждый шаг описывается одним предложением не более 15-20 слов, и состоит из двух частей. Первая часть описывает что мы делаем по смыслу, чтобы это было понятно человеку без низкоуровневых технических деталей. Например: авторизует запрос, получает данные клиента, запрашивает справочники. Вторая часть описывает как это реализовано технически - вызывает эндпоинт /path/to/resource в системе <название системы>. - -- В описании шага не должно быть длинных технических деталей. Если техничсекую реализацию нельхзя описатьодним предложенеим (в лимите длины описания шага), то необхлодимо это вынести в отдельное функциональное требование FR.<номер>. <Название> и описать в нем технические детали. А в шаге сослаться на это требование через "Описание приведено в FR.<номер>. <Название>" - -- Для шагов авторизации обязателен доп шаг с описанием обработки ошибки. -- Для шагов с интеграцией обязателен доп шаг с описанием обработки ошибки. -- Для шагов с проверкой условий обязательны доп шаги с описанием переходов по сценарию. - -- Название "FR.<номер>. <Название>" формируется следующим образом: - - <номер> идет инкрементально внутри конкретного документа, начинается с 1. - - <Название> - кратко описывает что делает требование, суть действий. - -- Для каждого шага при необходимости нужно прописать логику действий в случае ошибки или если логика шага определяет несколько сценариев разивития при выполнении заданных условий. - -- Для шагов, которые описывают интеграцию с другой системой необходимо указать название точки интеграции (название эндпоинта, название топика и так далее) и сделать ссылку на FR.<номер>. <Название> с описанием шагов интеграции - как сформировать запрос/сообщение, как обработать ответ, политику ретраев. - - - Сценарий собирается из тезисов, приведенных системной аналимтике в свободной формулировке - - - Функциональные требования "FR.<номер>. <Название>" не должны дублировать шагов сценария в use case. Они содержат детали, которые вынесены из юзкейса чтобы не делать его тяжелым. Если шаг юзкейса описывается одним предложением в лимите длины, то FR делать не нужно. - - - FR обязательно описывается для шага с интеграцией - - FR Не описывается для шага авторизации. - - - - - - -## Пример целевого описания сценария - -### Примеры шагов сценария - -Пример 1 -- Авторизует запрос пользователя по наличию у него экшена ролевой модели CI02792632.ContactsDGR.Detail - - В случае ошибки - завершить сценарий с кодом UNAUTHORIZED - -Пример 2 -- Запрашивает данные клиента - вызывает /api/v1/clients/{client-id}/info - - В случае ошибки - завершить сценарий с кодом CLIENT_INFO_REQUEST_FAIL - -Пример 3 -- Возвращает ответ в формате <название DTO> - -### Примеры названия FR - - Получение данных клиента из АС ЕПК - - Проверка уровня доступа - - Сценарий построения списка связанных предложений \ No newline at end of file diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/documentation-rules.md b/src/app/core/agent/processes/v2/doc_rules_v2/documentation-rules.md deleted file mode 100644 index 1be0884..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/documentation-rules.md +++ /dev/null @@ -1,71 +0,0 @@ -# Documentation Rules - -Этот каталог оформляет MVP документации проекта в атомарном формате. - -## Базовая структура - -- Каждый документ содержит YAML frontmatter. -- В документе должен быть один `H1`, совпадающий с `title`. -- Основные разделы оформляются как `## Summary` и `## Details`. -- Внутри `Details` используются заголовки уровня `###` и ниже. -- Связи, сущности и навигация описываются во frontmatter через `related_docs`, `links`, `entities`, `parent`, `children`. - -## Summary - -- Краткий explain-слой быстрого контекста. -- Должен позволять быстро понять назначение документа без чтения `Details`. -- Предпочтительный формат: компактный список ключевых фактов без длинных абзацев. - -## Details - -- Раскрывает полное описание объекта. -- Структура `Details` зависит от типа документа. -- Сценарии, ограничения, интеграции, ошибки и кодовые привязки должны быть разнесены по отдельным подразделам. - -## API documents - -Для `api_method` внутри `## Details` обязательны разделы: -- `### Описание` -- `### Сценарий` -- `### Функциональные требования` -- `### Нефункциональные требования` -- `### Контракт` - -Если у метода есть интеграции и ошибки, также обязательны: -- `### Интеграции` -- `### Ошибки` -- `### Связанный код` -- `### История изменений` - -### Сценарий - -Сценарий оформляется как технический use case и содержит: -- название -- предусловия -- триггер -- основной сценарий -- альтернативный сценарий -- обработку ошибок -- постусловие - -### Требования - -- Функциональные требования маркируются как `FR-1`, `FR-2`, ... -- Нефункциональные требования маркируются как `NFR-1`, `NFR-2`, ... -- Идентификаторы требований локальны в рамках одного документа. - -### Контракт - -Контракт должен быть пригоден для последующей сборки OpenAPI-спецификации и включать: -- входные параметры -- выходные параметры -- структуру JSON-сообщений -- обязательность полей -- типы и ограничения -- описание полей -- правила заполнения -- примеры данных -- auth -- idempotency -- timeout -- ошибки и их HTTP-коды diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/global/documentation-system.md b/src/app/core/agent/processes/v2/doc_rules_v2/global/documentation-system.md deleted file mode 100644 index 656b569..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/global/documentation-system.md +++ /dev/null @@ -1,38 +0,0 @@ -# Documentation System - -## Назначение - -Этот файл задает общую модель документации проекта. - -## Базовая модель - -Каждый документ должен состоять из двух слоев: -- YAML frontmatter -- контент - -Контент всегда состоит из двух обязательных разделов: -- `## Summary` -- `## Details` - -Над ними должен быть один заголовок `# <title>`, совпадающий со значением `title` во frontmatter. - -## Принципы - -- Документы должны быть атомарными. -- Один документ описывает одну тему. -- Вместо дублирования между документами используются явные ссылки. -- Связи и навигация должны быть формализованы. -- Документы должны быть пригодны для чтения человеком и для RAG. -- Документы должны быть пригодны для частичного обновления без деградации структуры. - -## Типы документов - -На уровне проекта поддерживаются типы: -- `api_method` -- `logic_block` -- `architecture_overview` -- `domain_entity` -- `ui_page` -- `integration_doc` -- `index_page` -- `glossary_item` diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/global/frontmatter.md b/src/app/core/agent/processes/v2/doc_rules_v2/global/frontmatter.md deleted file mode 100644 index 2b4dee5..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/global/frontmatter.md +++ /dev/null @@ -1,67 +0,0 @@ -# Frontmatter Rules - -## Назначение - -Этот файл описывает единый контракт YAML frontmatter для всех документов. - -## Обязательные поля - -```yaml -id: string -title: string -doc_type: string -domain: string -sub_domain: string -related_docs: [] -status: string -``` - -## Поля совместимости и рекомендуемые поля - -```yaml -type: string -name: string -module: string -layer: string -updated_at: YYYY-MM-DD -tags: [] -entities: [] -parent: string | null -children: [] -links: {} -source_of_truth: string -related_code: [] -system_analytics_refs: [] -``` - -## Правила - -- `id` должен быть стабильным и уникальным в пределах документации проекта. -- `title` — человекочитаемый заголовок. -- `doc_type` — канонический тип документа. -- `domain` и `sub_domain` определяют бизнес-контекст документа. -- `related_docs` хранит явные связи с другими markdown-документами. -- `status` хранит жизненный цикл документа: например `draft`, `approved`, `active`. -- `type` допустимо дублировать как alias для tooling-совместимости с индексаторами. -- `name` — короткое системное имя документа. -- `module` — модуль или подсистема. -- `layer` — слой системы. -- `updated_at` хранится в формате `YYYY-MM-DD`. - -## Связи и навигация - -- `entities` описывает сущности, связанные с документом. -- `parent` и `children` описывают иерархию. -- `links` описывает typed graph связей между документами, кодом и интеграциями. - -## Формат links - -```yaml -links: - called_by: - - ext.health_probe - uses_logic: - - logic.some_flow - integrates_with: - - ext.some_system -``` diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/global/linking.md b/src/app/core/agent/processes/v2/doc_rules_v2/global/linking.md deleted file mode 100644 index 6e49171..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/global/linking.md +++ /dev/null @@ -1,33 +0,0 @@ -# Linking Rules - -## Назначение - -Этот файл описывает, как связывать документы между собой. - -## Иерархия - -- `parent` используется для родительского документа. -- `children` используется для прямых дочерних документов. -- Иерархия должна быть осмысленной и стабильной. -- Для общей точки входа допустим `index_page`. - -## Графовые связи - -Для `related_docs` используются ссылки на соседние документы. - -Для `links` рекомендуется использовать typed-ключи: -- `called_by` -- `uses_logic` -- `reads_db` -- `writes_db` -- `integrates_with` -- `used_by` -- `exposes_api` -- `uses_entities` - -## Правила использования - -- Если документ логически входит в другой, использовать `parent`/`children`. -- Если связь нужна для навигации между равноправными документами, дублировать ее в `related_docs`. -- Если связь отражает поведение, интеграции или переиспользование, фиксировать ее в `links`. -- Детальное описание интеграций хранить в body документа, а не только во frontmatter. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/global/naming.md b/src/app/core/agent/processes/v2/doc_rules_v2/global/naming.md deleted file mode 100644 index c722416..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/global/naming.md +++ /dev/null @@ -1,24 +0,0 @@ -# Naming Rules - -## Назначение - -Этот файл описывает правила именования документов, файлов и идентификаторов. - -## Правила для файлов - -- Имена файлов должны быть в kebab-case. -- Имя файла должно отражать одну тему. -- Для шаблонов использовать суффикс `.template.md`. - -## Правила для id - -- `id` строится в формате `<type-group>.<name>`. -- Примеры: - - `api.send_message_endpoint` - - `logic.telegram_notification_loop` - - `architecture.telegram_notify_app` - -## Правила для title - -- `title` должен быть кратким и человекочитаемым. -- В `title` допускаются пробелы и естественный язык. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/global/writing-style.md b/src/app/core/agent/processes/v2/doc_rules_v2/global/writing-style.md deleted file mode 100644 index 6c1caec..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/global/writing-style.md +++ /dev/null @@ -1,19 +0,0 @@ -# Writing Style - -## Назначение - -Этот файл задает правила стиля для текстового наполнения документации. - -## Правила стиля - -- Текст должен быть лаконичным. -- Формулировки должны быть точными и техническими. -- Summary должен быть кратким explain-слоем. -- Details должен раскрывать суть без лишней воды. -- Нежелательно смешивать несколько тем в одном документе. -- Если детали относятся к другому артефакту, их нужно выносить в отдельный документ. - -## Язык - -- Основной язык документации — русский. -- Технические термины, названия классов, API, RAG, OpenAPI, runtime и другие устоявшиеся identifiers можно оставлять на английском. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/templates/api_method.template.md b/src/app/core/agent/processes/v2/doc_rules_v2/templates/api_method.template.md deleted file mode 100644 index ba2ac07..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/templates/api_method.template.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -id: api.example_method -type: api_method -doc_type: api_method -name: example_method -title: HTTP API /example -module: example_module -layer: application -domain: example_domain -sub_domain: example_subdomain -related_docs: [] -status: draft -updated_at: 2026-03-20 -source_of_truth: code -parent: null -children: [] -tags: [] -entities: [] -links: {} ---- - -# HTTP API /example - -## Summary - -Краткое описание метода. - -## Details - -## Описание - -Короткое описание сути метода. - -## Сценарий - -**Название:** - -**Предусловия:** -- - -**Триггер:** -- - -**Основной сценарий:** -1. - -**Альтернативный сценарий:** -1. - -**Обработка ошибок:** -1. - -**Постусловие:** -- - -## Функциональные требования - -**FR-1.** - -## Нефункциональные требования - -**NFR-1.** - -## Контракт - -### Входные параметры - -| Параметр | Где передается | Тип | Обязательность | Ограничения | Описание | Пример | -|---|---|---|---|---|---|---| -| | | | | | | | - -### Выходные параметры - -| Поле | Тип | Обязательность | Ограничения | Описание | Заполнение | Пример | -|---|---|---|---|---|---|---| -| | | | | | | | - -### Интеграции - -### Ошибки - -### Связанный код - -### История изменений diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/templates/architecture_overview.template.md b/src/app/core/agent/processes/v2/doc_rules_v2/templates/architecture_overview.template.md deleted file mode 100644 index 21c7319..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/templates/architecture_overview.template.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: architecture.example_system -type: architecture_overview -doc_type: architecture_overview -name: example_system -title: Обзор архитектуры Example System -module: example_module -layer: system -domain: example_domain -sub_domain: example_subdomain -related_docs: [] -status: draft -updated_at: 2026-03-20 -source_of_truth: mixed -parent: null -children: [] -tags: [] -entities: [] -links: {} ---- - -# Обзор архитектуры Example System - -## Summary - -Краткое описание архитектуры. - -## Details - -### Описание - -### Контекст - -### Границы системы - -### Компоненты - -### Интеграционные сценарии - -### Интеграции - -### Ограничения - -### Связанный код - -### Связанные документы - -### История изменений diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/templates/domain_entity.template.md b/src/app/core/agent/processes/v2/doc_rules_v2/templates/domain_entity.template.md deleted file mode 100644 index f8fd65e..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/templates/domain_entity.template.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -id: domain.example_entity -type: domain_entity -doc_type: domain_entity -name: example_entity -title: Пример доменной сущности -module: example_module -layer: domain -domain: example_domain -sub_domain: example_subdomain -related_docs: [] -status: draft -updated_at: 2026-03-20 -source_of_truth: code -parent: null -children: [] -tags: [] -entities: [] -links: {} ---- - -# Пример доменной сущности - -## Summary - -Краткое описание сущности. - -## Details - -### Описание - -### Модель данных - -### Состояния и инварианты - -### Технический use case - -### Функциональные требования - -### Нефункциональные требования - -### Интеграции - -### Связанный код - -### Связанные документы - -### История изменений diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/templates/logic_block.template.md b/src/app/core/agent/processes/v2/doc_rules_v2/templates/logic_block.template.md deleted file mode 100644 index 36e1d5d..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/templates/logic_block.template.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: logic.example_block -type: logic_block -doc_type: logic_block -name: example_block -title: Пример блока логики -module: example_module -layer: application -domain: example_domain -sub_domain: example_subdomain -related_docs: [] -status: draft -updated_at: 2026-03-20 -source_of_truth: code -parent: null -children: [] -tags: [] -entities: [] -links: {} ---- - -# Пример блока логики - -## Summary - -Краткое описание блока логики. - -## Details - -### Описание - -### Контекст - -### Технический use case - -### Функциональные требования - -### Нефункциональные требования - -### Интеграции - -### Ограничения и условия вызова - -### Ошибки и деградации - -### Связанные API - -### Связанный код - -### История изменений diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/templates/ui_page.template.md b/src/app/core/agent/processes/v2/doc_rules_v2/templates/ui_page.template.md deleted file mode 100644 index 5bd32fe..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/templates/ui_page.template.md +++ /dev/null @@ -1,50 +0,0 @@ ---- -id: ui.example_page -type: ui_page -doc_type: ui_page -name: example_page -title: Пример UI-страницы -module: example_module -layer: presentation -domain: example_domain -sub_domain: example_subdomain -related_docs: [] -status: draft -updated_at: 2026-03-20 -source_of_truth: mixed -parent: null -children: [] -tags: [] -entities: [] -links: {} ---- - -# Пример UI-страницы - -## Summary - -Краткое описание страницы и её назначения. - -## Details - -### Назначение страницы - -### Пользовательский сценарий - -### Основные блоки интерфейса - -### Связанные API и сущности - -### Функциональные требования - -### Нефункциональные требования - -### Ограничения и граничные случаи - -### Ошибки и валидации - -### Связанный код - -### Связанные документы - -### История изменений diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/api_method.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/api_method.md deleted file mode 100644 index bfc6428..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/api_method.md +++ /dev/null @@ -1,39 +0,0 @@ -# API Method Rules - -## Назначение - -Этот файл задает правила для документов типа `api_method`. - -## Когда использовать - -Использовать для описания одного HTTP endpoint или одного отдельного API метода. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -Внутри `## Details` обязательны: -- `### Описание` -- `### Сценарий` -- `### Функциональные требования` -- `### Нефункциональные требования` -- `### Контракт` - -## Особые правила - -- Сценарий оформляется как технический use case. -- Функциональные требования маркируются `FR-*`. -- Нефункциональные требования маркируются `NFR-*`. -- Контракт должен быть пригоден для последующей сборки OpenAPI. -- Если у метода есть интеграции, они выносятся в `### Интеграции`. -- Ошибки и HTTP-коды либо описываются в `### Ошибки`, либо ссылаются на централизованный каталог ошибок. - -## Ошибки оформления - -- Нельзя заменять контракт общим текстовым описанием. -- Нельзя смешивать несколько endpoint в одном документе. -- Нельзя хранить связи и навигацию вне frontmatter. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/architecture_overview.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/architecture_overview.md deleted file mode 100644 index e4b146e..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/architecture_overview.md +++ /dev/null @@ -1,31 +0,0 @@ -# Architecture Overview Rules - -## Назначение - -Этот файл задает правила для документов типа `architecture_overview`. - -## Когда использовать - -Использовать как входной документ для понимания системы, модуля или сервиса. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -## Что описывать в Details - -- границы системы -- основные компоненты -- ключевые взаимодействия -- интеграционные сценарии -- главные ограничения -- ссылки на дочерние документы по API, logic, domain и другим артефактам - -## Ошибки оформления - -- Нельзя дублировать в архитектурном обзоре полные API-контракты. -- Нельзя делать архитектурный обзор единственным документом на всю систему без декомпозиции. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/domain_entity.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/domain_entity.md deleted file mode 100644 index c533266..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/domain_entity.md +++ /dev/null @@ -1,30 +0,0 @@ -# Domain Entity Rules - -## Назначение - -Этот файл задает правила для документов типа `domain_entity`. - -## Когда использовать - -Использовать для описания одной доменной сущности, ее смысла, состояния и роли в системе. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -## Что описывать в Details - -- смысл сущности -- ключевые атрибуты -- состояния или инварианты -- использование сущности в системе -- интеграции с API, workflow или внешними потребителями, если они важны для понимания модели - -## Ошибки оформления - -- Нельзя смешивать несколько независимых сущностей в одном документе. -- Нельзя подменять доменную сущность описанием endpoint или workflow. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/integration_doc.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/integration_doc.md deleted file mode 100644 index 7f1fe35..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/integration_doc.md +++ /dev/null @@ -1,25 +0,0 @@ -# Integration Doc Rules - -## Назначение - -Этот файл задает правила для документов типа `integration_doc`. - -## Когда использовать - -Использовать для описания интеграции между системами, сервисами или внешними провайдерами. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -## Что описывать в Details - -- цель интеграции -- участвующие стороны -- направление обмена -- ключевой сценарий взаимодействия -- ограничения и риски diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/logic_block.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/logic_block.md deleted file mode 100644 index 788e4f7..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/logic_block.md +++ /dev/null @@ -1,31 +0,0 @@ -# Logic Block Rules - -## Назначение - -Этот файл задает правила для документов типа `logic_block`. - -## Когда использовать - -Использовать для описания одного законченного блока логики, workflow или процесса. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -## Что описывать в Details - -- назначение логического блока -- входы и выходы -- последовательность выполнения -- интеграции -- ключевые ограничения -- состояние и ошибки, если они важны для понимания блока - -## Ошибки оформления - -- Нельзя описывать весь модуль целиком, если логика распадается на несколько независимых блоков. -- Нельзя превращать документ в пересказ исходного кода построчно. diff --git a/src/app/core/agent/processes/v2/doc_rules_v2/types/ui_page.md b/src/app/core/agent/processes/v2/doc_rules_v2/types/ui_page.md deleted file mode 100644 index 9640671..0000000 --- a/src/app/core/agent/processes/v2/doc_rules_v2/types/ui_page.md +++ /dev/null @@ -1,24 +0,0 @@ -# UI Page Rules - -## Назначение - -Этот файл задает правила для документов типа `ui_page`. - -## Когда использовать - -Использовать для описания одной пользовательской страницы, экрана или отдельного UI-сценария. - -## Обязательная структура - -Документ должен содержать: -- YAML frontmatter -- `# <title>` -- `## Summary` -- `## Details` - -## Что описывать в Details - -- назначение страницы -- пользовательский сценарий -- основные блоки интерфейса -- связанные API и сущности diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/README.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/README.md new file mode 100644 index 0000000..4c291e7 --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/README.md @@ -0,0 +1,9 @@ +# DOC_UPDATE/FROM_FEATURE v2 Rules + +Этот каталог содержит общие rules для всех шагов и подпроцессов workflow `doc_update_from_feature_v2`. + +- `attribute_resolution.md` — правила определения type/id/application/platform. +- `path_resolution.md` — правила резолва путей документации. +- `section_frontmatter.md` — инструкции для frontmatter. +- `section_summary.md` — инструкции для summary. +- `section_details.md` — инструкции для details. diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/attribute_resolution.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/attribute_resolution.md new file mode 100644 index 0000000..e9fe656 --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/attribute_resolution.md @@ -0,0 +1,5 @@ +# Attribute Resolution + +1. Приоритет: теги из requirement > metadata документа > LLM fallback. +2. Обязательные атрибуты: `type`, `id`, `application`, `platform`. +3. Если атрибут отсутствует, разрешен fallback через LLM. diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/path_resolution.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/path_resolution.md new file mode 100644 index 0000000..827021c --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/path_resolution.md @@ -0,0 +1,7 @@ +# Path Resolution + +Путь строится как: + +`docs/<application>/<platform>/<type>/<id>.md` + +Нормализация сегментов: lowercase + замена недопустимых символов на `-`. diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_details.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_details.md new file mode 100644 index 0000000..4a87491 --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_details.md @@ -0,0 +1,3 @@ +# Details Rules + +Details содержит детализированное описание поведения, ограничений и сценариев. diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_frontmatter.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_frontmatter.md new file mode 100644 index 0000000..a2d1a35 --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_frontmatter.md @@ -0,0 +1,4 @@ +# Frontmatter Rules + +1. Frontmatter всегда в блоке `---`. +2. Должны быть поля id/title/type/application/platform. diff --git a/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_summary.md b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_summary.md new file mode 100644 index 0000000..7c5e57d --- /dev/null +++ b/src/app/core/agent/processes/v2/rules_doc_update_from_feature_v2/section_summary.md @@ -0,0 +1,3 @@ +# Summary Rules + +Summary содержит краткую цель страницы и основные изменения. diff --git a/src/app/core/agent/processes/v2/v2_process.py b/src/app/core/agent/processes/v2/v2_process.py index ddf6ef3..3b2f53f 100644 --- a/src/app/core/agent/processes/v2/v2_process.py +++ b/src/app/core/agent/processes/v2/v2_process.py @@ -18,6 +18,10 @@ from app.core.agent.processes.v2.workflows.doc_update_from_feature.graph import from app.core.agent.processes.v2.workflows.doc_update_from_feature.workflow_runtime.context import ( DocUpdateFromFeatureContext, ) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.graph import DocUpdateFromFeatureV2Graph +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) from app.core.agent.processes.v2.workflows.general_qa_summary.workflow_runtime.context import GeneralQaSummaryContext from app.core.agent.processes.v2.workflows.general_qa_summary.graph import GeneralQaSummaryGraph from app.core.agent.utils.llm import AgentLlmService @@ -44,6 +48,7 @@ class V2Process(AgentProcess): general_summary_prompt_name: str = "v2_general.summary_answer", workflow_llm_enabled: bool = True, doc_rules_enabled: bool = True, + doc_update_workflow_version: str = "v2", ) -> None: self._router = router or V2IntentRouter() gate = evidence_gate or DocsEvidenceGate() @@ -51,6 +56,8 @@ class V2Process(AgentProcess): self._general_summary_prompt_name = general_summary_prompt_name self._workflow_llm_enabled = workflow_llm_enabled self._doc_rules_enabled = doc_rules_enabled + self._doc_update_workflow_version = doc_update_workflow_version.strip().lower() or "v2" + doc_update_graph = self._build_doc_update_workflow(llm, doc_rules_enabled) self._workflows: dict[tuple[str, str, str], Any] = { (V2Domain.DOCS, V2Intent.DOC_EXPLAIN, V2Subintent.SUMMARY): DocExplainSummaryGraph( llm, @@ -69,10 +76,7 @@ class V2Process(AgentProcess): policy_resolver=policy_resolver, rag_adapter=rag_adapter, ), - (V2Domain.DOCS, V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE): DocUpdateFromFeatureGraph( - llm=llm, - doc_rules_enabled=doc_rules_enabled, - ), + (V2Domain.DOCS, V2Intent.DOC_UPDATE, V2Subintent.FROM_FEATURE): doc_update_graph, (V2Domain.GENERAL, V2Intent.GENERAL_QA, V2Subintent.SUMMARY): GeneralQaSummaryGraph( llm, policy_resolver=policy_resolver, @@ -175,12 +179,20 @@ class V2Process(AgentProcess): ) ) if route.intent == V2Intent.DOC_UPDATE and route.subintent == V2Subintent.FROM_FEATURE: + if self._doc_update_workflow_version == "legacy": + return await workflow.run( + DocUpdateFromFeatureContext( + runtime=runtime_context, + route=route, + rag_session_id=rag_session_id, + doc_rules_enabled=self._doc_rules_enabled, + ) + ) return await workflow.run( - DocUpdateFromFeatureContext( + DocUpdateFromFeatureV2Context( runtime=runtime_context, route=route, rag_session_id=rag_session_id, - doc_rules_enabled=self._doc_rules_enabled, ) ) return await workflow.run( @@ -192,3 +204,8 @@ class V2Process(AgentProcess): workflow_llm_enabled=self._workflow_llm_enabled, ) ) + + def _build_doc_update_workflow(self, llm: AgentLlmService, doc_rules_enabled: bool): + if self._doc_update_workflow_version == "legacy": + return DocUpdateFromFeatureGraph(llm=llm, doc_rules_enabled=doc_rules_enabled) + return DocUpdateFromFeatureV2Graph(llm=llm) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/__init__.py new file mode 100644 index 0000000..eddbc35 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/__init__.py @@ -0,0 +1,11 @@ +"""DOC_UPDATE/FROM_FEATURE v2 workflow package.""" + +__all__ = ["DocUpdateFromFeatureV2Graph"] + + +def __getattr__(name: str): + if name == "DocUpdateFromFeatureV2Graph": + from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.graph import DocUpdateFromFeatureV2Graph + + return DocUpdateFromFeatureV2Graph + raise AttributeError(name) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/graph.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/graph.py new file mode 100644 index 0000000..8b47895 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/graph.py @@ -0,0 +1,43 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step1_resolve_source.step import ( + ResolveSourceStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step2_load_source_content.step import ( + LoadSourceContentStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step3_parse_requirements.step import ( + ParseRequirementsStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step4_prepare_tasks.step import ( + PrepareRequirementTasksStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step4_load_rules.step import LoadRulesStep +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.step import ( + ExecuteRequirementSubprocessesStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step6_finalize.step import FinalizeAnswerStep +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.buffered_graph import ( + DocUpdateFromFeatureV2WorkflowGraph, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.llm import AgentLlmService + + +class DocUpdateFromFeatureV2Graph(DocUpdateFromFeatureV2WorkflowGraph[DocUpdateFromFeatureV2Context]): + def __init__(self, llm: AgentLlmService) -> None: + super().__init__( + workflow_id="v2.docs_update.from_feature_v2", + source="workflow.v2.docs_update.from_feature_v2", + steps=[ + ResolveSourceStep(), + LoadSourceContentStep(), + ParseRequirementsStep(), + PrepareRequirementTasksStep(llm=llm), + LoadRulesStep(), + ExecuteRequirementSubprocessesStep(llm=llm), + FinalizeAnswerStep(), + ], + ) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/step.py new file mode 100644 index 0000000..268b985 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step1_resolve_source/step.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import re +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.workflow import WorkflowStep + + +class ResolveSourceStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step1_resolve_source" + title = "Определение источника аналитики" + + _PATH_PATTERN = re.compile(r"(/[^\n`]+?\.md)") + _URL_PATTERN = re.compile(r"https?://[^\s)]+") + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer: + return context + query = context.route.user_query + path = self._extract_path(query) + if path: + context.source_ref = path + context.source_kind = "markdown_file" + return context + url = self._extract_url(query) + if url: + context.source_ref = url + context.source_kind = "confluence_url" if "confluence" in url.lower() else "url" + return context + context.issues.append("Не удалось определить источник системной аналитики (ожидался путь .md или URL).") + return context + + def _extract_path(self, query: str) -> str: + if "`" in query: + for chunk in query.split("`"): + value = chunk.strip().strip('"').strip("'") + if value.endswith(".md") and value.startswith("/"): + return value + match = self._PATH_PATTERN.search(query) + return match.group(1).strip().strip('"').strip("'") if match else "" + + def _extract_url(self, query: str) -> str: + match = self._URL_PATTERN.search(query) + return match.group(0).strip() if match else "" + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + query = str(context.route.user_query or "") + return {"query_excerpt": query[:300], "query_len": len(query)} + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return {"source_kind": context.source_kind, "source_ref": context.source_ref} diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/step.py new file mode 100644 index 0000000..6701c2e --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step2_load_source_content/step.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.workflow import WorkflowStep + + +class LoadSourceContentStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step2_load_source_content" + title = "Загрузка системной аналитики" + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer: + return context + if not context.source_ref: + return context + if context.source_kind == "confluence_url": + context.issues.append("Confluence source пока не реализован (stub).") + return context + if context.source_kind != "markdown_file": + context.issues.append("Поддерживается только markdown-файл (.md).") + return context + + source_path = Path(context.source_ref) + if not source_path.exists() or not source_path.is_file(): + context.issues.append(f"Файл системной аналитики не найден: {context.source_ref}") + return context + try: + context.source_content = source_path.read_text(encoding="utf-8") + context.project_root = self._resolve_project_root(source_path).as_posix() + except Exception as exc: + context.issues.append(f"Не удалось прочитать системную аналитику: {exc}") + return context + + def _resolve_project_root(self, source_path: Path) -> Path: + parts = list(source_path.parts) + if "_incoming" in parts: + idx = parts.index("_incoming") + if idx > 0: + return Path(*parts[:idx]) + return source_path.parent + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return {"source_kind": context.source_kind, "source_ref": context.source_ref} + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "project_root": context.project_root, + "source_content_len": len(context.source_content or ""), + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/parser.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/parser.py new file mode 100644 index 0000000..902bfe0 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/parser.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +import re + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import ( + AnalyticsMeta, + RequirementUnit, +) + + +class MarkdownHeadingScanner: + _HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$") + _ROOT_RE = re.compile(r"^(?:6(?:[.)\s]|$)|функциональные требования\b|описание изменений\b)", re.IGNORECASE) + _SECTION_RE = re.compile(r"^6(?:\.\d+)+(?:\s|$|[.)])") + + def find_root(self, lines: list[str]) -> tuple[int, int, int] | None: + headings = list(self.iter_headings(lines)) + for idx, (line_index, level, title) in enumerate(headings): + if not self._ROOT_RE.match(title.strip()): + continue + end = len(lines) + for next_line, next_level, _ in headings[idx + 1 :]: + if next_level <= level: + end = next_line + break + return line_index, level, end + return None + + def split_sections(self, lines: list[str], *, section_level: int) -> tuple[list[str], list[tuple[str, list[str]]]]: + meta_lines: list[str] = [] + sections: list[tuple[str, list[str]]] = [] + current_title = "" + current_body: list[str] = [] + seen_section = False + + for raw in lines: + heading = self._match_heading(raw) + if heading and heading[0] == section_level and self._SECTION_RE.match(heading[1].strip()): + if current_title: + sections.append((current_title, current_body)) + current_title = heading[1].strip() + current_body = [] + seen_section = True + continue + if current_title: + current_body.append(raw) + elif not seen_section: + meta_lines.append(raw) + + if current_title: + sections.append((current_title, current_body)) + return meta_lines, sections + + def iter_headings(self, lines: list[str]) -> list[tuple[int, int, str]]: + items: list[tuple[int, int, str]] = [] + for index, raw in enumerate(lines): + heading = self._match_heading(raw) + if heading: + items.append((index, heading[0], heading[1])) + return items + + def _match_heading(self, raw: str) -> tuple[int, str] | None: + match = self._HEADING_RE.match(raw.strip()) + if not match: + return None + return len(match.group(1)), match.group(2).strip() + + +class MarkdownMetadataParser: + _KEY_VALUE_RE = re.compile(r"^[-*]?\s*`?([A-Za-z_][A-Za-z0-9_-]*)`?\s*:\s*(.*)$") + + def parse_block(self, lines: list[str]) -> dict[str, str]: + values: dict[str, str] = {} + for raw in lines: + line = raw.strip() + if not line: + continue + match = self._KEY_VALUE_RE.match(line) + if not match: + break + key = match.group(1).strip().lower() + value = self._normalize_value(match.group(2)) + values[key] = value + return values + + def split_metadata_and_body(self, lines: list[str]) -> tuple[dict[str, str], list[str]]: + metadata: dict[str, str] = {} + body_index = 0 + for idx, raw in enumerate(lines): + line = raw.strip() + if not line: + body_index = idx + 1 + continue + match = self._KEY_VALUE_RE.match(line) + if not match: + break + key = match.group(1).strip().lower() + value = self._normalize_value(match.group(2)) + metadata[key] = value + body_index = idx + 1 + return metadata, lines[body_index:] + + def _normalize_value(self, value: str) -> str: + return value.strip().strip("`").strip('"').strip("'").strip() + + +class FunctionalRequirementsParser: + _SECTION_KEY_RE = re.compile(r"^(6(?:\.\d+)+)[.)\s-]*(.*)$") + + def __init__( + self, + scanner: MarkdownHeadingScanner | None = None, + metadata_parser: MarkdownMetadataParser | None = None, + ) -> None: + self._scanner = scanner or MarkdownHeadingScanner() + self._metadata_parser = metadata_parser or MarkdownMetadataParser() + + def parse(self, content: str) -> tuple[AnalyticsMeta, list[RequirementUnit]]: + lines = content.splitlines() + meta = self._parse_preamble_meta(lines) + root = self._scanner.find_root(lines) + if root is None: + return meta, [] + + start, level, end = root + root_meta_lines, sections = self._scanner.split_sections(lines[start + 1 : end], section_level=level + 1) + root_meta = self._metadata_parser.parse_block(root_meta_lines) + meta.domain = root_meta.get("domain", meta.domain) + meta.subdomain = root_meta.get("sub_domain", root_meta.get("subdomain", meta.subdomain)) + meta.application = root_meta.get("application", meta.application) + meta.platform = root_meta.get("platform", meta.platform) + + units: list[RequirementUnit] = [] + for raw_heading, body_lines in sections: + metadata, body = self._metadata_parser.split_metadata_and_body(body_lines) + section_key, heading = self._split_heading(raw_heading) + units.append( + RequirementUnit( + section_key=section_key, + heading=heading, + body="\n".join(body).strip(), + metadata=metadata, + ) + ) + return meta, units + + def _parse_preamble_meta(self, lines: list[str]) -> AnalyticsMeta: + preamble: list[str] = [] + for raw in lines: + if raw.strip().startswith("#"): + break + preamble.append(raw) + values = self._metadata_parser.parse_block(preamble) + return AnalyticsMeta( + analysis_id=values.get("analysis_id", ""), + application=values.get("application", ""), + platform=values.get("platform", ""), + domain=values.get("domain", ""), + subdomain=values.get("sub_domain", values.get("subdomain", "")), + ) + + def _split_heading(self, raw_heading: str) -> tuple[str, str]: + match = self._SECTION_KEY_RE.match(raw_heading.strip()) + if not match: + return "", raw_heading.strip() + section_key = match.group(1).strip() + heading = match.group(2).strip() or section_key + return section_key, heading diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/step.py new file mode 100644 index 0000000..cdc72b5 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step3_parse_requirements/step.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step3_parse_requirements.parser import ( + FunctionalRequirementsParser, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.workflow import WorkflowStep + + +class ParseRequirementsStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step3_parse_requirements" + title = "Парсинг раздела функциональных требований" + + def __init__(self, parser: FunctionalRequirementsParser | None = None) -> None: + self._parser = parser or FunctionalRequirementsParser() + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer or not context.source_content: + return context + meta, units = self._parser.parse(context.source_content) + context.analytics_meta = meta + context.requirements = units + + if not units: + context.issues.append( + "Не найден раздел 6 с подразделами 6.x для генерации страниц документации." + ) + if not context.analytics_meta.analysis_id: + context.issues.append("Не найден metadata-тег analysis_id в начале аналитики.") + return context + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return {"source_content_len": len(context.source_content or "")} + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "analytics_meta": { + "analysis_id": context.analytics_meta.analysis_id, + "application": context.analytics_meta.application, + "platform": context.analytics_meta.platform, + "domain": context.analytics_meta.domain, + "sub_domain": context.analytics_meta.subdomain, + }, + "requirements": [ + { + "section_key": item.section_key, + "heading": item.heading, + "metadata": dict(item.metadata), + } + for item in context.requirements + ], + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/step.py new file mode 100644 index 0000000..c9a4a46 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_load_rules/step.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from pathlib import Path +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RuleDocument +from app.core.agent.utils.workflow import WorkflowStep + + +class LoadRulesStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step5_load_rules" + title = "Загрузка правил документации v3" + + def __init__(self, rules_root: Path | None = None) -> None: + self._rules_root = rules_root or self._discover_rules_root() + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer: + return context + if not self._rules_root.exists(): + context.issues.append(f"Папка rules не найдена: {self._rules_root.as_posix()}") + return context + loaded: list[RuleDocument] = [] + for item in sorted(self._rules_root.rglob("*.md")): + try: + loaded.append( + RuleDocument( + name=item.relative_to(self._rules_root).as_posix(), + content=item.read_text(encoding="utf-8"), + ) + ) + except Exception as exc: + context.issues.append(f"Не удалось прочитать rule {item.name}: {exc}") + context.rules = loaded + if not context.rules: + context.issues.append("Rules v2 пустые: не найдено ни одного *.md файла.") + return context + + def _discover_rules_root(self) -> Path: + current = Path(__file__).resolve() + for parent in current.parents: + candidate = parent / "_process" / "doc_rules_v3" + if candidate.exists(): + return candidate + return current.parents[9] / "_process" / "doc_rules_v3" + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return {"rules_root": self._rules_root.as_posix()} + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "rules_count": len(context.rules), + "rule_names": [item.name for item in context.rules], + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/__init__.py new file mode 100644 index 0000000..f41e2e1 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/__init__.py @@ -0,0 +1,5 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step4_prepare_tasks.step import ( + PrepareRequirementTasksStep, +) + +__all__ = ["PrepareRequirementTasksStep"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/services.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/services.py new file mode 100644 index 0000000..b504dc9 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/services.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.path_resolver import ( + DocsPathResolver, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.services import ( + TaskActionResolver, + TaskAttributeResolver, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import ( + RequirementTaskContext, +) +from app.core.agent.utils.llm import AgentLlmService + +if TYPE_CHECKING: + from app.core.rag.persistence.query_repository import RagQueryRepository + + +class DocsCatalogLoader: + def __init__(self, query_repository: "RagQueryRepository | None" = None) -> None: + self._query_repository = query_repository + + def load(self, context: DocUpdateFromFeatureV2Context) -> None: + if not context.rag_session_id: + context.issues.append("Отсутствует active RAG session: fallback по структуре документации будет ограничен.") + return + repository = self._query_repository or self._build_repository() + try: + context.docs_catalog_rows = repository.list_docs_scope_index_rows(context.rag_session_id) + except Exception as exc: + context.issues.append(f"Не удалось загрузить docs catalog rows: {exc}") + + def _build_repository(self) -> "RagQueryRepository": + from app.core.rag.persistence.query_repository import RagQueryRepository + + self._query_repository = RagQueryRepository() + return self._query_repository + + +class RequirementTaskBuilder: + def __init__(self, llm: AgentLlmService) -> None: + self._path_resolver = DocsPathResolver() + self._attribute_resolver = TaskAttributeResolver(llm) + self._action_resolver = TaskActionResolver(llm) + + def build(self, context: DocUpdateFromFeatureV2Context) -> list[RequirementTaskContext]: + known_paths = {str(row.get("path") or "") for row in context.docs_catalog_rows} + tasks: list[RequirementTaskContext] = [] + for index, requirement in enumerate(context.requirements): + task = RequirementTaskContext( + index=index, + section_key=requirement.section_key or f"6.{index + 1}", + heading=requirement.heading, + body=requirement.body, + metadata=dict(requirement.metadata), + ) + self._attribute_resolver.resolve(context, task, rules_text="") + task.path = self._path_resolver.resolve( + application=task.application, + platform=task.platform, + doc_type=task.doc_type, + doc_id=task.doc_id, + domain=task.domain, + ) + task.action = self._action_resolver.resolve(task, known_paths) + known_paths.add(task.path) + tasks.append(task) + return tasks + + +class RequirementTaskOrderer: + _PLATFORM_PRIORITY = {"pprb": 0, "ufs": 1, "web": 2} + + def order(self, tasks: list[RequirementTaskContext]) -> list[RequirementTaskContext]: + return sorted(tasks, key=self._sort_key) + + def _sort_key(self, task: RequirementTaskContext) -> tuple[int, int, int]: + explicit_order = self._explicit_order(task.metadata) + platform_order = self._PLATFORM_PRIORITY.get(task.platform, len(self._PLATFORM_PRIORITY)) + return explicit_order, platform_order, task.index + + def _explicit_order(self, metadata: dict[str, object]) -> int: + value = str(metadata.get("build_order") or metadata.get("order") or "").strip() + return int(value) if value.isdigit() else 1000 diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/step.py new file mode 100644 index 0000000..d73bb58 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step4_prepare_tasks/step.py @@ -0,0 +1,66 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step4_prepare_tasks.services import ( + DocsCatalogLoader, + RequirementTaskBuilder, + RequirementTaskOrderer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.llm import AgentLlmService +from app.core.agent.utils.workflow import WorkflowStep + +if TYPE_CHECKING: + from app.core.rag.persistence.query_repository import RagQueryRepository + + +class PrepareRequirementTasksStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step4_prepare_tasks" + title = "Подготовка задач по страницам документации" + + def __init__(self, llm: AgentLlmService, query_repository: "RagQueryRepository | None" = None) -> None: + self._catalog_loader = DocsCatalogLoader(query_repository=query_repository) + self._task_builder = RequirementTaskBuilder(llm) + self._task_orderer = RequirementTaskOrderer() + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer or not context.requirements: + return context + self._catalog_loader.load(context) + context.requirement_tasks = self._task_orderer.order(self._task_builder.build(context)) + if not context.requirement_tasks: + context.issues.append("Не удалось подготовить задачи по разделу 6 аналитики.") + return context + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "requirements_count": len(context.requirements), + "requirements": [ + {"section_key": item.section_key, "heading": item.heading, "metadata": dict(item.metadata)} + for item in context.requirements + ], + } + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "docs_catalog_rows_count": len(context.docs_catalog_rows), + "tasks": [ + { + "section_key": item.section_key, + "heading": item.heading, + "doc_id": item.doc_id, + "doc_type": item.doc_type, + "application": item.application, + "platform": item.platform, + "domain": item.domain, + "sub_domain": item.subdomain, + "action": item.action.value, + "path": item.path, + } + for item in context.requirement_tasks + ], + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/classifier.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/classifier.py new file mode 100644 index 0000000..87cfd24 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/classifier.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +import re + + +class DeleteIntentHeuristic: + _PATTERNS = ( + re.compile(r"\bdelete\b"), + re.compile(r"\bremove\b"), + re.compile(r"\bdecommission(?:ed|ing)?\b"), + re.compile(r"\bудал(?:ить|ение|яем|яется|ен[аоы]?|ить страницу|ить документ)\b"), + re.compile(r"\bдекомисс"), + ) + + def is_delete(self, text: str) -> bool: + lowered = (text or "").lower() + return any(pattern.search(lowered) for pattern in self._PATTERNS) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/path_resolver.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/path_resolver.py new file mode 100644 index 0000000..c7328e2 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/path_resolver.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import re + + +class DocsPathResolver: + def resolve(self, *, application: str, platform: str, doc_type: str, doc_id: str, domain: str = "") -> str: + root = self._clean(domain) or self._clean(application) or "common" + plat = self._clean(platform) or "web" + dtype = self._clean(doc_type) or "misc" + did = self._clean(doc_id) or "untitled" + return f"docs/{root}/{plat}/{dtype}/{did}.md" + + def _clean(self, value: str) -> str: + normalized = re.sub(r"[^a-zA-Z0-9._-]+", "-", (value or "").strip().lower()) + return normalized.strip(".-") diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/prompts/prompts.yml new file mode 100644 index 0000000..8431df5 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/prompts/prompts.yml @@ -0,0 +1,11 @@ +namespace: v2_docs_update_v2 + +prompts: + resolve_attributes_fallback: | + Определи недостающие атрибуты страницы документации по секции аналитики и структуре docs catalog. + Верни только JSON-объект с полями: doc_type, id, application, platform, domain, sub_domain. + Не добавляй пояснений. + + classify_action: | + Классифицируй действие для страницы документации. + Верни только JSON: {"action":"create|update|delete"}. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/services.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/services.py new file mode 100644 index 0000000..819cfc4 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/services.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.classifier import ( + DeleteIntentHeuristic, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc import ( + CreateDocSubprocess, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.delete_doc import ( + DeleteDocSubprocess, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc import EditDocSubprocess +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import ( + AccumulatedPageDraft, + DocAction, + RequirementTaskContext, +) +from app.core.agent.utils.llm import AgentLlmService +from app.schemas.changeset import ChangeItem, ChangeOp + + +class TaskAttributeResolver: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def resolve(self, context: DocUpdateFromFeatureV2Context, task: RequirementTaskContext, rules_text: str) -> None: + task.doc_type = str(task.metadata.get("doc_type") or task.metadata.get("type") or "").strip() + task.doc_id = str(task.metadata.get("id") or self._slug(task.heading)).strip() + task.application = str(task.metadata.get("application") or context.analytics_meta.application or "").strip() + task.platform = str(task.metadata.get("platform") or context.analytics_meta.platform or "").strip().lower() + task.domain = str(task.metadata.get("domain") or context.analytics_meta.domain or "").strip() + task.subdomain = str( + task.metadata.get("sub_domain") or task.metadata.get("subdomain") or context.analytics_meta.subdomain or "" + ).strip() + missing = [ + name + for name, value in [ + ("doc_type", task.doc_type), + ("id", task.doc_id), + ("application", task.application), + ("platform", task.platform), + ("domain", task.domain), + ("sub_domain", task.subdomain), + ] + if not value + ] + if not missing: + return + payload = { + "missing": missing, + "full_document": context.source_content, + "requirement": {"heading": task.heading, "body": task.body, "metadata": task.metadata}, + "docs_catalog_paths": [str(row.get("path") or "") for row in context.docs_catalog_rows[:300]], + "rules": rules_text, + } + raw = self._llm.generate( + "v2_docs_update_v2.resolve_attributes_fallback", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.resolve_attributes", + ) + parsed = self._json_or_empty(raw) + task.doc_type = task.doc_type or str(parsed.get("doc_type") or parsed.get("type") or "").strip() + task.doc_id = task.doc_id or str(parsed.get("id") or self._slug(task.heading)).strip() + task.application = task.application or str(parsed.get("application") or "").strip() + task.platform = task.platform or str(parsed.get("platform") or "web").strip().lower() + task.domain = task.domain or str(parsed.get("domain") or "").strip() + task.subdomain = task.subdomain or str(parsed.get("sub_domain") or parsed.get("subdomain") or "").strip() + + def _slug(self, value: str) -> str: + return re.sub(r"[^a-z0-9._-]+", "-", (value or "").strip().lower()).strip(".-") or "untitled" + + def _json_or_empty(self, raw: str) -> dict[str, object]: + value = str(raw or "").strip() + if not value: + return {} + try: + payload = json.loads(value) + return payload if isinstance(payload, dict) else {} + except Exception: + return {} + + +class TaskActionResolver: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + self._delete_heuristic = DeleteIntentHeuristic() + + def resolve(self, task: RequirementTaskContext, known_paths: set[str]) -> DocAction: + explicit = self._explicit_action(task) + if explicit is not None: + return explicit + if self._delete_heuristic.is_delete(task.body): + return DocAction.DELETE + return DocAction.EDIT if task.path in known_paths else DocAction.CREATE + + def _explicit_action(self, task: RequirementTaskContext) -> DocAction | None: + value = str(task.metadata.get("action") or task.metadata.get("op") or task.metadata.get("operation") or "") + normalized = value.strip().lower() + if normalized in {"create", "add", "new"}: + return DocAction.CREATE + if normalized in {"update", "edit", "modify"}: + return DocAction.EDIT + if normalized in {"delete", "remove"}: + return DocAction.DELETE + return None + +class TaskChangeExecutor: + def __init__(self, llm: AgentLlmService) -> None: + self._create = CreateDocSubprocess(llm) + self._edit = EditDocSubprocess(llm) + self._delete = DeleteDocSubprocess() + + def execute(self, context: DocUpdateFromFeatureV2Context, task: RequirementTaskContext) -> None: + shared_context = self._build_shared_context(context) + if task.action == DocAction.CREATE: + result = self._create.run(task, rule_documents=context.rules, shared_context=shared_context) + context.changeset.append( + ChangeItem(op=ChangeOp.CREATE, path=task.path, proposed_content=result.content, reason=task.heading) + ) + self._remember_change(context, task, result.content) + return + if task.action == DocAction.EDIT: + current_content, base_hash = self._load_current_content(context, task.path) + if not current_content or not base_hash: + context.issues.append(f"Пропущен UPDATE для {task.path}: не найден текущий файл.") + return + result = self._edit.run( + task, + current_content=current_content, + rule_documents=context.rules, + shared_context=shared_context, + ) + context.changeset.append( + ChangeItem( + op=ChangeOp.UPDATE, + path=task.path, + base_hash=base_hash, + proposed_content=result.content, + reason=task.heading, + ) + ) + self._remember_change(context, task, result.content) + return + _, base_hash = self._load_current_content(context, task.path) + if not base_hash: + context.issues.append(f"Пропущен DELETE для {task.path}: не найден текущий файл.") + return + self._delete.run(task) + context.changeset.append(ChangeItem(op=ChangeOp.DELETE, path=task.path, base_hash=base_hash, reason=task.heading)) + self._remember_change(context, task, "") + + def _build_shared_context(self, context: DocUpdateFromFeatureV2Context) -> dict[str, object]: + return { + "analytics_meta": { + "analysis_id": context.analytics_meta.analysis_id, + "application": context.analytics_meta.application, + "platform": context.analytics_meta.platform, + "domain": context.analytics_meta.domain, + "sub_domain": context.analytics_meta.subdomain, + }, + "planned_pages": [ + { + "section_key": item.section_key, + "heading": item.heading, + "path": item.path, + "doc_id": item.doc_id, + "doc_type": item.doc_type, + "platform": item.platform, + "application": item.application, + "domain": item.domain, + "sub_domain": item.subdomain, + "action": item.action.value, + } + for item in context.requirement_tasks + ], + "accumulated_pages": [ + { + "path": item.path, + "heading": item.heading, + "doc_id": item.doc_id, + "doc_type": item.doc_type, + "platform": item.platform, + "action": item.action.value, + "content": item.content, + } + for item in context.accumulated_pages + ], + } + + def _load_current_content(self, context: DocUpdateFromFeatureV2Context, rel_path: str) -> tuple[str, str]: + for item in reversed(context.accumulated_pages): + if item.path == rel_path and item.action != DocAction.DELETE: + content = item.content + return content, hashlib.sha256(content.encode("utf-8")).hexdigest() + root = Path(context.project_root or "") + if not root.is_absolute(): + return "", "" + target = root / rel_path + if not target.exists() or not target.is_file(): + return "", "" + content = target.read_text(encoding="utf-8") + base_hash = hashlib.sha256(content.encode("utf-8")).hexdigest() + return content, base_hash + + def _remember_change(self, context: DocUpdateFromFeatureV2Context, task: RequirementTaskContext, content: str) -> None: + context.accumulated_pages = [item for item in context.accumulated_pages if item.path != task.path] + context.accumulated_pages.append( + AccumulatedPageDraft( + path=task.path, + heading=task.heading, + doc_id=task.doc_id, + doc_type=task.doc_type, + platform=task.platform, + action=task.action, + content=content, + ) + ) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/step.py new file mode 100644 index 0000000..25ad820 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/step.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.services import ( + TaskChangeExecutor, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.llm import AgentLlmService +from app.core.agent.utils.workflow import WorkflowStep + + +class ExecuteRequirementSubprocessesStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step6_execute_subprocesses" + title = "Выполнение подпроцессов create/edit/delete" + + def __init__(self, llm: AgentLlmService) -> None: + self._change_executor = TaskChangeExecutor(llm) + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer or not context.requirement_tasks: + return context + for task in context.requirement_tasks: + self._change_executor.execute(context, task) + return context + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "tasks": [ + { + "section_key": item.section_key, + "heading": item.heading, + "action": item.action.value, + "path": item.path, + "doc_type": item.doc_type, + } + for item in context.requirement_tasks + ], + "rules_count": len(context.rules), + } + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "changeset": [ + {"op": item.op.value, "path": item.path, "reason": item.reason} + for item in context.changeset + ], + "accumulated_pages": [ + { + "path": item.path, + "doc_id": item.doc_id, + "doc_type": item.doc_type, + "action": item.action.value, + } + for item in context.accumulated_pages + ], + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/step.py new file mode 100644 index 0000000..467426c --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step6_finalize/step.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import json +from typing import Any + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.utils.workflow import WorkflowStep + + +class FinalizeAnswerStep(WorkflowStep[DocUpdateFromFeatureV2Context]): + step_id = "step7_finalize" + title = "Формирование итогового ответа" + + async def run(self, context: DocUpdateFromFeatureV2Context) -> DocUpdateFromFeatureV2Context: + if context.answer: + return context + lines: list[str] = ["DOC_UPDATE/FROM_FEATURE v2: сформирован changeset."] + if context.issues: + lines.append("\nIssues:") + for issue in context.issues: + lines.append(f"- {issue}") + lines.append("\nTasks:") + for task in context.requirement_tasks: + lines.append( + f"- {task.section_key or f'#{task.index + 1}'} {task.action.value}: {task.path}" + f" [{task.platform or '-'} | {task.doc_type or '-'}]" + ) + if not context.requirement_tasks: + lines.append("- Нет задач для обработки.") + lines.append("\nChangeset:") + lines.append("```json") + lines.append(json.dumps([item.model_dump() for item in context.changeset], ensure_ascii=False, indent=2)) + lines.append("```") + context.apply_changeset = bool(context.changeset) + lines.append(f"\napply_changeset: {str(context.apply_changeset).lower()}") + context.answer = "\n".join(lines) + context.answer_generated_payload = { + "answer_mode": "docs_update_changeset_v2", + "changeset_items": len(context.changeset), + "apply_changeset": context.apply_changeset, + } + return context + + def trace_input(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "tasks_count": len(context.requirement_tasks), + "changeset_count": len(context.changeset), + "issues_count": len(context.issues), + } + + def trace_output(self, context: DocUpdateFromFeatureV2Context) -> dict[str, Any]: + return { + "answer_generated_payload": dict(context.answer_generated_payload or {}), + "answer_len": len(context.answer or ""), + "apply_changeset": context.apply_changeset, + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/__init__.py new file mode 100644 index 0000000..e36595d --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/__init__.py @@ -0,0 +1,10 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.models import ( + SubprocessResult, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSection, + TemplateSpec, +) + +__all__ = ["SectionDraft", "SubprocessResult", "TemplateSection", "TemplateSpec"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/document_composer.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/document_composer.py new file mode 100644 index 0000000..69dbec9 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/document_composer.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import re + +import yaml + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSpec, +) + + +class TemplateDocumentComposer: + def compose(self, template: TemplateSpec, frontmatter: str, sections: list[SectionDraft], fallback_title: str) -> str: + title = self._extract_title(frontmatter) or fallback_title + section_map = {(item.level, item.title): item.content.strip() for item in sections} + lines = [frontmatter.strip(), "", f"{'#' * template.title_level} {title}", ""] + for section in template.sections: + lines.append(f"{'#' * section.level} {section.title}") + content = section_map.get((section.level, section.title), "") + if content: + lines.extend(["", content, ""]) + continue + lines.append("") + return "\n".join(lines).strip() + "\n" + + def _extract_title(self, frontmatter: str) -> str: + value = frontmatter.strip() + match = re.match(r"^---\n(.*?)\n---$", value, re.DOTALL) + if not match: + return "" + payload = yaml.safe_load(match.group(1)) or {} + return str(payload.get("title") or "").strip() if isinstance(payload, dict) else "" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/models.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/models.py new file mode 100644 index 0000000..9f55346 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/models.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext + + +@dataclass(slots=True) +class SubprocessResult: + task: RequirementTaskContext + content: str | None + issue: str = "" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/rules_catalog.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/rules_catalog.py new file mode 100644 index 0000000..29882ab --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/rules_catalog.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RuleDocument + + +@dataclass(slots=True) +class RulesCatalog: + by_name: dict[str, str] + + @classmethod + def from_documents(cls, rules: list[RuleDocument]) -> "RulesCatalog": + return cls(by_name={item.name: item.content for item in rules}) + + def template_text(self, doc_type: str) -> str: + return self.by_name.get(f"templates/{doc_type}.template.md", "") + + def section_text(self, rule_path: str) -> str: + return self.by_name.get(rule_path, "") + + def global_text(self) -> str: + parts: list[str] = [] + documentation_rules = self.by_name.get("documentation-rules.md", "").strip() + if documentation_rules: + parts.append("## documentation-rules.md") + parts.append(documentation_rules) + for name in sorted(self.by_name): + if not name.startswith("global/"): + continue + value = self.by_name.get(name, "").strip() + if not value: + continue + parts.append(f"## {name}") + parts.append(value) + return "\n\n".join(parts).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/source_sections.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/source_sections.py new file mode 100644 index 0000000..5bd5d0d --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/source_sections.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import re + + +class RequirementSourceSections: + _HEADING_RE = re.compile(r"^(#{2,6})\s+(.+?)\s*$") + + def extract(self, text: str) -> dict[str, str]: + sections: dict[str, str] = {} + current_key = "" + current_level = 0 + current_lines: list[str] = [] + for raw in text.splitlines(): + heading = self._match_heading(raw) + if heading: + level, title = heading + if current_key and level <= current_level: + sections[current_key] = "\n".join(current_lines).strip() + current_key = "" + current_lines = [] + normalized = self._normalize(title) + if level >= 3: + if current_key: + sections[current_key] = "\n".join(current_lines).strip() + current_key = normalized + current_level = level + current_lines = [] + continue + if current_key: + current_lines.append(raw) + if current_key: + sections[current_key] = "\n".join(current_lines).strip() + return sections + + def find_for_title(self, sections: dict[str, str], title: str) -> str: + normalized = self._normalize(title) + if normalized in sections: + return sections[normalized] + for key, value in sections.items(): + if key.startswith(normalized) or normalized.startswith(key): + return value + return "" + + def _match_heading(self, raw: str) -> tuple[int, str] | None: + match = self._HEADING_RE.match(raw.strip()) + if not match: + return None + return len(match.group(1)), match.group(2).strip() + + def _normalize(self, value: str) -> str: + text = value.lower().replace("ё", "е") + text = text.replace("контракт метода", "контракт") + text = text.replace("технический use case (тезисно)", "технический use case") + text = re.sub(r"[^a-zа-я0-9]+", " ", text) + return re.sub(r"\s+", " ", text).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_models.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_models.py new file mode 100644 index 0000000..3c12325 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_models.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses import field + + +@dataclass(slots=True) +class TemplateSection: + title: str + level: int + rule_path: str = "" + has_children: bool = False + + +@dataclass(slots=True) +class TemplateSpec: + doc_type: str + source_name: str + template_text: str + title_level: int = 1 + sections: list[TemplateSection] = field(default_factory=list) + + +@dataclass(slots=True) +class SectionDraft: + title: str + level: int + content: str diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_parser.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_parser.py new file mode 100644 index 0000000..2eb7087 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_parser.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import re + +import yaml + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + TemplateSection, + TemplateSpec, +) + + +class TemplateParser: + _HEADING_RE = re.compile(r"^(#{1,6})\s+(.+?)\s*$") + _RULE_REF_RE = re.compile(r"`(\.\./common-elements/[^`]+|common-elements/[^`]+)`") + + def parse(self, source_name: str, template_text: str) -> TemplateSpec: + _, body = self._split_frontmatter(template_text) + doc_type = self._doc_type(template_text, source_name) + title_level = 1 + sections: list[TemplateSection] = [] + lines = body.splitlines() + index = 0 + while index < len(lines): + heading = self._match_heading(lines[index]) + if not heading: + index += 1 + continue + level, title = heading + if "<title>" in title: + title_level = level + index += 1 + continue + rule_path = self._extract_rule_path(lines, index + 1) + sections.append(TemplateSection(title=title, level=level, rule_path=rule_path)) + index += 1 + self._mark_children(sections) + return TemplateSpec( + doc_type=doc_type, + source_name=source_name, + template_text=template_text, + title_level=title_level, + sections=sections, + ) + + def _split_frontmatter(self, text: str) -> tuple[dict[str, object], str]: + value = text.strip() + if not value.startswith("---\n"): + return {}, text + _, _, tail = value.partition("---\n") + frontmatter_text, _, body = tail.partition("\n---\n") + payload = yaml.safe_load(frontmatter_text) or {} + return payload if isinstance(payload, dict) else {}, body + + def _doc_type(self, text: str, source_name: str) -> str: + frontmatter, _ = self._split_frontmatter(text) + doc_type = str(frontmatter.get("doc_type") or "").strip() + if doc_type: + return doc_type + suffix = ".template.md" + filename = source_name.split("/")[-1] + return filename[: -len(suffix)] if filename.endswith(suffix) else filename + + def _match_heading(self, line: str) -> tuple[int, str] | None: + match = self._HEADING_RE.match(line.strip()) + if not match: + return None + return len(match.group(1)), match.group(2).strip() + + def _extract_rule_path(self, lines: list[str], start: int) -> str: + limit = min(len(lines), start + 4) + for index in range(start, limit): + match = self._RULE_REF_RE.search(lines[index]) + if not match: + continue + return match.group(1).replace("../", "") + return "" + + def _mark_children(self, sections: list[TemplateSection]) -> None: + for index, section in enumerate(sections): + if index + 1 >= len(sections): + continue + section.has_children = sections[index + 1].level > section.level diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_registry.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_registry.py new file mode 100644 index 0000000..905a497 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/common/template_registry.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + TemplateSpec, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_parser import ( + TemplateParser, +) + + +class TemplateRegistry: + def __init__(self, parser: TemplateParser | None = None) -> None: + self._parser = parser or TemplateParser() + + def load(self, catalog: RulesCatalog, doc_type: str) -> TemplateSpec: + template_name = f"templates/{doc_type}.template.md" + template_text = catalog.template_text(doc_type) + if not template_text.strip(): + raise ValueError(f"Template not found for doc_type '{doc_type}'") + return self._parser.parse(template_name, template_text) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/__init__.py new file mode 100644 index 0000000..c2085a8 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/__init__.py @@ -0,0 +1,5 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc.process import ( + CreateDocSubprocess, +) + +__all__ = ["CreateDocSubprocess"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/process.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/process.py new file mode 100644 index 0000000..52bb330 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/process.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common import SubprocessResult +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.document_composer import ( + TemplateDocumentComposer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_registry import ( + TemplateRegistry, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc.steps.step1_generate_frontmatter.step import ( + GenerateFrontmatterStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc.steps.step2_generate_sections.step import ( + GenerateSectionsStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc.steps.step4_compose_document.step import ( + ComposeDocumentStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RuleDocument +from app.core.agent.utils.llm import AgentLlmService + + +class CreateDocSubprocess: + def __init__(self, llm: AgentLlmService) -> None: + self._frontmatter = GenerateFrontmatterStep(llm) + self._sections = GenerateSectionsStep(llm) + self._templates = TemplateRegistry() + self._compose = ComposeDocumentStep(TemplateDocumentComposer()) + + def run( + self, + task: RequirementTaskContext, + *, + rule_documents: list[RuleDocument], + shared_context: dict[str, object], + ) -> SubprocessResult: + catalog = RulesCatalog.from_documents(rule_documents) + template = self._templates.load(catalog, task.doc_type) + frontmatter = self._frontmatter.run(task, template=template, catalog=catalog, shared_context=shared_context) + sections = self._sections.run(task, template=template, catalog=catalog, shared_context=shared_context) + content = self._compose.run(frontmatter, template, sections, fallback_title=task.heading) + return SubprocessResult(task=task, content=content) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/prompts/prompts.yml new file mode 100644 index 0000000..fcd3109 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/prompts/prompts.yml @@ -0,0 +1,8 @@ +namespace: v2_docs_update_v2 + +prompts: + create_frontmatter: | + Сгенерируй только frontmatter в markdown между --- и ---. + Не добавляй другие секции и не добавляй пояснений. + Используй requirement_text, template, global_rules, frontmatter_rules и shared_context. + Обязательно заполни id, title, doc_type, status, domain, sub_domain, related_docs. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/step.py new file mode 100644 index 0000000..79b5d4d --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/step.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + TemplateSpec, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class GenerateFrontmatterStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + *, + template: TemplateSpec, + catalog: RulesCatalog, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "application": task.application, + "platform": task.platform, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + "heading": task.heading, + }, + "requirement_text": task.body, + "template": {"source_name": template.source_name, "template_text": template.template_text}, + "global_rules": catalog.global_text(), + "frontmatter_rules": catalog.section_text("global/frontmatter.md"), + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.create_frontmatter", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.create.frontmatter", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/__init__.py new file mode 100644 index 0000000..8f4492c --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/__init__.py @@ -0,0 +1,5 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.create_doc.steps.step2_generate_sections.step import ( + GenerateSectionsStep, +) + +__all__ = ["GenerateSectionsStep"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/prompts/prompts.yml new file mode 100644 index 0000000..1f354e6 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/prompts/prompts.yml @@ -0,0 +1,11 @@ +namespace: v2_docs_update_v2 + +prompts: + create_section: | + Сгенерируй содержимое одного markdown-раздела документа. + Верни только тело секции, без заголовка. + Строго следуй template.template_text, target_section.rule_text и global_rules. + Используй source_fragment как приоритетный источник фактов. + Если для секции есть структура в rule_text, заполни ее полностью, без заглушек и без фраз вида "описано отдельно". + Если source_fragment содержит "Не выявлены", это не запрещает детализировать интеграционный FR из use case, contract и shared_context. + Не ссылайся на rule-файлы в тексте документа. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/step.py new file mode 100644 index 0000000..f98a261 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/step.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.source_sections import ( + RequirementSourceSections, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSpec, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class GenerateSectionsStep: + def __init__(self, llm: AgentLlmService, source_sections: RequirementSourceSections | None = None) -> None: + self._llm = llm + self._source_sections = source_sections or RequirementSourceSections() + + def run( + self, + task: RequirementTaskContext, + *, + template: TemplateSpec, + catalog: RulesCatalog, + shared_context: dict[str, object], + ) -> list[SectionDraft]: + source_sections = self._source_sections.extract(task.body) + drafts: list[SectionDraft] = [] + for section in template.sections: + content = self._generate_section(task, template, catalog, section.title, section.level, section.rule_path, section.has_children, source_sections, shared_context) + drafts.append(SectionDraft(title=section.title, level=section.level, content=content)) + return drafts + + def _generate_section( + self, + task: RequirementTaskContext, + template: TemplateSpec, + catalog: RulesCatalog, + title: str, + level: int, + rule_path: str, + has_children: bool, + source_sections: dict[str, str], + shared_context: dict[str, object], + ) -> str: + source_fragment = self._source_sections.find_for_title(source_sections, title) + if has_children and not source_fragment: + return "" + payload = { + "task": self._task_payload(task), + "template": { + "doc_type": template.doc_type, + "source_name": template.source_name, + "template_text": template.template_text, + "sections": [{"title": item.title, "level": item.level, "rule_path": item.rule_path} for item in template.sections], + }, + "target_section": { + "title": title, + "level": level, + "rule_path": rule_path, + "rule_text": catalog.section_text(rule_path), + }, + "source_fragment": source_fragment, + "all_source_sections": source_sections, + "global_rules": catalog.global_text(), + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.create_section", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context=f"workflow.v2.docs_update.from_feature_v2.create.section.{title}", + ).strip() + + def _task_payload(self, task: RequirementTaskContext) -> dict[str, object]: + return { + "section_key": task.section_key, + "heading": task.heading, + "doc_type": task.doc_type, + "doc_id": task.doc_id, + "domain": task.domain, + "sub_domain": task.subdomain, + "application": task.application, + "platform": task.platform, + "path": task.path, + "requirement_text": task.body, + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/prompts/prompts.yml new file mode 100644 index 0000000..cf2294c --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/prompts/prompts.yml @@ -0,0 +1,7 @@ +namespace: v2_docs_update_v2 + +prompts: + create_summary: | + Сгенерируй раздел Summary для нового документа. + Учитывай related pages из shared_context, чтобы не дублировать соседние страницы. + Верни только текст раздела, без заголовка уровня 2. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/step.py new file mode 100644 index 0000000..62425df --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_summary/step.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class GenerateSummaryStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + requirement_text: str, + rules_text: str, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "heading": task.heading, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + }, + "requirement_text": requirement_text, + "rules": rules_text, + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.create_summary", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.create.summary", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/prompts/prompts.yml new file mode 100644 index 0000000..e358799 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/prompts/prompts.yml @@ -0,0 +1,7 @@ +namespace: v2_docs_update_v2 + +prompts: + create_details: | + Сгенерируй раздел Details для нового документа. + Используй rules из _process/doc_rules_v3 и accumulated_pages из shared_context. + Верни только тело раздела, без заголовка уровня 2. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/step.py new file mode 100644 index 0000000..5498d1b --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step3_generate_details/step.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class GenerateDetailsStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + requirement_text: str, + rules_text: str, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "heading": task.heading, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + }, + "requirement_text": requirement_text, + "rules": rules_text, + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.create_details", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.create.details", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/step.py new file mode 100644 index 0000000..7c00932 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step4_compose_document/step.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.document_composer import ( + TemplateDocumentComposer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSpec, +) + +class ComposeDocumentStep: + def __init__(self, composer: TemplateDocumentComposer | None = None) -> None: + self._composer = composer or TemplateDocumentComposer() + + def run(self, frontmatter: str, template: TemplateSpec, sections: list[SectionDraft], fallback_title: str) -> str: + return self._composer.compose(template, frontmatter, sections, fallback_title) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/__init__.py new file mode 100644 index 0000000..ba22254 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/__init__.py @@ -0,0 +1,5 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.delete_doc.process import ( + DeleteDocSubprocess, +) + +__all__ = ["DeleteDocSubprocess"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/process.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/process.py new file mode 100644 index 0000000..08463da --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/process.py @@ -0,0 +1,15 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common import SubprocessResult +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.delete_doc.steps.step1_build_change.step import ( + BuildDeleteChangeStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext + + +class DeleteDocSubprocess: + def __init__(self) -> None: + self._step = BuildDeleteChangeStep() + + def run(self, task: RequirementTaskContext) -> SubprocessResult: + return self._step.run(task) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/step.py new file mode 100644 index 0000000..ab1fde4 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/delete_doc/steps/step1_build_change/step.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common import SubprocessResult +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext + + +class BuildDeleteChangeStep: + def run(self, task: RequirementTaskContext) -> SubprocessResult: + return SubprocessResult(task=task, content=None) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/__init__.py new file mode 100644 index 0000000..6b45bed --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/__init__.py @@ -0,0 +1,3 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc.process import EditDocSubprocess + +__all__ = ["EditDocSubprocess"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/process.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/process.py new file mode 100644 index 0000000..1b811fe --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/process.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common import SubprocessResult +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.document_composer import ( + TemplateDocumentComposer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_registry import ( + TemplateRegistry, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc.steps.step1_generate_frontmatter.step import ( + EditFrontmatterStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc.steps.step2_generate_sections.step import ( + EditSectionsStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc.steps.step4_compose_document.step import ( + ComposeEditedDocumentStep, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RuleDocument +from app.core.agent.utils.llm import AgentLlmService + + +class EditDocSubprocess: + def __init__(self, llm: AgentLlmService) -> None: + self._frontmatter = EditFrontmatterStep(llm) + self._sections = EditSectionsStep(llm) + self._templates = TemplateRegistry() + self._compose = ComposeEditedDocumentStep(TemplateDocumentComposer()) + + def run( + self, + task: RequirementTaskContext, + *, + current_content: str, + rule_documents: list[RuleDocument], + shared_context: dict[str, object], + ) -> SubprocessResult: + catalog = RulesCatalog.from_documents(rule_documents) + template = self._templates.load(catalog, task.doc_type) + frontmatter = self._frontmatter.run( + task, + current_content=current_content, + template=template, + catalog=catalog, + shared_context=shared_context, + ) + sections = self._sections.run( + task, + current_content=current_content, + template=template, + catalog=catalog, + shared_context=shared_context, + ) + content = self._compose.run(frontmatter, template, sections, fallback_title=task.heading) + return SubprocessResult(task=task, content=content) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/prompts/prompts.yml new file mode 100644 index 0000000..36e905c --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/prompts/prompts.yml @@ -0,0 +1,7 @@ +namespace: v2_docs_update_v2 + +prompts: + edit_frontmatter: | + Внеси изменения в frontmatter документа по requirement_text. + Учитывай template, global_rules, frontmatter_rules и новые domain/sub_domain и связи из shared_context. + Верни только frontmatter между --- и ---. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/step.py new file mode 100644 index 0000000..a3527ed --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/step.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + TemplateSpec, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class EditFrontmatterStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + *, + current_content: str, + template: TemplateSpec, + catalog: RulesCatalog, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "heading": task.heading, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + }, + "current_content": current_content, + "requirement_text": task.body, + "template": {"source_name": template.source_name, "template_text": template.template_text}, + "global_rules": catalog.global_text(), + "frontmatter_rules": catalog.section_text("global/frontmatter.md"), + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.edit_frontmatter", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.edit.frontmatter", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/__init__.py new file mode 100644 index 0000000..d48e4ab --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/__init__.py @@ -0,0 +1,5 @@ +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.edit_doc.steps.step2_generate_sections.step import ( + EditSectionsStep, +) + +__all__ = ["EditSectionsStep"] diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/prompts/prompts.yml new file mode 100644 index 0000000..015840b --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/prompts/prompts.yml @@ -0,0 +1,9 @@ +namespace: v2_docs_update_v2 + +prompts: + edit_section: | + Обнови содержимое одного markdown-раздела документа. + Верни только тело секции, без заголовка. + Строго следуй template.template_text, target_section.rule_text и global_rules. + Используй source_fragment как приоритетный источник фактов и сохраняй согласованность с current_content. + Не оставляй заглушки, само-ссылки и фразы вида "описано отдельно". diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/step.py new file mode 100644 index 0000000..1a20564 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/step.py @@ -0,0 +1,91 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.rules_catalog import ( + RulesCatalog, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.source_sections import ( + RequirementSourceSections, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSpec, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class EditSectionsStep: + def __init__(self, llm: AgentLlmService, source_sections: RequirementSourceSections | None = None) -> None: + self._llm = llm + self._source_sections = source_sections or RequirementSourceSections() + + def run( + self, + task: RequirementTaskContext, + *, + current_content: str, + template: TemplateSpec, + catalog: RulesCatalog, + shared_context: dict[str, object], + ) -> list[SectionDraft]: + source_sections = self._source_sections.extract(task.body) + drafts: list[SectionDraft] = [] + for section in template.sections: + content = self._generate_section(task, current_content, template, catalog, section.title, section.level, section.rule_path, section.has_children, source_sections, shared_context) + drafts.append(SectionDraft(title=section.title, level=section.level, content=content)) + return drafts + + def _generate_section( + self, + task: RequirementTaskContext, + current_content: str, + template: TemplateSpec, + catalog: RulesCatalog, + title: str, + level: int, + rule_path: str, + has_children: bool, + source_sections: dict[str, str], + shared_context: dict[str, object], + ) -> str: + source_fragment = self._source_sections.find_for_title(source_sections, title) + if has_children and not source_fragment: + return "" + payload = { + "task": { + "section_key": task.section_key, + "heading": task.heading, + "doc_type": task.doc_type, + "doc_id": task.doc_id, + "domain": task.domain, + "sub_domain": task.subdomain, + "application": task.application, + "platform": task.platform, + "path": task.path, + "requirement_text": task.body, + }, + "current_content": current_content, + "template": { + "doc_type": template.doc_type, + "source_name": template.source_name, + "template_text": template.template_text, + "sections": [{"title": item.title, "level": item.level, "rule_path": item.rule_path} for item in template.sections], + }, + "target_section": { + "title": title, + "level": level, + "rule_path": rule_path, + "rule_text": catalog.section_text(rule_path), + }, + "source_fragment": source_fragment, + "all_source_sections": source_sections, + "global_rules": catalog.global_text(), + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.edit_section", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context=f"workflow.v2.docs_update.from_feature_v2.edit.section.{title}", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/prompts/prompts.yml new file mode 100644 index 0000000..2c2267c --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/prompts/prompts.yml @@ -0,0 +1,7 @@ +namespace: v2_docs_update_v2 + +prompts: + edit_summary: | + Обнови только раздел Summary документа по requirement_text. + Сохраняй согласованность с accumulated_pages из shared_context. + Верни только тело Summary без заголовка. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/step.py new file mode 100644 index 0000000..9ae31ca --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_summary/step.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class EditSummaryStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + current_content: str, + rules_text: str, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "heading": task.heading, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + }, + "current_content": current_content, + "requirement_text": task.body, + "rules": rules_text, + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.edit_summary", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.edit.summary", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/prompts/prompts.yml b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/prompts/prompts.yml new file mode 100644 index 0000000..847c14f --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/prompts/prompts.yml @@ -0,0 +1,7 @@ +namespace: v2_docs_update_v2 + +prompts: + edit_details: | + Обнови только раздел Details документа по requirement_text. + Учитывай rules и взаимосвязанные страницы из shared_context. + Верни только тело Details без заголовка. diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/step.py new file mode 100644 index 0000000..c8720cb --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step3_generate_details/step.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import RequirementTaskContext +from app.core.agent.utils.llm import AgentLlmService + + +class EditDetailsStep: + def __init__(self, llm: AgentLlmService) -> None: + self._llm = llm + + def run( + self, + task: RequirementTaskContext, + current_content: str, + rules_text: str, + shared_context: dict[str, object], + ) -> str: + payload = { + "task": { + "section_key": task.section_key, + "doc_type": task.doc_type, + "id": task.doc_id, + "heading": task.heading, + "domain": task.domain, + "sub_domain": task.subdomain, + "path": task.path, + }, + "current_content": current_content, + "requirement_text": task.body, + "rules": rules_text, + "shared_context": shared_context, + } + return self._llm.generate( + "v2_docs_update_v2.edit_details", + json.dumps(payload, ensure_ascii=False, indent=2), + log_context="workflow.v2.docs_update.from_feature_v2.edit.details", + ).strip() diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/__init__.py new file mode 100644 index 0000000..040eb75 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/__init__.py @@ -0,0 +1 @@ +"""Auto-generated package init.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/step.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/step.py new file mode 100644 index 0000000..ecf0bfa --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step4_compose_document/step.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.document_composer import ( + TemplateDocumentComposer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_models import ( + SectionDraft, + TemplateSpec, +) + +class ComposeEditedDocumentStep: + def __init__(self, composer: TemplateDocumentComposer | None = None) -> None: + self._composer = composer or TemplateDocumentComposer() + + def run(self, frontmatter: str, template: TemplateSpec, sections: list[SectionDraft], fallback_title: str) -> str: + return self._composer.compose(template, frontmatter, sections, fallback_title) diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/__init__.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/__init__.py new file mode 100644 index 0000000..a76c6f5 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/__init__.py @@ -0,0 +1 @@ +"""Runtime helpers for DOC_UPDATE/FROM_FEATURE v2 workflow.""" diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/buffered_graph.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/buffered_graph.py new file mode 100644 index 0000000..9a0dfae --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/buffered_graph.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +from typing import TypeVar + +from app.core.agent.utils.workflow.context import WorkflowContext +from app.core.agent.utils.workflow.graph import WorkflowGraph + +TContext = TypeVar("TContext", bound=WorkflowContext) + + +class DocUpdateFromFeatureV2WorkflowGraph(WorkflowGraph[TContext]): + async def run(self, context: TContext) -> TContext: + trace = context.runtime.trace.module(self._source) + trace.log("workflow_started", {"workflow_id": self._workflow_id}) + steps_buffer: list[dict[str, object]] = [] + for step in self._steps: + before = self._snapshot(context) + raw_inp = step.trace_input(context) + inp = self._merge_trace_payload(raw_inp, before) + request_id = context.runtime.request.request_id + await context.runtime.publisher.publish_status( + request_id, + self._source, + f"Шаг workflow: {step.title}.", + {"workflow_id": self._workflow_id, "step_id": step.step_id}, + ) + context = await step.run(context) + after = self._snapshot(context) + raw_out = step.trace_output(context) + out = self._merge_trace_payload(raw_out, after) + trace.log( + "workflow_step_traced", + { + "workflow_id": self._workflow_id, + "step": {"id": step.step_id, "title": step.title}, + "input": inp, + "output": out, + }, + ) + steps_buffer.append({"step_id": step.step_id, "title": step.title, "input": inp, "output": out}) + trace.log("workflow_trace_flushed", {"workflow_id": self._workflow_id, "steps": steps_buffer}) + trace.log("workflow_completed", {"workflow_id": self._workflow_id}) + return context + + def _merge_trace_payload(self, payload: dict[str, object] | None, snapshot: dict[str, object]) -> dict[str, object]: + if not payload: + return snapshot + merged = dict(payload) + merged["_context"] = snapshot + return merged + + def _snapshot(self, context: TContext) -> dict[str, object]: + analytics = getattr(context, "analytics_meta", None) + requirements = list(getattr(context, "requirements", []) or []) + tasks = list(getattr(context, "requirement_tasks", []) or []) + rules = list(getattr(context, "rules", []) or []) + changeset = list(getattr(context, "changeset", []) or []) + issues = list(getattr(context, "issues", []) or []) + accumulated = list(getattr(context, "accumulated_pages", []) or []) + return { + "source_kind": str(getattr(context, "source_kind", "") or ""), + "source_ref": str(getattr(context, "source_ref", "") or ""), + "project_root": str(getattr(context, "project_root", "") or ""), + "source_content_len": len(str(getattr(context, "source_content", "") or "")), + "analysis_id": str(getattr(analytics, "analysis_id", "") or ""), + "application": str(getattr(analytics, "application", "") or ""), + "platform": str(getattr(analytics, "platform", "") or ""), + "domain": str(getattr(analytics, "domain", "") or ""), + "sub_domain": str(getattr(analytics, "subdomain", "") or ""), + "requirements_count": len(requirements), + "requirements_preview": [ + { + "section_key": str(getattr(item, "section_key", "") or ""), + "heading": str(getattr(item, "heading", "") or ""), + } + for item in requirements[:6] + ], + "docs_rows_count": len(list(getattr(context, "docs_catalog_rows", []) or [])), + "tasks_count": len(tasks), + "tasks_preview": [ + { + "section_key": str(getattr(item, "section_key", "") or ""), + "action": str(getattr(getattr(item, "action", None), "value", "") or ""), + "path": str(getattr(item, "path", "") or ""), + "doc_type": str(getattr(item, "doc_type", "") or ""), + "platform": str(getattr(item, "platform", "") or ""), + } + for item in tasks[:8] + ], + "rules_count": len(rules), + "rule_names_preview": [str(getattr(item, "name", "") or "") for item in rules[:8]], + "changeset_count": len(changeset), + "changeset_preview": [ + { + "op": str(getattr(item, "op", "") or ""), + "path": str(getattr(item, "path", "") or ""), + } + for item in changeset[:8] + ], + "accumulated_pages_count": len(accumulated), + "accumulated_pages_preview": [ + { + "path": str(getattr(item, "path", "") or ""), + "action": str(getattr(getattr(item, "action", None), "value", "") or ""), + } + for item in accumulated[:8] + ], + "apply_changeset": bool(getattr(context, "apply_changeset", False)), + "answer_len": len(str(getattr(context, "answer", "") or "")), + "issues_count": len(issues), + "issues_preview": [str(item) for item in issues[:8]], + } diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/context.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/context.py new file mode 100644 index 0000000..a8cdec3 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/context.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from app.core.agent.runtime.execution_context import RuntimeExecutionContext +from app.core.agent.utils.process_v2.models import V2RouteResult +from app.schemas.changeset import ChangeItem +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import ( + AccumulatedPageDraft, + AnalyticsMeta, + RequirementTaskContext, + RequirementUnit, + RuleDocument, +) + + +@dataclass(slots=True) +class DocUpdateFromFeatureV2Context: + runtime: RuntimeExecutionContext + route: V2RouteResult + rag_session_id: str + source_ref: str = "" + source_kind: str = "" + project_root: str = "" + source_content: str = "" + analytics_meta: AnalyticsMeta = field(default_factory=AnalyticsMeta) + requirements: list[RequirementUnit] = field(default_factory=list) + rules: list[RuleDocument] = field(default_factory=list) + docs_catalog_rows: list[dict] = field(default_factory=list) + requirement_tasks: list[RequirementTaskContext] = field(default_factory=list) + accumulated_pages: list[AccumulatedPageDraft] = field(default_factory=list) + changeset: list[ChangeItem] = field(default_factory=list) + apply_changeset: bool = False + issues: list[str] = field(default_factory=list) + answer: str = "" + answer_generated_payload: dict[str, object] | None = None diff --git a/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/models.py b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/models.py new file mode 100644 index 0000000..2a8abb5 --- /dev/null +++ b/src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/workflow_runtime/models.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from enum import Enum + + +class DocAction(str, Enum): + CREATE = "create" + EDIT = "update" + DELETE = "delete" + + +@dataclass(slots=True) +class AnalyticsMeta: + analysis_id: str = "" + application: str = "" + platform: str = "" + domain: str = "" + subdomain: str = "" + + +@dataclass(slots=True) +class RequirementUnit: + section_key: str + heading: str + body: str + metadata: dict[str, object] = field(default_factory=dict) + + +@dataclass(slots=True) +class RequirementTaskContext: + index: int + section_key: str + heading: str + body: str + metadata: dict[str, object] + doc_type: str = "" + doc_id: str = "" + application: str = "" + platform: str = "" + domain: str = "" + subdomain: str = "" + action: DocAction = DocAction.CREATE + path: str = "" + issues: list[str] = field(default_factory=list) + + +@dataclass(slots=True) +class AccumulatedPageDraft: + path: str + heading: str + doc_id: str + doc_type: str + platform: str + action: DocAction + content: str = "" + + +@dataclass(slots=True) +class RuleDocument: + name: str + content: str diff --git a/src/app/core/application.py b/src/app/core/application.py index 31893ed..35952dc 100644 --- a/src/app/core/application.py +++ b/src/app/core/application.py @@ -61,6 +61,16 @@ class ModularApplication: / "agent/processes/v2/workflows/general_qa_summary/steps/prompts/prompts.yml", Path(__file__).resolve().parent / "agent/processes/v2/workflows/doc_update_from_feature/steps/prompts/prompts.yml", + Path(__file__).resolve().parent + / "agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/prompts/prompts.yml", + Path(__file__).resolve().parent + / "agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/prompts/prompts.yml", + Path(__file__).resolve().parent + / "agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/prompts/prompts.yml", + Path(__file__).resolve().parent + / "agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/prompts/prompts.yml", + Path(__file__).resolve().parent + / "agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/prompts/prompts.yml", Path(__file__).resolve().parent / "agent/processes/v2/intent_router/routers/prompts.yml", ] ) @@ -72,6 +82,7 @@ class ModularApplication: _v2_evidence = DocsEvidenceAssembler() _v2_policy = V2RetrievalPolicyResolver() _doc_rules_enabled = os.getenv("V2_DOC_RULES_ENABLED", "true").strip().lower() in {"1", "true", "yes"} + _doc_update_workflow_version = os.getenv("V2_DOC_UPDATE_WORKFLOW_VERSION", "v2").strip().lower() or "v2" self.agent_sessions = InMemorySessionStore() self.agent_requests = InMemoryRequestStore() @@ -94,6 +105,7 @@ class ModularApplication: router=V2IntentRouter(llm=self._v2_llm), workflow_llm_enabled=True, doc_rules_enabled=_doc_rules_enabled, + doc_update_workflow_version=_doc_update_workflow_version, ), ] ) diff --git a/tests/pipeline_setup_v3/runtime/v2_process_adapter.py b/tests/pipeline_setup_v3/runtime/v2_process_adapter.py index 7d9dd55..7a337b4 100644 --- a/tests/pipeline_setup_v3/runtime/v2_process_adapter.py +++ b/tests/pipeline_setup_v3/runtime/v2_process_adapter.py @@ -250,6 +250,16 @@ def _build_v2_llm() -> AgentLlmService: / "src/app/core/agent/processes/v2/workflows/doc_explain_summary/steps/prompts/prompts.yml", Path(__file__).resolve().parents[3] / "src/app/core/agent/processes/v2/workflows/general_qa_summary/steps/prompts/prompts.yml", + Path(__file__).resolve().parents[3] + / "src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/steps/step5_execute_subprocesses/prompts/prompts.yml", + Path(__file__).resolve().parents[3] + / "src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step1_generate_frontmatter/prompts/prompts.yml", + Path(__file__).resolve().parents[3] + / "src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step1_generate_frontmatter/prompts/prompts.yml", + Path(__file__).resolve().parents[3] + / "src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/create_doc/steps/step2_generate_sections/prompts/prompts.yml", + Path(__file__).resolve().parents[3] + / "src/app/core/agent/processes/v2/workflows/doc_update_from_feature_v2/subprocesses/edit_doc/steps/step2_generate_sections/prompts/prompts.yml", Path(__file__).resolve().parents[3] / "src/app/core/agent/processes/v2/intent_router/routers/prompts.yml", ] return AgentLlmService(client=_build_client(), prompts=PromptLoader(prompt_paths)) diff --git a/tests/unit_tests/agent/test_doc_update_from_feature_v2_happy_path.py b/tests/unit_tests/agent/test_doc_update_from_feature_v2_happy_path.py new file mode 100644 index 0000000..395aceb --- /dev/null +++ b/tests/unit_tests/agent/test_doc_update_from_feature_v2_happy_path.py @@ -0,0 +1,236 @@ +from __future__ import annotations + +from types import SimpleNamespace + +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step3_parse_requirements.parser import ( + FunctionalRequirementsParser, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step4_prepare_tasks.services import ( + RequirementTaskBuilder, + RequirementTaskOrderer, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.steps.step5_execute_subprocesses.classifier import ( + DeleteIntentHeuristic, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.source_sections import ( + RequirementSourceSections, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.subprocesses.common.template_parser import ( + TemplateParser, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.context import ( + DocUpdateFromFeatureV2Context, +) +from app.core.agent.processes.v2.workflows.doc_update_from_feature_v2.workflow_runtime.models import ( + AnalyticsMeta, + RequirementUnit, +) + + +class _UnexpectedLlmCall: + def generate(self, *args, **kwargs): # noqa: ANN002, ANN003 + raise AssertionError("LLM should not be called for explicit happy-path metadata.") + + +def test_parser_reads_section_6_metadata_and_units() -> None: + parser = FunctionalRequirementsParser() + content = """ +analysis_id: AN-42 +application: loyalty +platform: web + +# 6. Функциональные требования +domain: billing +sub_domain: payments + +## 6.1 Экран статуса платежа +id: payment-status +doc_type: ui_page +application: loyalty +platform: pprb + +### Функциональные требования +- Показываем статус и ошибку. + +## 6.2 Метод получения статуса +id: get-payment-status +doc_type: api_method +application: loyalty +platform: ufs + +### Контракт метода +- GET /payments/status +""".strip() + + meta, units = parser.parse(content) + + assert meta.analysis_id == "AN-42" + assert meta.application == "loyalty" + assert meta.platform == "web" + assert meta.domain == "billing" + assert meta.subdomain == "payments" + assert [unit.section_key for unit in units] == ["6.1", "6.2"] + assert [unit.heading for unit in units] == ["Экран статуса платежа", "Метод получения статуса"] + assert units[0].metadata["doc_type"] == "ui_page" + assert "Показываем статус" in units[0].body + + +def test_parser_reads_backticked_root_metadata_format() -> None: + parser = FunctionalRequirementsParser() + content = """ +# 6. Описание изменений + +- `domain`: `orders` +- `sub_domain`: `list_read` + +## 6.1 Страница списка заказов +id: orders.ui.list +doc_type: ui_page +application: orders_web +platform: web + +### Требования к UI +- Таблица заказов. +""".strip() + + meta, units = parser.parse(content) + + assert meta.domain == "orders" + assert meta.subdomain == "list_read" + assert len(units) == 1 + assert units[0].metadata["doc_type"] == "ui_page" + + +def test_task_builder_orders_platforms_and_inherits_root_context() -> None: + context = DocUpdateFromFeatureV2Context( + runtime=SimpleNamespace(), + route=SimpleNamespace(), + rag_session_id="", + analytics_meta=AnalyticsMeta( + analysis_id="AN-42", + application="loyalty", + domain="billing", + subdomain="payments", + ), + requirements=[ + RequirementUnit( + section_key="6.3", + heading="WEB страница", + body="Описание", + metadata={"id": "web-page", "doc_type": "ui_page", "platform": "web", "op": "create"}, + ), + RequirementUnit( + section_key="6.1", + heading="PPRB страница", + body="Описание", + metadata={"id": "pprb-page", "doc_type": "ui_page", "platform": "pprb", "op": "create"}, + ), + RequirementUnit( + section_key="6.2", + heading="UFS метод", + body="Описание", + metadata={"id": "ufs-method", "doc_type": "api_method", "platform": "ufs", "op": "create"}, + ), + ], + ) + builder = RequirementTaskBuilder(_UnexpectedLlmCall()) + orderer = RequirementTaskOrderer() + + tasks = orderer.order(builder.build(context)) + + assert [task.platform for task in tasks] == ["pprb", "ufs", "web"] + assert [task.section_key for task in tasks] == ["6.1", "6.2", "6.3"] + assert all(task.application == "loyalty" for task in tasks) + assert all(task.domain == "billing" for task in tasks) + assert all(task.subdomain == "payments" for task in tasks) + assert tasks[0].path == "docs/billing/pprb/ui_page/pprb-page.md" + + +def test_task_builder_uses_create_when_path_is_new_and_no_delete_markers() -> None: + context = DocUpdateFromFeatureV2Context( + runtime=SimpleNamespace(), + route=SimpleNamespace(), + rag_session_id="", + analytics_meta=AnalyticsMeta( + application="orders_web", + domain="orders", + subdomain="list_read", + ), + requirements=[ + RequirementUnit( + section_key="6.1", + heading="Страница списка заказов", + body="### Требования к UI\n- Показывать таблицу заказов.", + metadata={ + "id": "orders.ui.list", + "doc_type": "ui_page", + "application": "orders_web", + "platform": "web", + }, + ) + ], + ) + + tasks = RequirementTaskBuilder(_UnexpectedLlmCall()).build(context) + + assert len(tasks) == 1 + assert tasks[0].action.value == "create" + assert tasks[0].path == "docs/orders/web/ui_page/orders.ui.list.md" + + +def test_delete_heuristic_does_not_match_phrase_ne_udalos() -> None: + heuristic = DeleteIntentHeuristic() + + assert heuristic.is_delete("Не удалось загрузить список заказов.") is False + assert heuristic.is_delete("Нужно удалить существующую страницу документации.") is True + + +def test_template_parser_extracts_ordered_sections_from_ui_template() -> None: + parser = TemplateParser() + template = """ +--- +doc_type: ui_page +--- + +# <title> + +## Summary +Правила оформления: `../common-elements/summary.md` + +## Details +Правила оформления: `../common-elements/details.md` + +### Требования к UI +Правила оформления: `../common-elements/ui-requirements.md` +""".strip() + + spec = parser.parse("templates/ui_page.template.md", template) + + assert spec.doc_type == "ui_page" + assert spec.title_level == 1 + assert [(item.level, item.title, item.rule_path) for item in spec.sections] == [ + (2, "Summary", "common-elements/summary.md"), + (2, "Details", "common-elements/details.md"), + (3, "Требования к UI", "common-elements/ui-requirements.md"), + ] + assert spec.sections[1].has_children is True + + +def test_requirement_source_sections_match_template_titles() -> None: + locator = RequirementSourceSections() + body = """ +### Технический use case (тезисно) +1. Открыть экран. + +### Контракт метода +| Поле | Тип | + +### Нефункциональные требования +- Метрики. +""".strip() + + sections = locator.extract(body) + + assert "открыть экран" not in sections + assert locator.find_for_title(sections, "Технический use case") == "1. Открыть экран." + assert "| Поле | Тип |" in locator.find_for_title(sections, "Контракт")