АДРЕСНЫЙ РЕЖИМ - авторан история - базовая версия + доп кля + конфиг дизапйна

This commit is contained in:
dctouch 2026-04-09 16:32:19 +03:00
parent edfa09c9af
commit 99288c195d
40 changed files with 5174 additions and 853 deletions

18
designconfig.ts Normal file
View File

@ -0,0 +1,18 @@
export const designConfig = {
colors: {
backgroundRgb: "18, 18, 18",
mainSurfaceRgb: "25, 25, 25",
horizontalSurfaceRgb: "30, 30, 30",
focusSurfaceRgb: "35, 35, 35",
activeRgb: "167, 59, 255",
activeTextRgb: "240, 240, 240",
textMainRgb: "240, 240, 240",
textMutedRgb: "166, 166, 166",
dangerRgb: "126, 126, 126",
scrollbarTrackRgb: "20, 20, 20",
scrollbarThumbRgb: "30, 30, 30",
scrollbarThumbHoverRgb: "30, 50, 30"
}
} as const;
export type DesignConfig = typeof designConfig;

View File

@ -0,0 +1,51 @@
# Assistant Behavior Canon
Schema version: `assistant_canon_v1`
Updated at: `2026-04-09`
## Mission
Assist users with 1C data analysis in read-only mode: accurate, honest, and useful.
## Core Rules
1. Never fabricate capabilities.
2. Never claim operational/admin actions in 1C.
3. Never expose internal technical pipeline details in user-facing replies.
4. Always separate:
- what is confirmed,
- what is inferred,
- what is unavailable.
5. For unsupported questions, provide a soft boundary and nearest useful supported action.
## Covered Case Behavior
1. Answer directly and concretely.
2. Keep response concise and business-oriented.
3. Mention period/entity assumptions only when they impact correctness.
## Partial Coverage Behavior
1. Explicitly state covered vs uncovered parts.
2. Ask only minimal clarifications required for correctness.
3. Propose the next best executable query.
## Out-of-Scope Behavior
1. Do not output raw errors or internal route/classifier terms.
2. Use plain language boundary:
- "I cannot perform this action directly."
3. Offer safe alternatives:
- how to inspect in 1C,
- what data can be checked right now.
## High-Risk Behavior
1. No destructive guidance.
2. No unsafe legal/financial certainty without data confirmation.
3. Prefer "confirmed data only" framing for factual claims.
## Capability Disclosure Behavior
1. Use 3-level disclosure:
- L1: capability groups,
- L2: operations in selected group,
- L3: exact actionable query.
2. Never dump full internal route catalog by default.
## Tone
1. Professional, calm, non-technical language.
2. Respectful boundary statements without refusal-only dead ends.

View File

@ -0,0 +1,193 @@
{
"schema_version": "capabilities_registry_v1",
"updated_at": "2026-04-09T00:00:00.000Z",
"assistant_mode": "read_only",
"groups": [
{
"group_code": "vat",
"group_title": "НДС",
"description": "Расчеты и аналитика по НДС на основании данных 1С.",
"risk_level": "high",
"maturity_status": "partial",
"supported_operations": [
"vat_period_snapshot",
"vat_payable_forecast",
"vat_turnover_breakdown"
],
"unsupported_operations": [
"submit_tax_declaration",
"legal_tax_advice_as_final"
],
"required_entities": [
"period",
"organization"
],
"optional_entities": [
"counterparty",
"account_scope"
],
"typical_queries": [
"Сколько НДС к уплате за период?",
"Покажи срез НДС на дату.",
"Почему НДС к уплате ноль?"
],
"related_routes": [
"address_vat_payable_forecast_v1"
],
"safe_alternatives": [
"Показать движения по счетам 68/19 за период",
"Показать сверочный срез по документам НДС"
],
"one_c_hints": [
"РегистрБухгалтерии.Хозрасчетный",
"Книга покупок/продаж"
]
},
{
"group_code": "counterparties",
"group_title": "Контрагенты",
"description": "Срезы активности, платежей и документов по контрагентам.",
"risk_level": "medium",
"maturity_status": "production_ready",
"supported_operations": [
"list_documents_by_counterparty",
"bank_operations_by_counterparty",
"list_contracts_by_counterparty"
],
"unsupported_operations": [
"edit_counterparty_card",
"merge_counterparties"
],
"required_entities": [
"counterparty_scope_or_contract"
],
"optional_entities": [
"period",
"organization"
],
"typical_queries": [
"Покажи документы по контрагенту.",
"Какие операции по банку были с контрагентом?",
"Какие договоры есть у контрагента?"
],
"related_routes": [
"address_documents_by_counterparty_v1",
"address_bank_operations_by_counterparty_v1"
],
"safe_alternatives": [
"Ограничить период и организацию",
"Уточнить ИНН/наименование контрагента"
],
"one_c_hints": [
"Справочник.Контрагенты",
"Документы расчетов"
]
},
{
"group_code": "settlements",
"group_title": "Задолженности и расчеты",
"description": "Аналитика закрытия расчетов, сальдо и признаков незакрытых цепочек.",
"risk_level": "medium",
"maturity_status": "production_ready",
"supported_operations": [
"settlement_closure_state",
"advance_offset_state",
"open_items_snapshot"
],
"unsupported_operations": [
"force_close_settlements",
"writeoff_execution"
],
"required_entities": [
"period",
"account_scope"
],
"optional_entities": [
"counterparty",
"contract"
],
"typical_queries": [
"Закрылись ли расчеты по счету 60/62?",
"Есть ли незакрытые авансы?",
"Покажи незакрытые договоры."
],
"related_routes": [
"prove_settlement_closure_state"
],
"safe_alternatives": [
"Уточнить контрагента",
"Уточнить договор/объект расчетов"
],
"one_c_hints": [
"Счета 60, 62, 76",
"Регистры взаиморасчетов"
]
},
{
"group_code": "cash_and_balances",
"group_title": "Деньги и остатки",
"description": "Остатки и динамика по денежным счетам и кассе.",
"risk_level": "medium",
"maturity_status": "partial",
"supported_operations": [
"balance_snapshot",
"turnover_by_period"
],
"unsupported_operations": [
"payment_execution",
"bank_statement_import"
],
"required_entities": [
"period"
],
"optional_entities": [
"account_scope",
"organization"
],
"typical_queries": [
"Какой остаток по счету 51 на дату?",
"Покажи движение денег за месяц."
],
"related_routes": [
"address_balance_snapshot_v1"
],
"safe_alternatives": [
"Уточнить счет и период",
"Показать обороты вместо итоговой суммы"
],
"one_c_hints": [
"Счет 50, 51, 52, 55"
]
},
{
"group_code": "capability_boundaries",
"group_title": "Ограничения",
"description": "Операции, которые ассистент не выполняет в этом рантайме.",
"risk_level": "high",
"maturity_status": "production_ready",
"supported_operations": [
"explain_boundary",
"suggest_safe_next_step"
],
"unsupported_operations": [
"configure_1c",
"admin_server_actions",
"create_or_post_documents",
"destructive_database_actions"
],
"required_entities": [],
"optional_entities": [],
"typical_queries": [
"Можешь настроить 1С?",
"Можешь удалить базу?",
"Можешь подготовить и провести документ?"
],
"related_routes": [],
"safe_alternatives": [
"Дать безопасный диагностический план для 1С/ИТ-админа",
"Подсказать точный запрос к данным в read-only"
],
"one_c_hints": []
}
]
}

View File

@ -0,0 +1,567 @@
Да, тут уже напрашивается не просто “ещё одно поле в автопрогонах”, а **нормальная управляющая схема**.
То есть у вас должно быть не только “модель ответила / оценка 5-балльная”, а три опоры:
1. **эталон идеального поведения ассистента**;
2. **ручная разметка результата прогона с управленческим смыслом**;
3. **канонический файл возможностей ассистента по отработанным маршрутам 1С**.
И тогда автопрогоны перестают быть просто логами, а становятся контуром развития системы.
Ниже я собрал это так, чтобы можно было почти целиком отдать в Codex.
---
# Как я бы это сформулировал концептуально
## 1. Нужен отдельный блок: «Эталон поведения ассистента»
Это не просто описание “каким хотелось бы видеть ответ”.
Это должен быть **формальный канон**, который понимают:
* сам ассистент;
* система автопрогонов;
* пост-анализ;
* Codex, который потом дорабатывает маршруты и поведение.
То есть это не prose-блок “идеальная работа”, а именно **Assistant Canon / Behavior Canon**.
### Что в нём должно быть
#### А. Что такое хороший ответ
Хороший ответ ассистента:
* отвечает по существу, если кейс реально покрыт;
* не врёт, если кейс не покрыт;
* не выдаёт технические внутренности вместо нормальной коммуникации;
* не ломается на смежных вопросах;
* умеет мягко ограничить себя;
* умеет предложить близкий поддерживаемый сценарий;
* умеет подсказать, где это обычно смотреть в 1С;
* не вываливает полный список возможностей без запроса;
* раскрывает возможности по группам и по мере уточнения.
#### Б. Что такое плохой ответ
Плохой ответ ассистента:
* выдумывает функцию, которой нет;
* делает вид, что может точно ответить, когда не может;
* отвечает внутренним техническим языком;
* сухо отказывает без пользы;
* валит пользователя в огромный список умений;
* не понимает, когда вопрос надо передать в “не покрыто, но рядом”;
* не различает безопасный общий ответ и рискованный прикладной совет.
#### В. Какой идеал поведения на границе покрытия
Вот это вообще ключевой блок.
Ассистент должен:
* честно понимать границу своих возможностей;
* не маскировать отсутствие маршрута;
* не ломаться;
* не отвечать “не поддерживается” в лоб;
* не уходить в системные сообщения;
* давать человекочитаемое ограничение;
* предлагать ближайший полезный путь.
Это и есть ваш **эталон**.
---
## 2. Нужна ручная разметка не только по качеству, но и по судьбе вопроса
Вот это очень сильная мысль.
Пятибалльная оценка сама по себе почти бесполезна, потому что она **не говорит, что делать дальше**.
Нужна ещё одна сущность:
## **Decision Markup / Route Decision Markup**
То есть по каждому прогону вы размечаете не только “норм / не норм”, а **каково управленческое решение по классу вопроса**.
### Я бы добавил в UI не просто кнопку, а выпадающий классификатор
Например:
* `covered_ok` — кейс нормальный, покрывается, поведение ок;
* `covered_but_bad_answer` — кейс должен покрываться, но ответ плохой;
* `good_question_to_implement` — хороший вопрос, его надо брать в отработку;
* `out_of_scope_but_answer_softly` — вопрос не планируется покрывать, но нужно мягко и полезно отвечать;
* `unsafe_question_limit_strictly` — вопрос рискованный, на него нужно отвечать осторожно и ограниченно;
* `bad_test_case` — сам тестовый вопрос мусорный / нерелевантный;
* `needs_routing_extension` — нужен новый маршрут или расширение маршрутизации;
* `needs_capability_registry_update` — кейс выявил дыру в файле возможностей;
* `needs_dialog_policy_fix` — маршрут, возможно, не нужен, но политика ответа плохая.
Вот это уже даст системе смысл.
### Если хочется совсем по-простому
Можно ввести более короткий список:
* **Отрабатывается**
* **Должно отрабатываться**
* **Не будет отрабатываться**
* **Отвечать мягким ограничением**
* **Высокорисковый вопрос**
* **Плохой тест-кейс**
Но я бы всё-таки оставил более инженерный набор, а в UI уже сделал человекочитаемые названия.
---
## 3. Нужен канонический файл возможностей ассистента
Да, это обязательно. И это должен быть не просто текстовый файл “что умеем”.
Это должен быть **Capabilities Registry / Supported Routes Registry**.
И он должен быть источником истины для трёх вещей:
* ответа ассистента;
* классификации покрываемости;
* автопрогонов и анализа.
### Что там должно быть
Для каждого маршрута / домена:
* код маршрута;
* человекочитаемая группа;
* краткое описание;
* что реально умеется;
* что не умеется;
* обязательные параметры;
* типовые формулировки вопросов;
* похожие смежные сценарии;
* безопасные альтернативы;
* подсказка, где это обычно смотреть в 1С;
* уровень риска;
* статус зрелости:
* production-ready
* partial
* planned
* deprecated
### Пример групп
Не надо сразу показывать всё пользователю.
Надо хранить глубоко, а наружу отдавать по группам.
Например:
* НДС
* Контрагенты
* Задолженности
* Деньги и остатки
* Платежи и движения
* Аналитика по периодам
* Справочные бухгалтерские вопросы
А дальше уже внутри группы:
* что умеется конкретно.
То есть если юзер спрашивает “что ты можешь по НДС?”, ассистент отвечает не списком из 80 пунктов, а компактной группой возможностей по НДС.
---
## 4. Надо отдельно зафиксировать правило раскрытия возможностей
Это тоже очень важный продуктовый момент.
### Ассистент не должен
* автоматически вываливать весь список поддерживаемого;
* отвечать каталогом без запроса;
* перегружать пользователя техническими деталями маршрутов.
### Ассистент должен
* раскрывать возможности **по группам**;
* сначала давать верхнеуровневую сегментацию;
* при уточнении — углубляться;
* говорить в продуктовой, а не внутренне-технической логике.
То есть:
“Могу помочь с НДС, остатками и движением денег, контрагентами и задолженностями, а также с частью аналитики по периодам. Если хочешь, могу уточнить отдельно по любому из этих блоков.”
А уже потом:
“По НДС могу показать суммы, динамику по периодам, сверку по организации, сравнительные разрезы...”
---
## 5. Нужен отдельный тип разметки: «вопрос хороший, но ещё не покрыт»
Это, по сути, мост между автопрогоном и roadmap.
То есть если в прогоне всплыл вопрос:
* он адекватный;
* он реально нужен;
* пользователь его точно задаст;
* сейчас он не покрыт,
то это не просто “ответ плохой”.
Это **кандидат на новый маршрут / на расширение текущего покрытия**.
Поэтому в ручной разметке должен быть отдельный флаг:
* `candidate_for_implementation`
или
* `planned_route_gap`
Именно его потом должен видеть Codex и дальше использовать как список задач на развитие.
---
## 6. Надо разделить две разные проблемы
Сейчас у тебя в одном описании смешаны две вещи, а их лучше развести.
### Проблема 1. Функциональное покрытие
Что система реально умеет по данным и маршрутам.
### Проблема 2. Поведенческая зрелость
Как система ведёт себя, когда вопрос:
* вне покрытия;
* частично в покрытии;
* опасный;
* слишком общий;
* смежный;
* абстрактный.
То есть даже если маршрут не реализован, поведение всё равно может быть:
* хорошим;
* плохим;
* опасным;
* слишком техническим;
* бесполезным.
И автопрогоны должны это различать.
---
# Ниже — готовая мини-ТЗшка для Codex
## Мини-ТЗ: эталон поведения ассистента, ручная разметка прогонов и канонический файл возможностей
### Цель
Доработать систему автопрогонов бухгалтерского ассистента так, чтобы она оценивала не только качество конкретного ответа, но и соответствие эталонному поведению ассистента, а также позволяла вручную размечать судьбу вопроса: покрывается, должен быть отработан, не будет отрабатываться, должен обрабатываться мягким ограничением, требует нового маршрута или требует доработки политики ответа.
---
## 1. Ввести отдельную сущность: Assistant Behavior Canon
Нужен канонический блок, описывающий эталонную работу ассистента.
### Требования
Создать отдельную структуру/файл, который будет использоваться:
* в логике ответа ассистента;
* в пост-анализе автопрогонов;
* в интерфейсе разметки прогонов;
* в дальнейшей работе Codex по развитию маршрутов и поведения.
### В Assistant Behavior Canon зафиксировать:
#### 1.1. Поведение на покрытых кейсах
* отвечать уверенно и по существу;
* не уходить в лишние оговорки;
* не использовать технические внутренние формулировки.
#### 1.2. Поведение на частично покрытых кейсах
* явно разделять, что ассистент может сделать, а что нет;
* не маскировать ограничения;
* предлагать полезное продолжение.
#### 1.3. Поведение на непокрытых, но близких кейсах
* не выдумывать поддержку функциональности;
* мягко и по-человечески объяснять ограничение;
* предлагать ближайший поддерживаемый сценарий;
* при уместности подсказывать, где это обычно посмотреть в 1С.
#### 1.4. Поведение на высокорисковых вопросах
* не выдавать неподтверждённые рекомендации как надёжный ответ;
* не делать вид, что прикладная логика существует, если её нет;
* сохранять полезность без ложной уверенности.
#### 1.5. Поведение при вопросе “что ты умеешь”
* не вываливать весь список возможностей сразу;
* раскрывать возможности по крупным группам;
* углубляться только после уточнения;
* использовать человекочитаемые продуктовые группы, а не внутренние названия маршрутов.
---
## 2. Ввести канонический файл возможностей ассистента
Нужен отдельный реестр отработанных возможностей ассистента по маршрутам 1С.
### Назначение
Этот файл является источником истины для:
* определения покрытия вопроса;
* ответа ассистента на вопросы о своих возможностях;
* similarity-логики;
* автопрогонов и пост-анализа;
* Codex при дальнейшем развитии маршрутов.
### Для каждого маршрута / домена хранить:
* `route_code`
* `group_code`
* `group_title`
* `title`
* `description`
* `supported_operations`
* `unsupported_operations`
* `required_entities`
* `optional_entities`
* `typical_queries`
* `related_routes`
* `safe_alternatives`
* `one_c_hints`
* `risk_level`
* `maturity_status` (`production_ready`, `partial`, `planned`, `deprecated`)
### Требования к пользовательскому раскрытию возможностей
Ассистент должен уметь:
* сначала показывать верхнеуровневые группы;
* по запросу раскрывать детали внутри выбранной группы;
* не использовать длинные технические перечни без необходимости.
---
## 3. Доработать интерфейс автопрогонов: ручная управленческая разметка
Существующую 5-балльную оценку оставить, но дополнить отдельной выпадающей ручной классификацией результата прогона.
### Добавить новое поле:
`manual_case_decision`
### Возможные значения:
* `covered_ok`
* `covered_but_bad_answer`
* `candidate_for_implementation`
* `needs_routing_extension`
* `out_of_scope_but_answer_softly`
* `unsafe_question_limit_strictly`
* `needs_dialog_policy_fix`
* `needs_capability_registry_update`
* `bad_test_case`
### Смысл значений
* `covered_ok` — кейс уже покрыт, поведение нормальное;
* `covered_but_bad_answer` — кейс покрывается, но ответ/диалог плохой;
* `candidate_for_implementation` — хороший пользовательский кейс, которого пока нет, его стоит брать в разработку;
* `needs_routing_extension` — нужен новый маршрут или расширение существующего;
* `out_of_scope_but_answer_softly` — кейс не планируется покрывать, но нужен качественный мягкий ответ без техничности;
* `unsafe_question_limit_strictly` — кейс относится к рискованным, и ассистент должен ограничивать себя особенно строго;
* `needs_dialog_policy_fix` — проблема не в маршруте, а в стиле/логике ответа;
* `needs_capability_registry_update` — реестр возможностей неактуален или недостаточно формализован;
* `bad_test_case` — вопрос мусорный, нерелевантный или бесполезный для развития системы.
### Дополнительно
Для каждой ручной метки предусмотреть:
* короткий комментарий;
* автора разметки;
* timestamp;
* возможность использовать эту разметку в пост-анализе и отборе задач для Codex.
---
## 4. Доработать логику пост-анализа прогонов
После прогона система должна уметь отделять:
* ошибки покрытия;
* ошибки маршрутизации;
* ошибки политики ответа;
* хорошие, но ещё не покрытые кейсы;
* мусорные тест-кейсы;
* высокорисковые кейсы;
* кейсы на обновление файла возможностей.
### На выходе пост-анализа нужны агрегаты:
* список кейсов на доработку маршрутов;
* список кейсов на доработку policy;
* список кейсов на обновление capabilities registry;
* список кейсов, которые сознательно не будут покрываться, но требуют мягкого ограничения;
* список кейсов, пригодных для новых regression suites.
---
## 5. Встроить связь между ручной разметкой и дальнейшей работой Codex
Codex должен видеть не только сам диалог и оценку, но и управленческое решение по нему.
### Требование
При выгрузке данных для дальнейшего анализа и доработок обязательно передавать:
* question / dialog trace;
* current route / current coverage decision;
* 5-балльную оценку;
* `manual_case_decision`;
* комментарий аналитика;
* ссылку на ближайший домен из capabilities registry;
* признак: нужно ли брать кейс в маршрутную отработку.
### Ожидаемое поведение
Если кейс помечен как:
* `candidate_for_implementation` или `needs_routing_extension` — Codex рассматривает его как материал для новой/расширенной маршрутной логики;
* `out_of_scope_but_answer_softly` — Codex улучшает не маршруты, а policy-слой ответа;
* `needs_capability_registry_update` — Codex актуализирует реестр возможностей;
* `unsafe_question_limit_strictly` — Codex усиливает безопасное поведение и ограничения;
* `covered_but_bad_answer` — Codex чинит существующий покрываемый сценарий, а не создаёт новый.
---
## 6. Добавить в эталон обязательное правило минимизации технических ответов
Это отдельное критичное требование.
### Ассистент не должен
* отвечать внутренними техническими терминами;
* ссылаться на отсутствие маршрута, домена, интента, пайплайна, классификатора;
* создавать ощущение поломки системы.
### Ассистент должен
* говорить естественно;
* объяснять ограничения человеческим языком;
* сохранять полезность даже при отказе;
* ориентировать пользователя в доступных соседних возможностях.
---
## 7. Добавить в эталон обязательное правило сегментированного раскрытия возможностей
Если пользователь спрашивает:
* “что ты умеешь?”
* “что можешь по НДС?”
* “что можешь по остаткам?”
* “что умеешь по деньгам / поставщикам / задолженностям?”
ассистент должен отвечать через иерархию:
### Уровень 1
Крупные продуктовые группы:
* НДС
* Контрагенты
* Задолженности
* Деньги и остатки
* Движение и платежи
* Аналитика по периодам
* Справочные бухгалтерские вопросы
### Уровень 2
Уточнение по выбранной группе:
* что внутри группы реально доступно;
* какие ограничения есть;
* что можно сделать следующим шагом.
### Уровень 3
Точечный ответ по конкретному запросу.
---
## 8. Критерии приёмки
1. В системе появился отдельный канонический блок эталонного поведения ассистента.
2. Появился отдельный файл/реестр возможностей по маршрутам 1С.
3. В UI автопрогонов добавлена ручная управленческая разметка результата.
4. Ручная разметка сохраняется в логах и участвует в пост-анализе.
5. Система умеет отделять “не покрыто, но стоит реализовать” от “не покрыто и не планируется, но нужно мягко отвечать”.
6. Ассистент перестаёт отвечать внутренним техническим языком на границе покрытия.
7. Ассистент умеет раскрывать свои возможности по группам, а не полным списком.
8. Codex получает достаточно данных, чтобы понимать, что чинить: маршрут, policy, capabilities registry или сам тест-кейс.
---
# Что я бы ещё добавил от себя
Я бы прямо выделил в ТЗ отдельный артефакт:
## `assistant_canon.md`
В нём:
* идеал поведения;
* анти-паттерны;
* примеры хороших ответов;
* примеры плохих ответов;
* правила раскрытия возможностей;
* правила мягкого ограничения;
* правила поведения на рискованных вопросах.
И отдельно:
## `capabilities_registry.json`
или `capabilities_registry.yaml`
И ещё:
## `manual_case_decision_schema.json`
Чтобы UI и пост-анализ работали по одному словарю значений.
---
# Если совсем коротко, в чём суть
Тебе сейчас нужен не просто “ещё один контрол в модалке”, а вот такая конструкция:
**Эталон ассистента**
→ задаёт идеальное поведение
**Файл возможностей**
→ задаёт фактическое покрытие
**Ручная разметка кейса**
→ задаёт управленческое решение, что с этим вопросом делать дальше
**Codex**
→ уже понимает, нужно ли:
* чинить ответ,
* расширять маршрут,
* обновлять capabilities,
* улучшать мягкий отказ,
* или вообще выкинуть тест-кейс.
Если хочешь, я следующим сообщением могу собрать это ещё в более прикладной форме: **короткое ТЗ на 3040 строк для прямой отправки в Codex**, без пояснений и лирики.

View File

@ -0,0 +1,39 @@
{
"schema_version": "manual_case_decision_schema_v1",
"updated_at": "2026-04-09T00:00:00.000Z",
"title": "Manual Case Decision Schema",
"description": "Management decision for assistant auto-run case annotation.",
"enum": [
"covered_ok",
"covered_but_bad_answer",
"candidate_for_implementation",
"needs_routing_extension",
"out_of_scope_but_answer_softly",
"unsafe_question_limit_strictly",
"needs_dialog_policy_fix",
"needs_capability_registry_update",
"bad_test_case"
],
"labels": {
"covered_ok": "Покрыто и ок",
"covered_but_bad_answer": "Покрыто, но ответ плохой",
"candidate_for_implementation": "Кандидат на внедрение",
"needs_routing_extension": "Нужно расширение маршрутизации",
"out_of_scope_but_answer_softly": "Вне скоупа, но нужен мягкий ответ",
"unsafe_question_limit_strictly": "Высокий риск, строгие ограничения",
"needs_dialog_policy_fix": "Нужен фикс диалоговой политики",
"needs_capability_registry_update": "Нужно обновить реестр возможностей",
"bad_test_case": "Плохой тест-кейс"
},
"queue_mapping": {
"covered_ok": "none",
"covered_but_bad_answer": "policy_fix",
"candidate_for_implementation": "routing_extension",
"needs_routing_extension": "routing_extension",
"out_of_scope_but_answer_softly": "soft_boundary",
"unsafe_question_limit_strictly": "safety_policy",
"needs_dialog_policy_fix": "policy_fix",
"needs_capability_registry_update": "capability_registry",
"bad_test_case": "testset_hygiene"
}
}

View File

@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod }; return (mod && mod.__esModule) ? mod : { "default": mod };
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0; exports.ARCH_EXPORT_2020_DIR = exports.SCHEMAS_DIR = exports.EVAL_DATASETS_DIR = exports.REPORTS_DIR = exports.PROMPTS_DIR = exports.AUTORUN_GENERATOR_HISTORY_FILE = exports.AUTORUN_GENERATOR_DIR = exports.AUTORUN_ANNOTATIONS_FILE = exports.AUTORUN_ANNOTATIONS_DIR = exports.ASSISTANT_SESSIONS_DIR = exports.EVAL_CASES_DIR = exports.PRESETS_DIR = exports.TRACES_DIR = exports.DATA_DIR = exports.VAT_PAYABLE_19_PREFIXES = exports.VAT_PAYABLE_68_PREFIXES = exports.ASSISTANT_MCP_LIVE_LIMIT = exports.ASSISTANT_MCP_TIMEOUT_MS = exports.ASSISTANT_MCP_CHANNEL = exports.ASSISTANT_MCP_PROXY_URL = exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LIVE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_LLM_PREDECOMPOSE_V1 = exports.FEATURE_ASSISTANT_ADDRESS_QUERY_V1 = exports.FEATURE_ASSISTANT_MCP_RUNTIME_V1 = exports.FEATURE_ASSISTANT_GRAPH_RUNTIME_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_ANSWER_V1 = exports.FEATURE_ASSISTANT_LIFECYCLE_RUNTIME_V1 = exports.FEATURE_ASSISTANT_STAGE2_EVAL_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNIT_CONTINUITY_V1 = exports.FEATURE_ASSISTANT_PROBLEM_CENTRIC_ANSWER_V1 = exports.FEATURE_ASSISTANT_PROBLEM_UNITS_V1 = exports.FEATURE_ASSISTANT_ACCOUNTANT_EVAL_V1 = exports.FEATURE_ASSISTANT_ANSWER_POLICY_V11 = exports.FEATURE_ASSISTANT_ANTI_GENERIC_RANKING_GUARD_V1 = exports.FEATURE_ASSISTANT_MIN_EVIDENCE_GATE_V1 = exports.FEATURE_ASSISTANT_BROAD_GUARD_V1 = exports.FEATURE_ASSISTANT_EVIDENCE_ENRICHMENT_V1 = exports.FEATURE_ASSISTANT_STATE_FOLLOWUP_BINDING_V1 = exports.FEATURE_ASSISTANT_CONTRACTS_V11 = exports.FEATURE_ASSISTANT_INVESTIGATION_STATE_V1 = exports.DEFAULT_PROMPT_VERSION = exports.DEFAULT_MAX_OUTPUT_TOKENS = exports.DEFAULT_TEMPERATURE = exports.DEFAULT_MODEL = exports.DEFAULT_OPENAI_BASE_URL = exports.TIMEZONE = exports.PORT = exports.MODULE_ROOT = exports.BACKEND_ROOT = void 0;
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = exports.ASSISTANT_CANON_FILE = void 0;
const path_1 = __importDefault(require("path")); const path_1 = __importDefault(require("path"));
exports.BACKEND_ROOT = path_1.default.resolve(__dirname, ".."); exports.BACKEND_ROOT = path_1.default.resolve(__dirname, "..");
exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, ".."); exports.MODULE_ROOT = path_1.default.resolve(exports.BACKEND_ROOT, "..");
@ -71,8 +72,15 @@ exports.TRACES_DIR = path_1.default.resolve(exports.DATA_DIR, "traces");
exports.PRESETS_DIR = path_1.default.resolve(exports.DATA_DIR, "presets"); exports.PRESETS_DIR = path_1.default.resolve(exports.DATA_DIR, "presets");
exports.EVAL_CASES_DIR = path_1.default.resolve(exports.DATA_DIR, "eval_cases"); exports.EVAL_CASES_DIR = path_1.default.resolve(exports.DATA_DIR, "eval_cases");
exports.ASSISTANT_SESSIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "assistant_sessions"); exports.ASSISTANT_SESSIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "assistant_sessions");
exports.AUTORUN_ANNOTATIONS_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_annotations");
exports.AUTORUN_ANNOTATIONS_FILE = path_1.default.resolve(exports.AUTORUN_ANNOTATIONS_DIR, "annotations.json");
exports.AUTORUN_GENERATOR_DIR = path_1.default.resolve(exports.DATA_DIR, "autorun_generators");
exports.AUTORUN_GENERATOR_HISTORY_FILE = path_1.default.resolve(exports.AUTORUN_GENERATOR_DIR, "history.json");
exports.PROMPTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "prompts"); exports.PROMPTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "prompts");
exports.REPORTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "reports"); exports.REPORTS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "reports");
exports.EVAL_DATASETS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "eval_cases"); exports.EVAL_DATASETS_DIR = path_1.default.resolve(exports.MODULE_ROOT, "eval_cases");
exports.SCHEMAS_DIR = path_1.default.resolve(exports.BACKEND_ROOT, "src", "schemas"); exports.SCHEMAS_DIR = path_1.default.resolve(exports.BACKEND_ROOT, "src", "schemas");
exports.ARCH_EXPORT_2020_DIR = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт"); exports.ARCH_EXPORT_2020_DIR = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт");
exports.ASSISTANT_CANON_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "assistant_canon.md");
exports.ASSISTANT_CAPABILITIES_REGISTRY_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "capabilities_registry.json");
exports.MANUAL_CASE_DECISION_SCHEMA_FILE = path_1.default.resolve(exports.MODULE_ROOT, "..", "docs", "TECH", "manual_case_decision_schema.json");

View File

@ -9,6 +9,29 @@ const path_1 = __importDefault(require("path"));
const express_1 = require("express"); const express_1 = require("express");
const config_1 = require("../config"); const config_1 = require("../config");
const http_1 = require("../utils/http"); const http_1 = require("../utils/http");
const capabilitiesRegistry_1 = require("../services/capabilitiesRegistry");
const MANUAL_CASE_DECISIONS = [
"covered_ok",
"covered_but_bad_answer",
"candidate_for_implementation",
"needs_routing_extension",
"out_of_scope_but_answer_softly",
"unsafe_question_limit_strictly",
"needs_dialog_policy_fix",
"needs_capability_registry_update",
"bad_test_case"
];
const DECISION_QUEUE_MAP = {
covered_ok: "none",
covered_but_bad_answer: "policy_fix",
candidate_for_implementation: "routing_extension",
needs_routing_extension: "routing_extension",
out_of_scope_but_answer_softly: "soft_boundary",
unsafe_question_limit_strictly: "safety_policy",
needs_dialog_policy_fix: "policy_fix",
needs_capability_registry_update: "capability_registry",
bad_test_case: "testset_hygiene"
};
function toRecord(value) { function toRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) { if (!value || typeof value !== "object" || Array.isArray(value)) {
return null; return null;
@ -67,6 +90,304 @@ function clampInt(value, min, max, fallback) {
return max; return max;
return rounded; return rounded;
} }
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
const normalized = toStringSafe(value);
if (!normalized)
return fallback;
return (MANUAL_CASE_DECISIONS.includes(normalized) ? normalized : fallback);
}
function parseAnnotationAuthor(value) {
const author = toStringSafe(value);
if (!author)
return null;
return author.slice(0, 80);
}
function readManualDecisionSchema() {
const fallback = {
schema_version: "manual_case_decision_schema_v1_fallback",
enum: MANUAL_CASE_DECISIONS,
labels: {
covered_ok: "Покрыто и ок",
covered_but_bad_answer: "Покрыто, но ответ плохой",
candidate_for_implementation: "Кандидат на внедрение",
needs_routing_extension: "Нужно расширение маршрутизации",
out_of_scope_but_answer_softly: "Вне скоупа, но нужен мягкий ответ",
unsafe_question_limit_strictly: "Высокий риск, строгие ограничения",
needs_dialog_policy_fix: "Нужен фикс диалоговой политики",
needs_capability_registry_update: "Нужно обновить реестр возможностей",
bad_test_case: "Плохой тест-кейс"
},
queue_mapping: DECISION_QUEUE_MAP
};
if (!fs_1.default.existsSync(config_1.MANUAL_CASE_DECISION_SCHEMA_FILE)) {
return fallback;
}
try {
const parsed = JSON.parse(fs_1.default.readFileSync(config_1.MANUAL_CASE_DECISION_SCHEMA_FILE, "utf-8"));
const record = toRecord(parsed);
return record ?? fallback;
}
catch {
return fallback;
}
}
function readAutoGenHistory() {
if (!fs_1.default.existsSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE))
return [];
try {
const parsed = JSON.parse(fs_1.default.readFileSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE, "utf-8"));
if (!Array.isArray(parsed))
return [];
return parsed
.map((item) => toRecord(item))
.filter((item) => item !== null)
.map((item) => ({
generation_id: toStringSafe(item.generation_id) ?? "",
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
mode: toStringSafe(item.mode) ?? "codex_creative",
count: clampInt(toNumberSafe(item.count), 1, 300, 20),
domain: toStringSafe(item.domain),
questions: toArray(item.questions)
.map((q) => toStringSafe(q))
.filter((q) => q !== null)
.slice(0, 500),
generated_by: toStringSafe(item.generated_by),
saved_case_set_file: toStringSafe(item.saved_case_set_file),
context: toRecord(item.context)
? {
llm_provider: toStringSafe(toRecord(item.context)?.llm_provider),
model: toStringSafe(toRecord(item.context)?.model),
assistant_prompt_version: toStringSafe(toRecord(item.context)?.assistant_prompt_version),
decomposition_prompt_version: toStringSafe(toRecord(item.context)?.decomposition_prompt_version),
prompt_fingerprint: toStringSafe(toRecord(item.context)?.prompt_fingerprint)
}
: null
}))
.filter((item) => item.generation_id.length > 0)
.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
}
catch {
return [];
}
}
function writeAutoGenHistory(records) {
const dir = path_1.default.dirname(config_1.AUTORUN_GENERATOR_HISTORY_FILE);
if (!fs_1.default.existsSync(dir)) {
fs_1.default.mkdirSync(dir, { recursive: true });
}
fs_1.default.writeFileSync(config_1.AUTORUN_GENERATOR_HISTORY_FILE, JSON.stringify(records, null, 2), "utf-8");
}
function readEvalDatasetCases(filePath) {
try {
const parsed = JSON.parse(fs_1.default.readFileSync(filePath, "utf-8"));
if (Array.isArray(parsed)) {
return parsed.map((item) => toRecord(item)).filter((item) => item !== null);
}
const record = toRecord(parsed);
if (!record)
return [];
const cases = toArray(record.cases).map((item) => toRecord(item)).filter((item) => item !== null);
return cases;
}
catch {
return [];
}
}
function collectCanonicalQuestions(limit = 300) {
if (!fs_1.default.existsSync(config_1.EVAL_DATASETS_DIR)) {
return [];
}
const entries = fs_1.default.readdirSync(config_1.EVAL_DATASETS_DIR, { withFileTypes: true });
const questions = [];
for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith(".json"))
continue;
const fullPath = path_1.default.resolve(config_1.EVAL_DATASETS_DIR, entry.name);
const cases = readEvalDatasetCases(fullPath);
for (const testCase of cases) {
const rawQuestion = toStringSafe(testCase.raw_question) ?? toStringSafe(testCase.user_message) ?? toStringSafe(testCase.query);
if (rawQuestion) {
questions.push(rawQuestion);
}
}
}
return Array.from(new Set(questions)).slice(0, limit);
}
function normalizeDomainHint(value) {
const domain = toStringSafe(value);
if (!domain)
return null;
return domain.toLowerCase();
}
function fallbackDomainTemplates(domain) {
if (domain?.includes("vat") || domain?.includes("ндс")) {
return [
"Сколько НДС к уплате на дату по организации?",
"Покажи прогноз НДС за период по организации.",
"Почему по НДС сейчас ноль и из чего сложился расчет?"
];
}
if (domain?.includes("counter") || domain?.includes("контраг")) {
return [
"Покажи топ контрагентов по сумме платежей за период.",
"Какой самый крупный договор у выбранной организации?",
"Какие документы были по контрагенту за весь период?"
];
}
if (domain?.includes("settlement") || domain?.includes("задолж") || domain?.includes("расчет")) {
return [
"Какие незакрытые расчеты висят на конец периода?",
"Есть ли незакрытые авансы по поставщикам?",
"Покажи цепочки закрытия по счетам 60/62."
];
}
return [
"С какой организацией сейчас можно работать в активном контуре?",
"Покажи ключевые операции за выбранный период.",
"Какие вопросы по этому домену ассистент поддерживает прямо сейчас?"
];
}
function mutateIntoQwenStyle(base, index) {
const wrappers = ["йо ", "слушай ", "подскажи плиз ", "короче ", "мож ", "а ну-ка "];
const tails = ["", " без воды", " по факту", " и коротко", " прям сейчас", " за весь период"];
const typoMap = [
[/\bкомпания\b/gi, "компиния"],
[/\bсейчас\b/gi, "щас"],
[/\bпожалуйста\b/gi, "плиз"],
[/\bкакая\b/gi, "кака"],
[/\bчто\b/gi, "че"]
];
const prefix = wrappers[index % wrappers.length];
const tail = tails[index % tails.length];
let text = `${prefix}${base}${tail}`.trim();
if (index % 2 === 0) {
const [pattern, replacement] = typoMap[index % typoMap.length];
text = text.replace(pattern, replacement);
}
return text;
}
function generateQwenSeedQuestions(count, domain) {
const seed = collectCanonicalQuestions(450);
const source = seed.length > 0 ? seed : fallbackDomainTemplates(domain);
const filtered = domain
? source.filter((item) => item.toLowerCase().includes(domain) || fallbackDomainTemplates(domain).includes(item))
: source;
const bag = filtered.length > 0 ? filtered : source;
const out = [];
for (let index = 0; index < count; index += 1) {
const base = bag[index % bag.length];
out.push(mutateIntoQwenStyle(base, index));
}
return Array.from(new Set(out)).slice(0, count);
}
function generateCodexCreativeQuestions(count, domain) {
const domainTemplates = fallbackDomainTemplates(domain);
const patterns = [
"Дай бизнес-срез по состоянию на дату: {q}",
"Нужен аккуратный ответ как бухгалтеру: {q}",
"Если данных не хватает, скажи что уточнить, но сначала попробуй: {q}",
"Сформулируй результат без технички и с шагом дальше: {q}",
"Проверь в read-only и скажи что видно: {q}"
];
const out = [];
for (let index = 0; index < count; index += 1) {
const base = domainTemplates[index % domainTemplates.length];
const pattern = patterns[index % patterns.length];
out.push(pattern.replace("{q}", base));
}
return Array.from(new Set(out)).slice(0, count);
}
function generateAutogenId() {
return `gen-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
}
function readAnnotations() {
if (!fs_1.default.existsSync(config_1.AUTORUN_ANNOTATIONS_FILE)) {
return [];
}
try {
const raw = fs_1.default.readFileSync(config_1.AUTORUN_ANNOTATIONS_FILE, "utf-8");
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((item) => toRecord(item))
.filter((item) => item !== null)
.map((item) => {
const context = toRecord(item.context);
return {
annotation_id: toStringSafe(item.annotation_id) ?? "",
run_id: toStringSafe(item.run_id) ?? "",
case_id: toStringSafe(item.case_id) ?? "",
session_id: toStringSafe(item.session_id) ?? "",
message_index: clampInt(toNumberSafe(item.message_index), 0, 100_000, 0),
rating: clampInt(toNumberSafe(item.rating), 1, 5, 1),
comment: toStringSafe(item.comment) ?? "",
manual_case_decision: parseManualCaseDecision(item.manual_case_decision),
annotation_author: parseAnnotationAuthor(item.annotation_author),
created_at: toStringSafe(item.created_at) ?? new Date().toISOString(),
updated_at: toStringSafe(item.updated_at) ?? new Date().toISOString(),
context: {
message_id: toStringSafe(context?.message_id),
trace_id: toStringSafe(context?.trace_id),
reply_type: toStringSafe(context?.reply_type),
eval_target: toStringSafe(context?.eval_target) ?? "unknown",
prompt_version: toStringSafe(context?.prompt_version),
domain: toStringSafe(context?.domain),
query_class: toStringSafe(context?.query_class)
}
};
})
.filter((item) => item.annotation_id && item.run_id && item.case_id);
}
catch {
return [];
}
}
function writeAnnotations(items) {
fs_1.default.writeFileSync(config_1.AUTORUN_ANNOTATIONS_FILE, JSON.stringify(items, null, 2), "utf-8");
}
function annotationKey(runId, caseId, messageIndex) {
return `${runId}::${caseId}::${messageIndex}`;
}
function buildAnnotationStatsMap(runId, annotations) {
const scoped = annotations.filter((item) => item.run_id === runId);
const buckets = new Map();
for (const item of scoped) {
const bucket = buckets.get(item.case_id) ?? { count: 0, ratings: [], latestMs: null };
bucket.count += 1;
bucket.ratings.push(item.rating);
const ms = Date.parse(item.updated_at);
if (Number.isFinite(ms) && (bucket.latestMs === null || ms > bucket.latestMs)) {
bucket.latestMs = ms;
}
buckets.set(item.case_id, bucket);
}
const result = new Map();
for (const [caseId, bucket] of buckets.entries()) {
const avg = bucket.ratings.length > 0 ? Number((bucket.ratings.reduce((a, b) => a + b, 0) / bucket.ratings.length).toFixed(2)) : null;
result.set(caseId, {
count: bucket.count,
latest_at: bucket.latestMs === null ? null : new Date(bucket.latestMs).toISOString(),
avg_rating: avg
});
}
return result;
}
function buildAnnotationsByMessageIndex(runId, caseId, annotations) {
const map = new Map();
for (const item of annotations) {
if (item.run_id !== runId || item.case_id !== caseId)
continue;
const current = map.get(item.message_index);
const currentMs = current ? Date.parse(current.updated_at) : null;
const nextMs = Date.parse(item.updated_at);
if (!current || (!Number.isNaN(nextMs) && (currentMs === null || nextMs >= currentMs))) {
map.set(item.message_index, item);
}
}
return map;
}
function resolveRunTarget(input) { function resolveRunTarget(input) {
const explicit = toStringSafe(input.report.eval_target); const explicit = toStringSafe(input.report.eval_target);
if (explicit === "assistant_stage1" || explicit === "assistant_stage2" || explicit === "assistant_p0" || explicit === "normalizer") { if (explicit === "assistant_stage1" || explicit === "assistant_stage2" || explicit === "assistant_p0" || explicit === "normalizer") {
@ -221,7 +542,7 @@ function getResultCases(report) {
.map((item) => toRecord(item)) .map((item) => toRecord(item))
.filter((item) => item !== null); .filter((item) => item !== null);
} }
function buildCaseSummaries(report, runId, checkDialogAvailability) { function buildCaseSummaries(report, runId, checkDialogAvailability, annotationStatsByCase) {
const results = getResultCases(report); const results = getResultCases(report);
return results.map((item, index) => { return results.map((item, index) => {
const caseId = toStringSafe(item.case_id) ?? `case-${index + 1}`; const caseId = toStringSafe(item.case_id) ?? `case-${index + 1}`;
@ -235,6 +556,7 @@ function buildCaseSummaries(report, runId, checkDialogAvailability) {
const dialogAvailable = checkDialogAvailability const dialogAvailable = checkDialogAvailability
? fs_1.default.existsSync(path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`)) ? fs_1.default.existsSync(path_1.default.resolve(config_1.ASSISTANT_SESSIONS_DIR, `${sessionId}.json`))
: false; : false;
const annotationStats = annotationStatsByCase?.get(caseId);
return { return {
case_id: caseId, case_id: caseId,
domain: toStringSafe(item.domain), domain: toStringSafe(item.domain),
@ -245,6 +567,9 @@ function buildCaseSummaries(report, runId, checkDialogAvailability) {
reply_type: toStringSafe(item.reply_type), reply_type: toStringSafe(item.reply_type),
session_id: sessionId, session_id: sessionId,
dialog_available: dialogAvailable, dialog_available: dialogAvailable,
commented_count: annotationStats?.count ?? 0,
latest_annotation_at: annotationStats?.latest_at ?? null,
avg_rating: annotationStats?.avg_rating ?? null,
checks, checks,
metric_subscores: metricSubscores metric_subscores: metricSubscores
}; };
@ -521,6 +846,7 @@ function loadSessionDialog(runId, caseId) {
.map((item) => toRecord(item)) .map((item) => toRecord(item))
.filter((item) => item !== null); .filter((item) => item !== null);
const messages = conversation.map((item) => ({ const messages = conversation.map((item) => ({
message_id: toStringSafe(item.message_id),
role: toStringSafe(item.role) ?? "unknown", role: toStringSafe(item.role) ?? "unknown",
text: toStringSafe(item.text) ?? "", text: toStringSafe(item.text) ?? "",
created_at: toStringSafe(item.created_at), created_at: toStringSafe(item.created_at),
@ -588,6 +914,7 @@ function buildFallbackDialog(run, caseId) {
session_id: sessionId, session_id: sessionId,
messages: [ messages: [
{ {
message_id: null,
role: "user", role: "user",
text: userText, text: userText,
created_at: null, created_at: null,
@ -595,6 +922,7 @@ function buildFallbackDialog(run, caseId) {
reply_type: null reply_type: null
}, },
{ {
message_id: null,
role: "assistant", role: "assistant",
text: assistantSummaryParts.join("\n"), text: assistantSummaryParts.join("\n"),
created_at: null, created_at: null,
@ -606,6 +934,175 @@ function buildFallbackDialog(run, caseId) {
assistant_mode: null assistant_mode: null
}; };
} }
function withMessageAnnotations(runId, caseId, messages, annotations) {
const byIndex = buildAnnotationsByMessageIndex(runId, caseId, annotations);
return messages.map((message, index) => {
const annotation = byIndex.get(index) ?? null;
return {
...message,
message_index: index,
commented: annotation !== null,
annotation
};
});
}
function generateAnnotationId() {
return `ann-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`;
}
function parseComment(value) {
const text = toStringSafe(value) ?? "";
return text.trim();
}
function parseDecisionFilter(value) {
const normalized = toStringSafe(value);
if (!normalized || normalized === "all")
return "all";
return parseManualCaseDecision(normalized);
}
function parseAutoGenMode(value) {
const normalized = toStringSafe(value)?.toLowerCase() ?? "";
if (normalized === "qwen_seed" || normalized === "codex_creative") {
return normalized;
}
return "codex_creative";
}
function parseAutogenCount(value) {
return clampInt(toNumberSafe(value), 1, 200, 24);
}
function parseAutogenDomain(value) {
const domain = normalizeDomainHint(value);
if (!domain)
return null;
return domain.slice(0, 80);
}
function hasAnyRunFilterQuery(query) {
return Boolean(toStringSafe(query.from) ??
toStringSafe(query.to) ??
toStringSafe(query.target) ??
toStringSafe(query.mode) ??
toStringSafe(query.use_mock) ??
toStringSafe(query.prompt_contains));
}
function buildAutogenCaseSetFileName(mode, generationId) {
const now = new Date();
const stamp = [
now.getUTCFullYear(),
String(now.getUTCMonth() + 1).padStart(2, "0"),
String(now.getUTCDate()).padStart(2, "0"),
String(now.getUTCHours()).padStart(2, "0"),
String(now.getUTCMinutes()).padStart(2, "0"),
String(now.getUTCSeconds()).padStart(2, "0")
].join("");
return `assistant_autogen_${mode}_${stamp}_${generationId}.json`;
}
function buildAutogenCaseSetPayload(input) {
const cases = input.questions.map((question, index) => ({
case_id: `AUTO-${String(index + 1).padStart(3, "0")}`,
scenario_tag: `${input.mode}_${input.domain ?? "general"}`,
question_type: "direct",
broadness_level: "medium",
turns: [{ user_message: question }],
expected_hints: {
expected_reply_type: null,
expected_degraded_to: null
}
}));
return {
suite_id: `assistant_autogen_${input.generationId}`,
suite_version: "0.1.0",
schema_version: "assistant_autogen_suite_v0_1",
generated_at: new Date().toISOString(),
generation_id: input.generationId,
mode: input.mode,
domain: input.domain,
scenario_count: cases.length,
case_ids: cases.map((item) => item.case_id),
cases
};
}
function collectPostAnalysis(annotations, runMap, limitPerQueue) {
const byDecision = {};
const byQueue = {};
const byDomain = new Map();
const queues = {
routing_extension: [],
policy_fix: [],
capability_registry: [],
soft_boundary: [],
safety_policy: [],
testset_hygiene: [],
covered_ok: []
};
const registry = (0, capabilitiesRegistry_1.loadCapabilitiesRegistry)();
for (const item of annotations) {
byDecision[item.manual_case_decision] = (byDecision[item.manual_case_decision] ?? 0) + 1;
const queueKey = DECISION_QUEUE_MAP[item.manual_case_decision];
byQueue[queueKey] = (byQueue[queueKey] ?? 0) + 1;
const run = runMap.get(item.run_id) ?? null;
const caseSummary = run
? buildCaseSummaries(run.report, run.run_id, false).find((candidate) => candidate.case_id === item.case_id) ?? null
: null;
const nearestGroup = (0, capabilitiesRegistry_1.resolveNearestCapabilityGroup)({
domain: caseSummary?.domain ?? item.context.domain,
queryClass: caseSummary?.query_class ?? item.context.query_class
}) ??
registry.groups[0] ??
null;
const domainKey = caseSummary?.domain ?? item.context.domain ?? "unknown";
byDomain.set(domainKey, (byDomain.get(domainKey) ?? 0) + 1);
const view = {
annotation_id: item.annotation_id,
run_id: item.run_id,
case_id: item.case_id,
message_index: item.message_index,
rating: item.rating,
comment: item.comment,
manual_case_decision: item.manual_case_decision,
annotation_author: item.annotation_author,
updated_at: item.updated_at,
domain: caseSummary?.domain ?? item.context.domain ?? null,
query_class: caseSummary?.query_class ?? item.context.query_class ?? null,
trace_id: item.context.trace_id ?? caseSummary?.trace_id ?? null,
reply_type: item.context.reply_type ?? caseSummary?.reply_type ?? null,
nearest_capability_group: nearestGroup
? {
group_code: nearestGroup.group_code,
group_title: nearestGroup.group_title,
maturity_status: nearestGroup.maturity_status
}
: null
};
if (queueKey === "none") {
if (queues.covered_ok.length < limitPerQueue)
queues.covered_ok.push(view);
continue;
}
if (!queues[queueKey]) {
queues[queueKey] = [];
}
if (queues[queueKey].length < limitPerQueue) {
queues[queueKey].push(view);
}
}
const domainSummary = Array.from(byDomain.entries())
.map(([domain, total]) => ({ domain, total }))
.sort((a, b) => b.total - a.total);
return {
stats: {
annotations_total: annotations.length,
by_decision: byDecision,
by_queue: byQueue,
domains_total: domainSummary.length
},
domain_summary: domainSummary,
queues,
recommended_regression_candidates: [
...queues.routing_extension.slice(0, 20),
...queues.policy_fix.slice(0, 20),
...queues.safety_policy.slice(0, 20)
].slice(0, 60)
};
}
function buildAutoRunsRouter() { function buildAutoRunsRouter() {
const router = (0, express_1.Router)(); const router = (0, express_1.Router)();
router.get("/api/autoruns/history", (req, res) => { router.get("/api/autoruns/history", (req, res) => {
@ -648,13 +1145,18 @@ function buildAutoRunsRouter() {
if (!run) { if (!run) {
throw new http_1.ApiError("AUTORUN_NOT_FOUND", `Run not found: ${runId}`, 404); throw new http_1.ApiError("AUTORUN_NOT_FOUND", `Run not found: ${runId}`, 404);
} }
const cases = buildCaseSummaries(run.report, run.run_id, true); const annotations = readAnnotations();
const annotationStatsByCase = buildAnnotationStatsMap(runId, annotations);
const cases = buildCaseSummaries(run.report, run.run_id, true, annotationStatsByCase);
const coverage = buildCoverageFromCases(cases); const coverage = buildCoverageFromCases(cases);
(0, http_1.ok)(res, { (0, http_1.ok)(res, {
ok: true, ok: true,
run: buildRunSummary(run), run: buildRunSummary(run),
coverage, coverage,
cases, cases,
annotations_summary: {
total: annotations.filter((item) => item.run_id === runId).length
},
report: run.report report: run.report
}); });
} }
@ -675,11 +1177,302 @@ function buildAutoRunsRouter() {
} }
const sessionDialog = loadSessionDialog(runId, caseId); const sessionDialog = loadSessionDialog(runId, caseId);
const dialog = sessionDialog ?? buildFallbackDialog(run, caseId); const dialog = sessionDialog ?? buildFallbackDialog(run, caseId);
const annotations = readAnnotations();
const messages = withMessageAnnotations(runId, caseId, dialog.messages, annotations);
(0, http_1.ok)(res, { (0, http_1.ok)(res, {
ok: true, ok: true,
run_id: runId, run_id: runId,
case_id: caseId, case_id: caseId,
...dialog ...dialog,
messages,
annotations: annotations
.filter((item) => item.run_id === runId && item.case_id === caseId)
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
});
}
catch (error) {
next(error);
}
});
router.get("/api/autoruns/annotations", (req, res, next) => {
try {
const runIdFilter = toStringSafe(req.query.run_id);
const caseIdFilter = toStringSafe(req.query.case_id);
const minRatingRaw = toNumberSafe(req.query.min_rating);
const minRating = minRatingRaw === null ? null : clampInt(minRatingRaw, 1, 5, 1);
const decisionFilter = parseDecisionFilter(req.query.manual_case_decision);
const limit = clampInt(toNumberSafe(req.query.limit), 1, 2000, 400);
const scanLimit = clampInt(toNumberSafe(req.query.scan_limit), 50, 5000, 2500);
const annotations = readAnnotations()
.filter((item) => (runIdFilter ? item.run_id === runIdFilter : true))
.filter((item) => (caseIdFilter ? item.case_id === caseIdFilter : true))
.filter((item) => (minRating === null ? true : item.rating >= minRating))
.filter((item) => (decisionFilter === "all" ? true : item.manual_case_decision === decisionFilter))
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
.slice(0, limit);
const runIndex = indexRuns(scanLimit);
const runMap = new Map(runIndex.map((item) => [item.run_id, item]));
const items = annotations.map((item) => {
const run = runMap.get(item.run_id) ?? null;
const runSummary = run ? buildRunSummary(run) : null;
const cases = run ? buildCaseSummaries(run.report, run.run_id, false) : [];
const caseSummary = cases.find((candidate) => candidate.case_id === item.case_id) ?? null;
return {
...item,
run: runSummary,
case_summary: caseSummary,
technical_context: {
report_path: run?.report_path ?? null,
trace_id: item.context.trace_id,
reply_type: item.context.reply_type,
domain: item.context.domain,
query_class: item.context.query_class,
checks: caseSummary?.checks ?? null,
metric_subscores: caseSummary?.metric_subscores ?? null
}
};
});
const avgRating = items.length > 0 ? Number((items.reduce((acc, item) => acc + item.rating, 0) / items.length).toFixed(2)) : null;
const byDecision = items.reduce((acc, item) => {
acc[item.manual_case_decision] = (acc[item.manual_case_decision] ?? 0) + 1;
return acc;
}, {});
(0, http_1.ok)(res, {
ok: true,
generated_at: new Date().toISOString(),
filters_applied: {
run_id: runIdFilter ?? null,
case_id: caseIdFilter ?? null,
min_rating: minRating,
manual_case_decision: decisionFilter,
limit
},
stats: {
total: items.length,
avg_rating: avgRating,
by_decision: byDecision
},
available_manual_case_decisions: MANUAL_CASE_DECISIONS,
manual_case_decision_schema: readManualDecisionSchema(),
items
});
}
catch (error) {
next(error);
}
});
router.post("/api/autoruns/annotations", (req, res, next) => {
try {
const body = toRecord(req.body);
if (!body) {
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "JSON body is required", 400);
}
const runId = toStringSafe(body.run_id);
const caseId = toStringSafe(body.case_id);
const messageIndexRaw = toNumberSafe(body.message_index);
const ratingRaw = toNumberSafe(body.rating);
const comment = parseComment(body.comment);
const manualCaseDecision = parseManualCaseDecision(body.manual_case_decision);
const annotationAuthor = parseAnnotationAuthor(body.annotation_author);
if (!runId || !caseId) {
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "run_id and case_id are required", 400);
}
if (messageIndexRaw === null) {
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "message_index is required", 400);
}
const messageIndex = clampInt(messageIndexRaw, 0, 100_000, 0);
if (ratingRaw === null) {
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "rating is required", 400);
}
const rating = clampInt(ratingRaw, 1, 5, 1);
if (comment.length === 0) {
throw new http_1.ApiError("INVALID_ANNOTATION_PAYLOAD", "comment is required", 400);
}
const run = findRunById(runId);
if (!run) {
throw new http_1.ApiError("AUTORUN_NOT_FOUND", `Run not found: ${runId}`, 404);
}
const cases = buildCaseSummaries(run.report, run.run_id, false);
const caseSummary = cases.find((item) => item.case_id === caseId) ?? null;
if (!caseSummary) {
throw new http_1.ApiError("AUTORUN_CASE_NOT_FOUND", `Case not found: ${caseId} in run ${runId}`, 404);
}
const sessionDialog = loadSessionDialog(runId, caseId);
const dialog = sessionDialog ?? buildFallbackDialog(run, caseId);
if (messageIndex >= dialog.messages.length) {
throw new http_1.ApiError("AUTORUN_MESSAGE_NOT_FOUND", `Message index ${messageIndex} out of range`, 400);
}
const targetMessage = dialog.messages[messageIndex];
const targetRole = toStringSafe(targetMessage.role) ?? "unknown";
if (targetRole !== "assistant") {
throw new http_1.ApiError("AUTORUN_MESSAGE_NOT_ASSISTANT", "Only assistant answers can be annotated", 400);
}
const nowIso = new Date().toISOString();
const annotations = readAnnotations();
const key = annotationKey(runId, caseId, messageIndex);
const existingIndex = annotations.findIndex((item) => annotationKey(item.run_id, item.case_id, item.message_index) === key);
const existing = existingIndex >= 0 ? annotations[existingIndex] : null;
const annotation = {
annotation_id: existing?.annotation_id ?? generateAnnotationId(),
run_id: runId,
case_id: caseId,
session_id: caseSummary.session_id,
message_index: messageIndex,
rating,
comment,
manual_case_decision: manualCaseDecision,
annotation_author: annotationAuthor,
created_at: existing?.created_at ?? nowIso,
updated_at: nowIso,
context: {
message_id: toStringSafe(targetMessage.message_id),
trace_id: toStringSafe(targetMessage.trace_id) ?? caseSummary.trace_id,
reply_type: toStringSafe(targetMessage.reply_type) ?? caseSummary.reply_type,
eval_target: run.eval_target,
prompt_version: toStringSafe(run.report.prompt_version),
domain: caseSummary.domain,
query_class: caseSummary.query_class
}
};
if (existingIndex >= 0) {
annotations[existingIndex] = annotation;
}
else {
annotations.push(annotation);
}
writeAnnotations(annotations);
const annotationStatsByCase = buildAnnotationStatsMap(runId, annotations);
const caseStats = annotationStatsByCase.get(caseId) ?? null;
(0, http_1.ok)(res, {
ok: true,
annotation,
case_annotation_stats: caseStats
});
}
catch (error) {
next(error);
}
});
router.get("/api/autoruns/manual-decision-schema", (_req, res) => {
(0, http_1.ok)(res, {
ok: true,
schema: readManualDecisionSchema(),
enum: MANUAL_CASE_DECISIONS
});
});
router.get("/api/autoruns/post-analysis", (req, res, next) => {
try {
const query = req.query;
const runIdFilter = toStringSafe(query.run_id);
const limitPerQueue = clampInt(toNumberSafe(query.limit_per_queue), 5, 250, 40);
const annotationLimit = clampInt(toNumberSafe(query.annotation_limit), 20, 5000, 1500);
const scanLimit = clampInt(toNumberSafe(query.scan_limit), 50, 5000, 2500);
const runFilters = parseFilters(query);
const applyRunFilters = hasAnyRunFilterQuery(query);
const runIndex = indexRuns(Math.max(scanLimit, runFilters.scan_limit));
const filteredRuns = applyRunFilters ? runIndex.filter((run) => matchesFilters(run, runFilters)) : runIndex;
const runMap = new Map(filteredRuns.map((run) => [run.run_id, run]));
const scopedAnnotations = readAnnotations()
.filter((item) => (runIdFilter ? item.run_id === runIdFilter : true))
.filter((item) => (runMap.size > 0 ? runMap.has(item.run_id) : true))
.sort((a, b) => Date.parse(b.updated_at) - Date.parse(a.updated_at))
.slice(0, annotationLimit);
const analysis = collectPostAnalysis(scopedAnnotations, runMap, limitPerQueue);
(0, http_1.ok)(res, {
ok: true,
generated_at: new Date().toISOString(),
filters_applied: {
run_id: runIdFilter ?? null,
run_filters_applied: applyRunFilters,
limit_per_queue: limitPerQueue,
annotation_limit: annotationLimit,
scan_limit: scanLimit
},
runs_considered: filteredRuns.slice(0, 500).map((item) => buildRunSummary(item)),
manual_case_decision_schema: readManualDecisionSchema(),
post_analysis: analysis
});
}
catch (error) {
next(error);
}
});
router.get("/api/autoruns/autogen/history", (req, res, next) => {
try {
const limit = clampInt(toNumberSafe(req.query.limit), 1, 500, 120);
const rawMode = toStringSafe(req.query.mode);
const includeAllModes = !rawMode || !["qwen_seed", "codex_creative"].includes(rawMode);
const modeFilter = rawMode ?? "codex_creative";
const items = readAutoGenHistory()
.filter((item) => (includeAllModes ? true : item.mode === modeFilter))
.slice(0, limit);
(0, http_1.ok)(res, {
ok: true,
generated_at: new Date().toISOString(),
items
});
}
catch (error) {
next(error);
}
});
router.post("/api/autoruns/autogen/generate", (req, res, next) => {
try {
const body = toRecord(req.body);
if (!body) {
throw new http_1.ApiError("INVALID_AUTOGEN_PAYLOAD", "JSON body is required", 400);
}
const mode = parseAutoGenMode(body.mode);
const count = parseAutogenCount(body.count);
const domain = parseAutogenDomain(body.domain);
const persistCaseSet = toBooleanSafe(body.persist_to_eval_cases) ?? true;
const generatedBy = parseAnnotationAuthor(body.generated_by);
const context = toRecord(body.context);
const questions = mode === "qwen_seed"
? generateQwenSeedQuestions(count, domain)
: generateCodexCreativeQuestions(count, domain);
const generationId = generateAutogenId();
let savedCaseSetFile = null;
if (persistCaseSet) {
if (!fs_1.default.existsSync(config_1.EVAL_CASES_DIR)) {
fs_1.default.mkdirSync(config_1.EVAL_CASES_DIR, { recursive: true });
}
const fileName = buildAutogenCaseSetFileName(mode, generationId);
const filePath = path_1.default.resolve(config_1.EVAL_CASES_DIR, fileName);
const payload = buildAutogenCaseSetPayload({
generationId,
mode,
domain,
questions
});
fs_1.default.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf-8");
savedCaseSetFile = fileName;
}
const record = {
generation_id: generationId,
created_at: new Date().toISOString(),
mode,
count: questions.length,
domain,
questions,
generated_by: generatedBy,
saved_case_set_file: savedCaseSetFile,
context: context
? {
llm_provider: toStringSafe(context.llm_provider),
model: toStringSafe(context.model),
assistant_prompt_version: toStringSafe(context.assistant_prompt_version),
decomposition_prompt_version: toStringSafe(context.decomposition_prompt_version),
prompt_fingerprint: toStringSafe(context.prompt_fingerprint)
}
: null
};
const history = readAutoGenHistory();
history.unshift(record);
writeAutoGenHistory(history.slice(0, 500));
(0, http_1.ok)(res, {
ok: true,
generation: record
}); });
} }
catch (error) { catch (error) {

View File

@ -31,6 +31,8 @@ function createApp() {
(0, files_1.ensureDir)(config_1.EVAL_CASES_DIR); (0, files_1.ensureDir)(config_1.EVAL_CASES_DIR);
(0, files_1.ensureDir)(config_1.REPORTS_DIR); (0, files_1.ensureDir)(config_1.REPORTS_DIR);
(0, files_1.ensureDir)(config_1.ASSISTANT_SESSIONS_DIR); (0, files_1.ensureDir)(config_1.ASSISTANT_SESSIONS_DIR);
(0, files_1.ensureDir)(config_1.AUTORUN_ANNOTATIONS_DIR);
(0, files_1.ensureDir)(config_1.AUTORUN_GENERATOR_DIR);
const app = (0, express_1.default)(); const app = (0, express_1.default)();
app.use((0, cors_1.default)()); app.use((0, cors_1.default)());
app.use(express_1.default.json({ type: ["application/json", "application/*+json"], limit: "2mb" })); app.use(express_1.default.json({ type: ["application/json", "application/*+json"], limit: "2mb" }));

View File

@ -1158,29 +1158,29 @@ function buildProblemCentricActions(input) {
actions.push("Проверьте зачет аванса или взаимозачет и связку платежа с закрытием расчета."); actions.push("Проверьте зачет аванса или взаимозачет и связку платежа с закрытием расчета.");
} }
if (unitTypes.has("broken_chain_segment")) { if (unitTypes.has("broken_chain_segment")) {
actions.push("Проверьте СЃРІСЏР·РєСѓ выписка -> документ -> РїСЂРѕРІРѕРґРєР° РїРѕ проблемным участкам цепочки."); actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
} }
if (unitTypes.has("unresolved_settlement_cluster")) { if (unitTypes.has("unresolved_settlement_cluster")) {
actions.push("Сверьте хвосты РїРѕ расчетам: закрылся ли документ оплаты корректным закрывающим документом."); actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
} }
if (unitTypes.has("period_risk_cluster")) { if (unitTypes.has("period_risk_cluster")) {
actions.push("Оцените влияние дефекта РЅР° закрытие периода Рё корректность регламентных операций."); actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
} }
if (unitTypes.has("cross_branch_inconsistency_cluster")) { if (unitTypes.has("cross_branch_inconsistency_cluster")) {
actions.push("Сверьте противоречия между документами, проводками Рё регистрами РїРѕ НДС/межконтурным СЃРІСЏР·СЏРј."); actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
} }
if (unitTypes.has("lifecycle_anomaly_node")) { if (unitTypes.has("lifecycle_anomaly_node")) {
actions.push("Проверьте lifecycle объекта: ожидаемый этап РЅРµ должен оставаться РІ partially_linked состоянии."); actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
} }
for (const unit of input.units) { for (const unit of input.units) {
if (unit.lifecycle_defect_type === "stale_active_state") { if (unit.lifecycle_defect_type === "stale_active_state") {
actions.push("Проверьте, почему объект завис: ожидаемый переход РЅРµ должен оставаться РІ активной стадии."); actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
} }
if (unit.lifecycle_defect_type === "misclosed_state") { if (unit.lifecycle_defect_type === "misclosed_state") {
actions.push("Проверьте закрывающий документ Рё РїСЂРѕРІРѕРґРєРё: закрытие может быть формальным, РЅРѕ некорректным РїРѕ пути."); actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
} }
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") { if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
actions.push("Сверьте бухгалтерскую Рё смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния."); actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
} }
} }
if (input.missingAnchors.period && input.mode !== "clarification_required") { if (input.missingAnchors.period && input.mode !== "clarification_required") {
@ -1188,20 +1188,20 @@ function buildProblemCentricActions(input) {
} }
if (input.mode === "clarification_required") { if (input.mode === "clarification_required") {
if (input.missingAnchors.period) { if (input.missingAnchors.period) {
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура."); actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
actions.push("Уточните счет или РіСЂСѓРїРїСѓ счетов для предметной локализации дефекта."); actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения."); actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
actions.push("Укажите контрагента/РґРѕРіРѕРІРѕСЂ, чтобы проверить хвосты Рё разрывы РЅР° конкретной СЃРІСЏР·РєРµ."); actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
} }
} }
if (input.coverageReport.requirements_uncovered.length > 0) { if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`); actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
} }
return uniqueStrings(actions, 6); return uniqueStrings(actions, 6);
} }
@ -1215,25 +1215,25 @@ function buildProblemCentricClarifications(input) {
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер."); questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
questions.push("Уточните счет или СЃРІСЏР·РєСѓ счетов (например, 51/60), РіРґРµ РІС РѕР¶РёРґР°РµС‚Рµ дефект."); questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/объект, РѕС РєРѕС‚РѕСЂРѕРіРѕ РЅСѓР¶РЅРѕ строить проверку цепочки."); questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или РґРѕРіРѕРІРѕСЂ, РїРѕ которому проверить незакрытую экспозицию."); questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
} }
if (unitTypes.has("broken_chain_segment")) { if (unitTypes.has("broken_chain_segment")) {
questions.push("Уточните участок цепочки: выписка, платежный документ или РїСЂРѕРІРѕРґРєР°."); questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
} }
if (unitTypes.has("period_risk_cluster")) { if (unitTypes.has("period_risk_cluster")) {
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок."); questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
} }
if (unitTypes.has("unresolved_settlement_cluster")) { if (unitTypes.has("unresolved_settlement_cluster")) {
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или РѕР±Р° направления."); questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
} }
if (input.coverageReport.clarification_needed_for.length > 0) { if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`); questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
} }
return uniqueStrings(questions, 6); return uniqueStrings(questions, 6);
} }
@ -1402,10 +1402,10 @@ function detectMissingAnchors(userMessage, retrievalResults = [], options) {
hasPeriodAnchorInRetrieval(retrievalResults) || hasPeriodAnchorInRetrieval(retrievalResults) ||
Boolean(options?.normalizationPeriodExplicit) || Boolean(options?.normalizationPeriodExplicit) ||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors); hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(lower) || hasAccountAnchorInRetrieval(retrievalResults); const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(lower) || hasAccountAnchorInRetrieval(retrievalResults);
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower); const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower); const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower); const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return { return {
period: !hasPeriod, period: !hasPeriod,
account: !hasAccount, account: !hasAccount,
@ -1424,50 +1424,50 @@ function buildClarificationQuestions(input) {
questions.push("Уточните период проверки (например, июль 2020)."); questions.push("Уточните период проверки (например, июль 2020).");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
questions.push("Уточните счет или РіСЂСѓРїРїСѓ счетов (например, 19, 60, 62)."); questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/GUID/конкретный объект для трассировки."); questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или РіСЂСѓРїРїСѓ контрагентов."); questions.push("Укажите контрагента или группу контрагентов.");
} }
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) { if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный СЂРёСЃРє."); questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
} }
if (input.coverageReport.clarification_needed_for.length > 0) { if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`); questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
} }
return uniqueStrings(questions, 6); return uniqueStrings(questions, 6);
} }
function buildRecommendedActions(input) { function buildRecommendedActions(input) {
const actions = []; const actions = [];
if (input.mode === "focused_grounded") { if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи РІ учетной базе Рё зафиксируйте итог РІ рабочем файле проверки."); actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
} }
if (input.mode === "broad_partial") { if (input.mode === "broad_partial") {
actions.push("Сузьте запрос РґРѕ периода + счета или периода + документа Рё повторите проверку."); actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
} }
if (input.mode === "clarification_required") { if (input.mode === "clarification_required") {
actions.push("Дайте недостающие СЏРєРѕСЂСЏ (период/счет/объект), иначе сильный factual вывод невозможен."); actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
} }
if (input.coverageReport.requirements_uncovered.length > 0) { if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`); actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
} }
if (input.coverageReport.requirements_partially_covered.length > 0) { if (input.coverageReport.requirements_partially_covered.length > 0) {
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`); actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
} }
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") { if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
actions.push("Добавьте более СѓР·РєРёР№ контекст: тип отклонения, РіСЂСѓРїРїСѓ документов Рё бизнес-участок."); actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
} }
if (input.limitationReasonCodes.includes("snapshot_only")) { if (input.limitationReasonCodes.includes("snapshot_only")) {
actions.push("Сверьте критичные выводы СЃ live source-of-record РІ 1C."); actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
} }
if (input.limitationReasonCodes.includes("weak_source_mapping")) { if (input.limitationReasonCodes.includes("weak_source_mapping")) {
actions.push("Проверьте source mapping для связей document/register РїРѕ указанным ref."); actions.push("Проверьте source mapping для связей document/register по указанным ref.");
} }
if (input.sourceRefs.length > 0) { if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку СЃ ${input.sourceRefs.length} подтвержденных записей Рё сверьте РёС… СЃ первичными документами.`); actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
} }
return uniqueStrings(actions, 6); return uniqueStrings(actions, 6);
} }

View File

@ -0,0 +1,41 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadAssistantCanonExcerpt = loadAssistantCanonExcerpt;
const fs_1 = __importDefault(require("fs"));
const config_1 = require("../config");
const FALLBACK_CANON = [
"Не выдумывай возможности.",
"Не обещай настройку 1С и админ-действия.",
"Не показывай внутренние технические термины пользователю.",
"Говори по-человечески и предлагай ближайший полезный поддерживаемый шаг."
].join(" ");
let cache = null;
function stripMarkdown(input) {
return input
.replace(/^#{1,6}\s+/gm, "")
.replace(/[`*_>\-\[\]\(\)]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function loadAssistantCanonExcerpt(maxChars = 900) {
try {
const mtimeMs = fs_1.default.existsSync(config_1.ASSISTANT_CANON_FILE) ? fs_1.default.statSync(config_1.ASSISTANT_CANON_FILE).mtimeMs : -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.excerpt;
}
if (!fs_1.default.existsSync(config_1.ASSISTANT_CANON_FILE)) {
return FALLBACK_CANON;
}
const raw = fs_1.default.readFileSync(config_1.ASSISTANT_CANON_FILE, "utf-8");
const normalized = stripMarkdown(raw);
const excerpt = normalized.length > maxChars ? `${normalized.slice(0, maxChars).trim()}...` : normalized;
cache = { mtimeMs, excerpt: excerpt || FALLBACK_CANON };
return cache.excerpt;
}
catch {
return cache?.excerpt ?? FALLBACK_CANON;
}
}

View File

@ -536,8 +536,8 @@ const P0_DOMAIN_CARDS = [
/закрыт[а-яё]*\s+период/i, /закрыт[а-яё]*\s+период/i,
/close\s+operation/i, /close\s+operation/i,
/allocation/i, /allocation/i,
/закр/i, /закр/i,
/перио/i, /перио/i,
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i, /\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i, /\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
/\u0437\u0430\u0442\u0440\u0430\u0442/i, /\u0437\u0430\u0442\u0440\u0430\u0442/i,
@ -559,14 +559,14 @@ function parseDateCandidate(value) {
} }
function extractDate(record) { function extractDate(record) {
const attrs = record.attributes ?? {}; const attrs = record.attributes ?? {};
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"]; const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
for (const key of directKeys) { for (const key of directKeys) {
if (attrs[key] !== undefined && attrs[key] !== null) { if (attrs[key] !== undefined && attrs[key] !== null) {
return String(attrs[key]); return String(attrs[key]);
} }
} }
for (const [key, value] of Object.entries(attrs)) { for (const [key, value] of Object.entries(attrs)) {
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) { if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
return value; return value;
} }
} }
@ -593,7 +593,7 @@ function countNavigationLinks(record) {
return count; return count;
} }
function findCounterpartyLinks(record) { function findCounterpartyLinks(record) {
return record.links.filter((link) => link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|РїРѕРєСѓРї/i.test(link.source_field)); return record.links.filter((link) => link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field));
} }
function extractGuids(text) { function extractGuids(text) {
const matches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? []; const matches = text.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi) ?? [];
@ -1305,14 +1305,14 @@ function inferPeriodScope(fragmentText) {
granularity: "year" granularity: "year"
}; };
} }
if (/квартал|quarter/i.test(fragmentText)) { if (/квартал|quarter/i.test(fragmentText)) {
return { return {
from: null, from: null,
to: null, to: null,
granularity: "quarter" granularity: "quarter"
}; };
} }
if (/месяц|month|период/i.test(fragmentText)) { if (/месяц|month|период/i.test(fragmentText)) {
return { return {
from: null, from: null,
to: null, to: null,
@ -1378,7 +1378,7 @@ function buildSemanticRetrievalProfile(fragmentText) {
const excludedInterpretations = []; const excludedInterpretations = [];
const rankingBasis = ["closure_risk", "repeatability", "financial_impact"]; const rankingBasis = ["closure_risk", "repeatability", "financial_impact"];
const explanationFocus = ["why_selected", "where_chain_breaks", "what_business_risk"]; const explanationFocus = ["why_selected", "where_chain_breaks", "what_business_risk"];
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) { if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
pushMany(domainScope, ["bank", "settlements"]); pushMany(domainScope, ["bank", "settlements"]);
pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]); pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
@ -1390,51 +1390,51 @@ function buildSemanticRetrievalProfile(fragmentText) {
const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97"); const hasDeferredExpenseAccountScope = accountScope.some((item) => item === "97");
const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item)); const hasMonthCloseCostsAccountScope = accountScope.some((item) => CLOSE_COST_ACCOUNTS.includes(item));
const hasExplicitMonthCloseLexicalMarker = /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(lower) || const hasExplicitMonthCloseLexicalMarker = /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(lower) ||
(/закр/i.test(lower) && /перио/i.test(lower)); (/закр/i.test(lower) && /перио/i.test(lower));
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) { if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["suppliers", "settlements"]); pushMany(domainScope, ["suppliers", "settlements"]);
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]); pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]); pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
} }
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) { if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["customers", "settlements"]); pushMany(domainScope, ["customers", "settlements"]);
pushMany(documentTypes, ["sales_document", "settlement_document"]); pushMany(documentTypes, ["sales_document", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]); pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
} }
if (/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(lower) || if (/ндс|vat|книга\s+покупок|книга\s+продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(lower) ||
hasVatAccountScope) { hasVatAccountScope) {
pushMany(domainScope, ["vat", "taxes"]); pushMany(domainScope, ["vat", "taxes"]);
pushMany(documentTypes, ["invoice", "vat_document"]); pushMany(documentTypes, ["invoice", "vat_document"]);
pushMany(entityTypes, ["document", "tax_entry", "posting"]); pushMany(entityTypes, ["document", "tax_entry", "posting"]);
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]); pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
} }
if (/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) || if (/ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(lower) ||
hasFixedAssetAccountScope) { hasFixedAssetAccountScope) {
pushMany(domainScope, ["fixed_assets"]); pushMany(domainScope, ["fixed_assets"]);
pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]); pushMany(documentTypes, ["fixed_asset_card", "fixed_asset_acceptance", "depreciation_document"]);
pushMany(entityTypes, ["fixed_asset", "document", "posting"]); pushMany(entityTypes, ["fixed_asset", "document", "posting"]);
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]); pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
} }
if (/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) || if (/рбп|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
hasDeferredExpenseAccountScope) { hasDeferredExpenseAccountScope) {
pushMany(domainScope, ["deferred_expense", "period_close"]); pushMany(domainScope, ["deferred_expense", "period_close"]);
pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]); pushMany(documentTypes, ["deferred_expense_document", "period_close_document"]);
pushMany(entityTypes, ["document", "posting"]); pushMany(entityTypes, ["document", "posting"]);
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]); pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
} }
if (/цепоч|разрыв|СЃРІСЏР·|документ.*РїСЂРѕРІРѕРґ|РіРґРµ рвет|Р¶РёРІСѓС‚ отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) { if (/цепоч|разрыв|связ|документ.*провод|где рвет|живут отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]); pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]); pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]);
} }
if (/аномал|СЂРёСЃРє|С…РІРѕСЃС‚|РїРѕРґРѕР·СЂ|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) { if (/аномал|риск|хвост|подозр|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]); pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]);
} }
if (WRONG_DOCUMENT_MARKERS.test(lower)) { if (WRONG_DOCUMENT_MARKERS.test(lower)) {
pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]); pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]);
} }
if (/Р¶РёРІСѓС‚ отдельно|РЅРµ СЃРІСЏР·|без СЃРІСЏР·Рё|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) { if (/живут отдельно|не связ|без связи|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]); pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
} }
if (REPEATED_ANOMALY_MARKERS.test(lower)) { if (REPEATED_ANOMALY_MARKERS.test(lower)) {
@ -1446,10 +1446,10 @@ function buildSemanticRetrievalProfile(fragmentText) {
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]); pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
pushMany(documentTypes, ["period_close_document"]); pushMany(documentTypes, ["period_close_document"]);
} }
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) { if (/не\s+в\s+платеже|not payment/i.test(lower)) {
pushMany(excludedInterpretations, ["simple_payment_delay"]); pushMany(excludedInterpretations, ["simple_payment_delay"]);
} }
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) { if (/не\s+по\s+сумм|не\s+сумм|не\s+сумма|not by amount/i.test(lower)) {
pushMany(excludedInterpretations, ["amount_only_anomaly"]); pushMany(excludedInterpretations, ["amount_only_anomaly"]);
pushMany(rankingBasis, ["amount_independent_risk"]); pushMany(rankingBasis, ["amount_independent_risk"]);
} }
@ -1735,16 +1735,16 @@ function inferAccountsFromRecord(record, corpus) {
accounts.push(token.split(".")[0]); accounts.push(token.split(".")[0]);
} }
for (const key of Object.keys(record.attributes ?? {})) { for (const key of Object.keys(record.attributes ?? {})) {
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) { if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
accounts.push("51"); accounts.push("51");
} }
if (/счетучетарасчетовсконтрагентом/i.test(key)) { if (/счетучетарасчетовсконтрагентом/i.test(key)) {
accounts.push("60"); accounts.push("60");
} }
if (/счетучетандс/i.test(key)) { if (/счетучетандс/i.test(key)) {
accounts.push("19"); accounts.push("19");
} }
if (/субконтодт/i.test(key)) { if (/субконтодт/i.test(key)) {
accounts.push("60"); accounts.push("60");
} }
} }
@ -1752,28 +1752,28 @@ function inferAccountsFromRecord(record, corpus) {
} }
function inferDocumentTypesFromRecord(record, corpus) { function inferDocumentTypesFromRecord(record, corpus) {
const items = []; const items = [];
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) { if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
pushMany(items, ["bank_statement", "payment_order"]); pushMany(items, ["bank_statement", "payment_order"]);
} }
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) { if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
items.push("supplier_receipt"); items.push("supplier_receipt");
} }
if (/реализациятоваровуслуг|реализац/i.test(corpus)) { if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
items.push("sales_document"); items.push("sales_document");
} }
if (/РЅРґСЃ|счетфактур|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
pushMany(items, ["invoice", "vat_document"]); pushMany(items, ["invoice", "vat_document"]);
} }
if (/корректировк|ручн|manual/i.test(corpus)) { if (/корректировк|ручн|manual/i.test(corpus)) {
items.push("manual_operation"); items.push("manual_operation");
} }
if (/закрытие|регламент/i.test(corpus)) { if (/закрытие|регламент/i.test(corpus)) {
items.push("period_close_document"); items.push("period_close_document");
} }
if (/РѕСЃРЅРѕРІРЅ|амортиз|fixed_asset/i.test(corpus)) { if (/основн|амортиз|fixed_asset/i.test(corpus)) {
pushMany(items, ["fixed_asset_card", "depreciation_document"]); pushMany(items, ["fixed_asset_card", "depreciation_document"]);
} }
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) { if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
items.push("deferred_expense_document"); items.push("deferred_expense_document");
} }
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) { if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
@ -1801,10 +1801,10 @@ function inferDomainsFromRecord(corpus, documentTypes, record) {
if (documentTypes.some((item) => item === "deferred_expense_document")) { if (documentTypes.some((item) => item === "deferred_expense_document")) {
pushMany(domains, ["deferred_expense", "period_close"]); pushMany(domains, ["deferred_expense", "period_close"]);
} }
if (/закрытие|регламент|period close/i.test(corpus)) { if (/закрытие|регламент|period close/i.test(corpus)) {
domains.push("period_close"); domains.push("period_close");
} }
const hasSettlementLexicalAnchor = /(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|РїРѕРєСѓРї)/i.test(corpus); const hasSettlementLexicalAnchor = /(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(corpus);
const hasSettlementDocAnchor = documentTypes.some((item) => item === "bank_statement" || item === "payment_order" || item === "supplier_receipt" || item === "sales_document"); const hasSettlementDocAnchor = documentTypes.some((item) => item === "bank_statement" || item === "payment_order" || item === "supplier_receipt" || item === "sales_document");
const hasSettlementDomainAnchor = domains.includes("bank") || domains.includes("suppliers") || domains.includes("customers") || domains.includes("supplier_payments"); const hasSettlementDomainAnchor = domains.includes("bank") || domains.includes("suppliers") || domains.includes("customers") || domains.includes("supplier_payments");
if (findCounterpartyLinks(record).length > 0 && (hasSettlementLexicalAnchor || hasSettlementDocAnchor || hasSettlementDomainAnchor)) { if (findCounterpartyLinks(record).length > 0 && (hasSettlementLexicalAnchor || hasSettlementDocAnchor || hasSettlementDomainAnchor)) {
@ -1824,13 +1824,13 @@ function inferEntityTypes(record) {
entities.push("counterparty"); entities.push("counterparty");
} }
const corpus = collectTextFromRecord(record); const corpus = collectTextFromRecord(record);
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) { if (/договор|contract/i.test(corpus)) {
entities.push("contract"); entities.push("contract");
} }
if (/РѕСЃРЅРѕРІРЅ|fixed_asset|инвентар/i.test(corpus)) { if (/основн|fixed_asset|инвентар/i.test(corpus)) {
entities.push("fixed_asset"); entities.push("fixed_asset");
} }
if (/РЅРґСЃ|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
entities.push("tax_entry"); entities.push("tax_entry");
} }
return uniqueStrings(entities); return uniqueStrings(entities);
@ -1842,25 +1842,25 @@ function inferRelationPatterns(record, corpus) {
if (hasDocLinks) { if (hasDocLinks) {
patterns.push("document_to_posting"); patterns.push("document_to_posting");
} }
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) { if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
patterns.push("payment_to_settlement"); patterns.push("payment_to_settlement");
} }
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) { if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
patterns.push("statement_to_document"); patterns.push("statement_to_document");
} }
if (/РѕСЃРЅРѕРІРЅ|fixed_asset|амортиз/i.test(corpus)) { if (/основн|fixed_asset|амортиз/i.test(corpus)) {
patterns.push("asset_card_to_depreciation"); patterns.push("asset_card_to_depreciation");
} }
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) { if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
patterns.push("deferred_expense_to_writeoff"); patterns.push("deferred_expense_to_writeoff");
} }
if (/РЅРґСЃ|счетфактур|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
patterns.push("invoice_to_vat"); patterns.push("invoice_to_vat");
} }
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) { if (/договор|contract/i.test(corpus) && hasDocLinks) {
patterns.push("contract_to_documents"); patterns.push("contract_to_documents");
} }
if (/склад|товар|материал|receipt/i.test(corpus)) { if (/склад|товар|материал|receipt/i.test(corpus)) {
patterns.push("receipt_to_stock_movement"); patterns.push("receipt_to_stock_movement");
} }
return uniqueStrings(patterns); return uniqueStrings(patterns);
@ -1899,7 +1899,7 @@ function inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) { if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
anomalies.push("posting_mismatch"); anomalies.push("posting_mismatch");
} }
if (/ручн|manual|корректировк/.test(corpus)) { if (/ручн|manual|корректировк/.test(corpus)) {
anomalies.push("manual_intervention_suspicion"); anomalies.push("manual_intervention_suspicion");
} }
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) { if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
@ -1911,7 +1911,7 @@ function inferAnomalyPatterns(record, corpus, relationPatterns, lifecycleMarkers
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) { if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
anomalies.push("silent_orphan"); anomalies.push("silent_orphan");
} }
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|СЃСѓРјРј|РёСРѕРіРѕ|amount/i.test(key)); const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
if (!hasAmountSignal && anomalies.length > 0) { if (!hasAmountSignal && anomalies.length > 0) {
anomalies.push("amount_independent_risk"); anomalies.push("amount_independent_risk");
} }
@ -1954,15 +1954,15 @@ function evaluateExcludedInterpretations(profile, signals, record) {
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"]; const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item)); const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (!hasStructural) { if (!hasStructural) {
reasons.push("Исключено как simple_payment_delay без структурного дефекта."); reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
} }
} }
if (interpretationSet.has("amount_only_anomaly")) { if (interpretationSet.has("amount_only_anomaly")) {
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|СЃСѓРјРј|РёСРѕРіРѕ|amount/i.test(key)); const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"]; const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item)); const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (hasAmountSignal && !hasStructural) { if (hasAmountSignal && !hasStructural) {
reasons.push("Исключено как amount-only аномалия без структурных признаков."); reasons.push("Исключено как amount-only аномалия без структурных признаков.");
} }
} }
return { return {
@ -2016,22 +2016,22 @@ function evaluateRecordAgainstProfile(record, profile) {
const graphTraversal = evaluateGraphTraversalForRecord(profile, signals); const graphTraversal = evaluateGraphTraversalForRecord(profile, signals);
const matchReasons = []; const matchReasons = [];
if (accountMatch && profile.account_scope.length > 0) { if (accountMatch && profile.account_scope.length > 0) {
matchReasons.push("Совпал account_scope."); matchReasons.push("Совпал account_scope.");
} }
if (domainMatch && profile.domain_scope.length > 0) { if (domainMatch && profile.domain_scope.length > 0) {
matchReasons.push("Совпал domain_scope."); matchReasons.push("Совпал domain_scope.");
} }
if (documentMatch && profile.document_types.length > 0) { if (documentMatch && profile.document_types.length > 0) {
matchReasons.push("Совпал document_types."); matchReasons.push("Совпал document_types.");
} }
if (relationMatch && profile.relation_patterns.length > 0) { if (relationMatch && profile.relation_patterns.length > 0) {
matchReasons.push("Совпали relation_patterns."); matchReasons.push("Совпали relation_patterns.");
} }
if (anomalyMatch && profile.anomaly_patterns.length > 0) { if (anomalyMatch && profile.anomaly_patterns.length > 0) {
matchReasons.push("Совпали anomaly_patterns."); matchReasons.push("Совпали anomaly_patterns.");
} }
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) { if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
matchReasons.push("Совпал lifecycle_stage_filters."); matchReasons.push("Совпал lifecycle_stage_filters.");
} }
if (graphTraversal.domain_match) { if (graphTraversal.domain_match) {
matchReasons.push("Graph traversal domain matched."); matchReasons.push("Graph traversal domain matched.");
@ -2206,7 +2206,7 @@ class AssistantDataLayer {
business_interpretation: [], business_interpretation: [],
confidence: "low", confidence: "low",
limitations: ["Snapshot data files could not be loaded."], limitations: ["Snapshot data files could not be loaded."],
errors: ["Слой данных недоступен: РЅРµ удалось загрузить snapshot-файлы."] errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
}; };
} }
let result = null; let result = null;
@ -3037,8 +3037,8 @@ class AssistantDataLayer {
? "medium" ? "medium"
: "low", : "low",
business_interpretation: group.risk_factors.size > 0 business_interpretation: group.risk_factors.size > 0
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно." ? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
: "Есть связанная операционная цепочка, РЅРѕ явные СЂРёСЃРє-паттерны выражены слабо.", : "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
relation_types: Array.from(group.relations.entries()) relation_types: Array.from(group.relations.entries())
.sort((left, right) => right[1] - left[1]) .sort((left, right) => right[1] - left[1])
.map((item) => item[0]), .map((item) => item[0]),
@ -3086,24 +3086,24 @@ class AssistantDataLayer {
evidence: [], evidence: [],
why_included: [], why_included: [],
selection_reason: [ selection_reason: [
"РџРѕРёСЃРє строился РїРѕ semantic retrieval profile, РЅРѕ подходящие контрагенты РЅРµ найдены.", "Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.", "Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.", domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
guidFilter.length > 0 guidFilter.length > 0
? "GUID-фильтрация включена." ? "GUID-фильтрация включена."
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`, : `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
`Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`, `Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.` `Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
], ],
risk_factors: semanticProfile.anomaly_patterns, risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: [ business_interpretation: [
"РџРѕ текущему профилю запроса устойчивых разрывов цепочки РЅРµ обнаружено.", "По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
"Для точечного drilldown добавьте GUID или уточните период/контрагента." "Для точечного drilldown добавьте GUID или уточните период/контрагента."
], ],
confidence: "medium", confidence: "medium",
limitations: [ limitations: [
guidFilter.length > 0 ? "РџРѕРёСЃРє ограничен переданными GUID." : "РџРѕРёСЃРє выполнен РїРѕ semantic narrowing без GUID.", guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
"Источник данных — snapshot 2020 (read-only), Р° РЅРµ live состояние базы 1РЎ.", "Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -3144,33 +3144,33 @@ class AssistantDataLayer {
}, },
evidence: evidence.slice(0, 12), evidence: evidence.slice(0, 12),
why_included: [ why_included: [
`Семантическое сужение выполнено РїРѕ профилю ${semanticProfile.query_subject}.`, `Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.", domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
semanticProfile.account_scope.length > 0 semanticProfile.account_scope.length > 0
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.` ? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
: "Счета РЅРµ были заданы СЏРІРЅРѕ, использованы domain/document/relation ограничения.", : "Счета не были заданы явно, использованы domain/document/relation ограничения.",
`После narrowing осталось ${filtered.length} РёР· ${sourceRecords.length} записей.`, `После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
`Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.` `Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.`
], ],
selection_reason: [ selection_reason: [
"Отбор основан РЅР° пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.", "Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
"GUID-mode отключен: full scan без ограничителей РЅРµ использовался.", "GUID-mode отключен: full scan без ограничителей не использовался.",
`Ранжирование выполнено РїРѕ basis: ${semanticProfile.ranking_basis.join(", ")}.`, `Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.", domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.",
`Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`, `Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.` `Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
], ],
risk_factors: aggregatedRiskFactors.length > 0 risk_factors: aggregatedRiskFactors.length > 0
? aggregatedRiskFactors ? aggregatedRiskFactors
: ["Высокая плотность операций РїРѕ контрагенту может указывать РЅР° незакрытые цепочки."], : ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
business_interpretation: [ business_interpretation: [
"Результат отражает РЅРµ просто объем операций, Р° структурные признаки разрыва цепочки Рё lifecycle-конфликта.", "Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
"Контрагенты РІ топе приоритетны для проверки РЅР° неверный тип закрывающего документа Рё незавершенные СЃРІСЏР·Рё." "Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
], ],
confidence: "high", confidence: "high",
limitations: [ limitations: [
guidFilter.length > 0 ? "Выборка ограничена GUID РёР· запроса." : "Выборка ограничена semantic retrieval profile.", guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
"Источник данных — snapshot 2020 (read-only), РЅРµ live контур 1РЎ.", "Источник данных — snapshot 2020 (read-only), не live контур 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -3455,12 +3455,12 @@ class AssistantDataLayer {
})), })),
why_included: items.length > 0 why_included: items.length > 0
? [ ? [
"Показаны сущности СЃ максимальным количеством записей.", "Показаны сущности с максимальным количеством записей.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced." domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
] ]
: [], : [],
selection_reason: [ selection_reason: [
"Ранжирование выполнено РїРѕ records_count РїРѕ убыванию.", "Ранжирование выполнено по records_count по убыванию.",
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied." domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
], ],
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]), risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
@ -3469,7 +3469,7 @@ class AssistantDataLayer {
], ],
confidence: "medium", confidence: "medium",
limitations: [ limitations: [
"Ранжирование РїРѕ объему РЅРµ всегда эквивалентно бизнес-СЂРёСЃРєСѓ.", "Ранжирование по объему не всегда эквивалентно бизнес-риску.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -3604,7 +3604,7 @@ class AssistantDataLayer {
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied." domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
], ],
risk_factors: semanticProfile.anomaly_patterns, risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."], business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
confidence: "high", confidence: "high",
limitations: [ limitations: [
"Это read-only snapshot, а не онлайн-состояние 1С.", "Это read-only snapshot, а не онлайн-состояние 1С.",
@ -3634,11 +3634,11 @@ class AssistantDataLayer {
}, },
evidence: [], evidence: [],
why_included: [], why_included: [],
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/СЃСѓРјРјР°/счет)."], selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
risk_factors: [], risk_factors: [],
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."], business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
confidence: "low", confidence: "low",
limitations: ["Добавьте GUID или СЏРєРѕСЂСЏ: номер документа, дату, СЃСѓРјРјСѓ, счет."], limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
errors: [] errors: []
}; };
} }
@ -3686,14 +3686,14 @@ class AssistantDataLayer {
}, },
evidence: matches.slice(0, 10), evidence: matches.slice(0, 10),
why_included: matches.length > 0 why_included: matches.length > 0
? ["Включены source-of-record записи, совпавшие РїРѕ business anchors (номер/дата/СЃСѓРјРјР°/счет)."] ? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
: [], : [],
selection_reason: [ selection_reason: [
"GUID отсутствует, использован business-anchor trace РїРѕ атрибутам документа Рё расчетов." "GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
], ],
risk_factors: [], risk_factors: [],
business_interpretation: [ business_interpretation: [
"Drilldown опирается РЅР° business anchors, поэтому вывод требует первичной проверки РІ source-of-record." "Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
], ],
confidence: matches.length > 0 ? "medium" : "low", confidence: matches.length > 0 ? "medium" : "low",
limitations: [ limitations: [
@ -3722,12 +3722,12 @@ class AssistantDataLayer {
matched_records: matches.length matched_records: matches.length
}, },
evidence: matches.slice(0, 10), evidence: matches.slice(0, 10),
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID РёР· запроса."] : [], why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
selection_reason: ["РџРѕРёСЃРє РїРѕ source_id, linked target_id Рё строковым атрибутам."], selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
risk_factors: [], risk_factors: [],
business_interpretation: ["Результат показывает source-of-record объекты РїРѕ переданным идентификаторам."], business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
confidence: matches.length > 0 ? "high" : "medium", confidence: matches.length > 0 ? "high" : "medium",
limitations: ["РџРѕРёСЃРє ограничен локальным snapshot-пакетом."], limitations: ["Поиск ограничен локальным snapshot-пакетом."],
errors: [] errors: []
}; };
} }

View File

@ -378,7 +378,7 @@ function parseDateLike(raw) {
if (dayMonthYear) { if (dayMonthYear) {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] }); return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
} }
const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|РёСЋРЅСЊ|июль|август|сентябрь|октябрь|РЅРѕСЏР±СЂСЊ|декабрь)\s+(20\d{2})\b/i); const rusMonthYear = value.match(/\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i);
if (rusMonthYear) { if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()]; const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
if (!month) if (!month)
@ -467,7 +467,7 @@ function normalizedAnchorFromFragments(normalized) {
source: `normalized_time_scope:${type || "unknown"}` source: `normalized_time_scope:${type || "unknown"}`
}; };
} }
if (/(?:июл|july|РёСЋР»)/i.test(value)) { if (/(?:июл|july|июл)/i.test(value)) {
return { return {
value: `${JULY_YEAR}-${JULY_MONTH}`, value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}` source: `normalized_time_scope:${type || "unknown"}`
@ -491,7 +491,7 @@ function resolveJulyAnchor(rawText) {
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null; const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i); const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/); const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower); const monthByNamed = /(?:июл|july|июл)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower); const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) { if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return { return {
@ -662,7 +662,7 @@ function applyTemporalHintToExecutionPlan(executionPlan, temporal) {
return item; return item;
} }
const text = String(item.fragment_text ?? "").trim(); const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|РёСЋР»|july/i.test(text)) { if (/2020-07|июл|июл|july/i.test(text)) {
return item; return item;
} }
return { return {
@ -681,7 +681,7 @@ function resolveDomainPolarityGuard(input) {
prefixes.has("62") || prefixes.has("62") ||
prefixes.has("51") || prefixes.has("51") ||
prefixes.has("76") || prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower); /(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|хвост)/i.test(lower);
if (!settlementSignal) { if (!settlementSignal) {
return { return {
applied: false, applied: false,
@ -700,12 +700,12 @@ function resolveDomainPolarityGuard(input) {
reason_codes: [] reason_codes: []
}; };
} }
const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) + const supplierScore = (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) + (prefixes.has("60") ? 2 : 0) +
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0); (/(?:сч[её]т\s*60|по\s*60|счет\s*60)/i.test(lower) ? 1 : 0);
const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) + const customerScore = (/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) + (prefixes.has("62") ? 2 : 0) +
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0); (/(?:сч[её]т\s*62|по\s*62|счет\s*62)/i.test(lower) ? 1 : 0);
let polarity = "mixed_or_unresolved"; let polarity = "mixed_or_unresolved";
if (supplierScore > 0 || customerScore > 0) { if (supplierScore > 0 || customerScore > 0) {
if (supplierScore >= customerScore + 2) { if (supplierScore >= customerScore + 2) {
@ -758,10 +758,10 @@ function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
return item; return item;
} }
const text = String(item.fragment_text ?? "").trim(); const text = String(item.fragment_text ?? "").trim();
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) { if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60)/i.test(text)) {
return item; return item;
} }
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) { if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62)/i.test(text)) {
return item; return item;
} }
return { return {
@ -771,10 +771,10 @@ function applyPolarityHintToExecutionPlan(executionPlan, polarity) {
}); });
} }
function containsReceivableSignal(value) { function containsReceivableSignal(value) {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value); return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
} }
function containsPayableSignal(value) { function containsPayableSignal(value) {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value); return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
} }
function problemUnitCorpus(unit) { function problemUnitCorpus(unit) {
return [ return [
@ -1319,15 +1319,15 @@ function applyEligibilityToGroundingCheck(groundingCheck, eligibility) {
? "no_grounded_answer" ? "no_grounded_answer"
: "partial"; : "partial";
const reasonMap = { const reasonMap = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.", admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие РїРѕ domain/account scope.", critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел Р·Р° РѕРєРЅРѕ company snapshot (июль 2020).", temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor РЅРµ разрешен надежно РІ пределах company snapshot.", temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.", business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "РќРµ удалось надежно определить supplier/customer polarity.", polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity РІ retrieval-контуре.", polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.", claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition РЅРµ дал допустимых попаданий РїРѕ claim target path." targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
}; };
const reasons = [ const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []), ...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),

View File

@ -65,6 +65,8 @@ const addressFilterExtractor_1 = __importStar(require("./addressFilterExtractor"
const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract")); const predecomposeContract_1 = __importStar(require("./address_runtime/predecomposeContract"));
const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient")); const openaiResponsesClient_1 = __importStar(require("./openaiResponsesClient"));
const addressMcpClient_1 = __importStar(require("./addressMcpClient")); const addressMcpClient_1 = __importStar(require("./addressMcpClient"));
const capabilitiesRegistry_1 = __importStar(require("./capabilitiesRegistry"));
const assistantCanon_1 = __importStar(require("./assistantCanon"));
const iconv_lite_1 = __importDefault(require("iconv-lite")); const iconv_lite_1 = __importDefault(require("iconv-lite"));
const DATA_SCOPE_CACHE_TTL_MS = 60_000; const DATA_SCOPE_CACHE_TTL_MS = 60_000;
const dataScopeProbeCache = new Map(); const dataScopeProbeCache = new Map();
@ -3903,17 +3905,7 @@ function buildLivingChatPrompt(userMessage, conversationWindow) {
return `${contextBlock}Сообщение пользователя:\n${userMessage}`; return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
} }
function buildAssistantCapabilityContractReply() { function buildAssistantCapabilityContractReply() {
return [ return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею сейчас:",
"1. Находить документы, операции, договоры и остатки по контрагенту/договору/периоду.",
"2. Делать агрегаты по базе: активность, роли контрагентов, top-срезы по суммам и операциям.",
"3. Кратко объяснять результат и подсказывать следующий точный запрос.",
"Что не умею:",
"1. Не настраиваю 1С и не меняю конфигурацию.",
"2. Не создаю и не провожу документы в базе.",
"3. Не выполняю админские действия на сервере."
].join("\n");
} }
function normalizeScopeLabel(value) { function normalizeScopeLabel(value) {
const repaired = repairAddressMojibake(String(value ?? "")); const repaired = repairAddressMojibake(String(value ?? ""));
@ -5016,6 +5008,7 @@ class AssistantService {
else { else {
const conversationWindow = buildLivingChatContextWindow(session.items); const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({ const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider, llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""), apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
@ -5029,7 +5022,8 @@ class AssistantService {
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.", "Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.", "Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.", "Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
"Если пользователь спрашивает про возможности, отвечай только по этому контракту." "Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
`Канон поведения: ${canonExcerpt}`
].join(" "), ].join(" "),
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.", developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
userMessage: userPrompt, userMessage: userPrompt,

View File

@ -0,0 +1,182 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadCapabilitiesRegistry = loadCapabilitiesRegistry;
exports.buildCapabilityContractReplyFromRegistry = buildCapabilityContractReplyFromRegistry;
exports.resolveNearestCapabilityGroup = resolveNearestCapabilityGroup;
const fs_1 = __importDefault(require("fs"));
const config_1 = require("../config");
const FALLBACK_REGISTRY = {
schema_version: "capabilities_registry_fallback_v1",
updated_at: "2026-04-09T00:00:00.000Z",
assistant_mode: "read_only",
groups: [
{
group_code: "vat",
group_title: "НДС",
description: "Срезы и расчеты НДС на базе данных 1С.",
risk_level: "high",
maturity_status: "partial",
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
unsupported_operations: ["submit_tax_declaration"],
required_entities: ["period", "organization"],
optional_entities: ["counterparty"],
typical_queries: ["Сколько НДС к уплате за период?"],
related_routes: [],
safe_alternatives: ["Показать движения по 68/19 за период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "counterparties",
group_title: "Контрагенты",
description: "Документы, операции, договоры и срезы по контрагентам.",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
unsupported_operations: ["edit_counterparty_card"],
required_entities: ["counterparty_scope_or_contract"],
optional_entities: ["period", "organization"],
typical_queries: ["Покажи документы по контрагенту"],
related_routes: [],
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
one_c_hints: ["Справочник.Контрагенты"]
},
{
group_code: "boundaries",
group_title: "Ограничения",
description: "Операции, которые ассистент не выполняет.",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
required_entities: [],
optional_entities: [],
typical_queries: ["Можешь настроить 1С?"],
related_routes: [],
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
one_c_hints: []
}
]
};
let cache = null;
function toRecord(value) {
if (!value || typeof value !== "object" || Array.isArray(value))
return null;
return value;
}
function toStringSafe(value) {
if (typeof value !== "string")
return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toArray(value) {
return Array.isArray(value) ? value : [];
}
function readRegistryFromFile() {
if (!fs_1.default.existsSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE))
return null;
try {
const raw = fs_1.default.readFileSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
const parsed = JSON.parse(raw);
const root = toRecord(parsed);
if (!root)
return null;
const groups = toArray(root.groups)
.map((item) => toRecord(item))
.filter((item) => item !== null)
.map((item) => ({
group_code: toStringSafe(item.group_code) ?? "unknown_group",
group_title: toStringSafe(item.group_title) ?? "Группа",
description: toStringSafe(item.description) ?? "",
risk_level: toStringSafe(item.risk_level) ?? "medium",
maturity_status: toStringSafe(item.maturity_status) ??
"partial",
supported_operations: toArray(item.supported_operations)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
unsupported_operations: toArray(item.unsupported_operations)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
required_entities: toArray(item.required_entities)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
optional_entities: toArray(item.optional_entities)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
typical_queries: toArray(item.typical_queries)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
related_routes: toArray(item.related_routes)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
safe_alternatives: toArray(item.safe_alternatives)
.map((v) => toStringSafe(v))
.filter((v) => v !== null),
one_c_hints: toArray(item.one_c_hints)
.map((v) => toStringSafe(v))
.filter((v) => v !== null)
}));
if (groups.length === 0)
return null;
return {
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
assistant_mode: toStringSafe(root.assistant_mode) ?? "read_only",
groups
};
}
catch {
return null;
}
}
function loadCapabilitiesRegistry() {
try {
const mtimeMs = fs_1.default.existsSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE)
? fs_1.default.statSync(config_1.ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
: -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.value;
}
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
cache = { mtimeMs, value };
return value;
}
catch {
return cache?.value ?? FALLBACK_REGISTRY;
}
}
function buildCapabilityContractReplyFromRegistry() {
const registry = loadCapabilitiesRegistry();
const topGroups = registry.groups.slice(0, 6);
const groupLines = topGroups.map((group, index) => {
const ops = group.supported_operations.slice(0, 3).join(", ");
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
});
return [
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею по группам:",
...groupLines,
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
].join("\n");
}
function resolveNearestCapabilityGroup(input) {
const registry = loadCapabilitiesRegistry();
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
if (!haystack.trim())
return null;
const scoring = registry.groups.map((group) => {
let score = 0;
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
if (bucket.includes(token))
score += 1;
}
return { group, score };
});
scoring.sort((a, b) => b.score - a.score);
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
}

View File

@ -127,53 +127,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "initiated_payment", state_code: "initiated_payment",
state_label: "Платеж инициирован", state_label: "Платеж инициирован",
state_class: "initial", state_class: "initial",
entry_conditions: ["payment_order_created"], entry_conditions: ["payment_order_created"],
exit_conditions: ["bank_recorded"], exit_conditions: ["bank_recorded"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Есть инициирование платежа." business_meaning: "Есть инициирование платежа."
}, },
{ {
state_code: "bank_recorded", state_code: "bank_recorded",
state_label: "Платеж отражен банком", state_label: "Платеж отражен банком",
state_class: "active", state_class: "active",
entry_conditions: ["bank_statement_recorded"], entry_conditions: ["bank_statement_recorded"],
exit_conditions: ["settlement_linked"], exit_conditions: ["settlement_linked"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие." business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
}, },
{ {
state_code: "settlement_closed", state_code: "settlement_closed",
state_label: "Расчет закрыт", state_label: "Расчет закрыт",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["payment_to_settlement_linked"], entry_conditions: ["payment_to_settlement_linked"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Платеж доведен РґРѕ расчетного результата." business_meaning: "Платеж доведен до расчетного результата."
}, },
{ {
state_code: "stale_unlinked_payment", state_code: "stale_unlinked_payment",
state_label: "Платеж завис без закрытия", state_label: "Платеж завис без закрытия",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["bank_recorded", "missing_link"], entry_conditions: ["bank_recorded", "missing_link"],
exit_conditions: ["settlement_closed"], exit_conditions: ["settlement_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Платеж отражен, РЅРѕ ожидаемая СЃРІСЏР·СЊ РїРѕ расчету РЅРµ завершена." business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
}, },
{ {
state_code: "misclosed_payment", state_code: "misclosed_payment",
state_label: "Платеж закрыт некорректно", state_label: "Платеж закрыт некорректно",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["wrong_document_type_or_posting_mismatch"], entry_conditions: ["wrong_document_type_or_posting_mismatch"],
exit_conditions: ["settlement_closed"], exit_conditions: ["settlement_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Формальное закрытие есть, РЅРѕ путь закрытия неверный." business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
} }
], ],
transitions: [ transitions: [
@ -184,7 +184,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
required_evidence: ["bank_statement_recorded"], required_evidence: ["bank_statement_recorded"],
optional_evidence: ["payment_order"], optional_evidence: ["payment_order"],
forbidden_conditions: [], forbidden_conditions: [],
business_meaning: "Платеж должен появиться РІРѕ выписке." business_meaning: "Платеж должен появиться во выписке."
}, },
{ {
from_state: "bank_recorded", from_state: "bank_recorded",
@ -193,7 +193,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
required_evidence: ["payment_to_settlement_link"], required_evidence: ["payment_to_settlement_link"],
optional_evidence: ["document_to_posting"], optional_evidence: ["document_to_posting"],
forbidden_conditions: ["wrong_document_type"], forbidden_conditions: ["wrong_document_type"],
business_meaning: "После выписки должен закрываться расчет." business_meaning: "После выписки должен закрываться расчет."
} }
], ],
defects: [] defects: []
@ -205,43 +205,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "invoice_issued", state_code: "invoice_issued",
state_label: "Реализация отражена", state_label: "Реализация отражена",
state_class: "initial", state_class: "initial",
entry_conditions: ["realization_document_exists"], entry_conditions: ["realization_document_exists"],
exit_conditions: ["payment_recorded"], exit_conditions: ["payment_recorded"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Возникла дебиторская позиция." business_meaning: "Возникла дебиторская позиция."
}, },
{ {
state_code: "payment_recorded", state_code: "payment_recorded",
state_label: "Оплата отражена", state_label: "Оплата отражена",
state_class: "active", state_class: "active",
entry_conditions: ["payment_document_exists"], entry_conditions: ["payment_document_exists"],
exit_conditions: ["receivable_closed"], exit_conditions: ["receivable_closed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Оплата есть, ожидается корректное закрытие." business_meaning: "Оплата есть, ожидается корректное закрытие."
}, },
{ {
state_code: "receivable_closed", state_code: "receivable_closed",
state_label: "Дебиторка закрыта", state_label: "Дебиторка закрыта",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["closing_document_linked"], entry_conditions: ["closing_document_linked"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Дебиторская позиция закрыта корректно." business_meaning: "Дебиторская позиция закрыта корректно."
}, },
{ {
state_code: "stale_receivable", state_code: "stale_receivable",
state_label: "Дебиторка зависла", state_label: "Дебиторка зависла",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["unresolved_settlement"], entry_conditions: ["unresolved_settlement"],
exit_conditions: ["receivable_closed"], exit_conditions: ["receivable_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Позиция остается незавершенной дольше ожидаемого." business_meaning: "Позиция остается незавершенной дольше ожидаемого."
} }
], ],
transitions: [ transitions: [
@ -252,7 +252,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
required_evidence: ["payment_document_exists"], required_evidence: ["payment_document_exists"],
optional_evidence: [], optional_evidence: [],
forbidden_conditions: [], forbidden_conditions: [],
business_meaning: "После реализации ожидается оплата/зачет." business_meaning: "После реализации ожидается оплата/зачет."
}, },
{ {
from_state: "payment_recorded", from_state: "payment_recorded",
@ -261,7 +261,7 @@ const LIFECYCLE_DOMAIN_MODELS = {
required_evidence: ["closing_document_linked"], required_evidence: ["closing_document_linked"],
optional_evidence: ["register_movement_exists"], optional_evidence: ["register_movement_exists"],
forbidden_conditions: ["cross_branch_inconsistency"], forbidden_conditions: ["cross_branch_inconsistency"],
business_meaning: "Оплата должна завершаться корректным закрытием расчета." business_meaning: "Оплата должна завершаться корректным закрытием расчета."
} }
], ],
defects: [] defects: []
@ -273,43 +273,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "recognized", state_code: "recognized",
state_label: "РБП признан", state_label: "РБП признан",
state_class: "initial", state_class: "initial",
entry_conditions: ["deferred_expense_created"], entry_conditions: ["deferred_expense_created"],
exit_conditions: ["writeoff_started"], exit_conditions: ["writeoff_started"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "РБП поставлен РЅР° учет." business_meaning: "РБП поставлен на учет."
}, },
{ {
state_code: "partially_written_off", state_code: "partially_written_off",
state_label: "Частичное списание", state_label: "Частичное списание",
state_class: "active", state_class: "active",
entry_conditions: ["partial_writeoff_exists"], entry_conditions: ["partial_writeoff_exists"],
exit_conditions: ["fully_written_off"], exit_conditions: ["fully_written_off"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Списание идет РїРѕ графику." business_meaning: "Списание идет по графику."
}, },
{ {
state_code: "fully_written_off", state_code: "fully_written_off",
state_label: "РБП полностью списан", state_label: "РБП полностью списан",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["full_writeoff_exists"], entry_conditions: ["full_writeoff_exists"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "РБП завершил lifecycle." business_meaning: "РБП завершил lifecycle."
}, },
{ {
state_code: "overdue_writeoff", state_code: "overdue_writeoff",
state_label: "Просроченное списание", state_label: "Просроченное списание",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["period_boundary", "missing_link"], entry_conditions: ["period_boundary", "missing_link"],
exit_conditions: ["fully_written_off"], exit_conditions: ["fully_written_off"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "РБП живет дольше допустимого РѕРєРЅР°." business_meaning: "РБП живет дольше допустимого окна."
} }
], ],
transitions: [], transitions: [],
@ -322,53 +322,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "capitalized", state_code: "capitalized",
state_label: "Капвложения отражены", state_label: "Капвложения отражены",
state_class: "initial", state_class: "initial",
entry_conditions: ["capitalization_document_exists"], entry_conditions: ["capitalization_document_exists"],
exit_conditions: ["accepted_for_accounting"], exit_conditions: ["accepted_for_accounting"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Объект зафиксирован как вложение." business_meaning: "Объект зафиксирован как вложение."
}, },
{ {
state_code: "accepted_for_accounting", state_code: "accepted_for_accounting",
state_label: "РџСЂРёРЅСЏС‚ Рє учету", state_label: "Принят к учету",
state_class: "active", state_class: "active",
entry_conditions: ["acceptance_document_exists"], entry_conditions: ["acceptance_document_exists"],
exit_conditions: ["depreciation_active"], exit_conditions: ["depreciation_active"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Объект переведен РІ РѕСЃРЅРѕРІРЅРѕР№ контур учета." business_meaning: "Объект переведен в основной контур учета."
}, },
{ {
state_code: "depreciation_active", state_code: "depreciation_active",
state_label: "Амортизация активна", state_label: "Амортизация активна",
state_class: "active", state_class: "active",
entry_conditions: ["depreciation_register_movement"], entry_conditions: ["depreciation_register_movement"],
exit_conditions: ["disposed"], exit_conditions: ["disposed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Жизненный цикл РћРЎ идет штатно." business_meaning: "Жизненный цикл ОС идет штатно."
}, },
{ {
state_code: "contradictory_asset_state", state_code: "contradictory_asset_state",
state_label: "Противоречивый статус РћРЎ", state_label: "Противоречивый статус ОС",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["posting_mismatch_or_wrong_path"], entry_conditions: ["posting_mismatch_or_wrong_path"],
exit_conditions: ["depreciation_active"], exit_conditions: ["depreciation_active"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Статус РћРЎ формально есть, РЅРѕ смыслово противоречив." business_meaning: "Статус ОС формально есть, но смыслово противоречив."
}, },
{ {
state_code: "disposed", state_code: "disposed",
state_label: "Выбыл", state_label: "Выбыл",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["disposal_document_exists"], entry_conditions: ["disposal_document_exists"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Жизненный цикл РћРЎ завершен." business_meaning: "Жизненный цикл ОС завершен."
} }
], ],
transitions: [], transitions: [],
@ -381,43 +381,43 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "vat_registered", state_code: "vat_registered",
state_label: "НДС отражен документно", state_label: "НДС отражен документно",
state_class: "initial", state_class: "initial",
entry_conditions: ["invoice_registered"], entry_conditions: ["invoice_registered"],
exit_conditions: ["vat_reflected"], exit_conditions: ["vat_reflected"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Сформирован первичный документный слой НДС." business_meaning: "Сформирован первичный документный слой НДС."
}, },
{ {
state_code: "vat_reflected", state_code: "vat_reflected",
state_label: "НДС отражен РІ учете", state_label: "НДС отражен в учете",
state_class: "active", state_class: "active",
entry_conditions: ["vat_register_movement"], entry_conditions: ["vat_register_movement"],
exit_conditions: ["vat_deducted"], exit_conditions: ["vat_deducted"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "НДС РїСЂРѕС…РѕРґРёС‚ штатную стадию отражения." business_meaning: "НДС проходит штатную стадию отражения."
}, },
{ {
state_code: "vat_deducted", state_code: "vat_deducted",
state_label: "НДС РїСЂРёРЅСЏС‚ Рє вычету", state_label: "НДС принят к вычету",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["deduction_confirmed"], entry_conditions: ["deduction_confirmed"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "НДС-цепочка завершена корректно." business_meaning: "НДС-цепочка завершена корректно."
}, },
{ {
state_code: "vat_conflict", state_code: "vat_conflict",
state_label: "Конфликт НДС-цепочки", state_label: "Конфликт НДС-цепочки",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["cross_branch_inconsistency"], entry_conditions: ["cross_branch_inconsistency"],
exit_conditions: ["vat_reflected"], exit_conditions: ["vat_reflected"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Бухгалтерская Рё налоговая ветки расходятся." business_meaning: "Бухгалтерская и налоговая ветки расходятся."
} }
], ],
transitions: [], transitions: [],
@ -430,53 +430,53 @@ const LIFECYCLE_DOMAIN_MODELS = {
states: [ states: [
{ {
state_code: "preclose_checks", state_code: "preclose_checks",
state_label: "Предзакрытие", state_label: "Предзакрытие",
state_class: "active", state_class: "active",
entry_conditions: ["period_scope_detected"], entry_conditions: ["period_scope_detected"],
exit_conditions: ["close_ready"], exit_conditions: ["close_ready"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Идет проверка готовности периода." business_meaning: "Идет проверка готовности периода."
}, },
{ {
state_code: "close_ready", state_code: "close_ready",
state_label: "Готов Рє закрытию", state_label: "Готов к закрытию",
state_class: "active", state_class: "active",
entry_conditions: ["no_blockers_detected"], entry_conditions: ["no_blockers_detected"],
exit_conditions: ["close_completed"], exit_conditions: ["close_completed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Период может быть закрыт." business_meaning: "Период может быть закрыт."
}, },
{ {
state_code: "close_completed", state_code: "close_completed",
state_label: "Закрытие завершено", state_label: "Закрытие завершено",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["close_operation_done"], entry_conditions: ["close_operation_done"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Период закрыт." business_meaning: "Период закрыт."
}, },
{ {
state_code: "close_blocked", state_code: "close_blocked",
state_label: "Закрытие заблокировано", state_label: "Закрытие заблокировано",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["period_close_risk_or_stale_state"], entry_conditions: ["period_close_risk_or_stale_state"],
exit_conditions: ["close_ready"], exit_conditions: ["close_ready"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Есть lifecycle-дефекты, влияющие РЅР° закрытие." business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
}, },
{ {
state_code: "close_contradicted", state_code: "close_contradicted",
state_label: "Закрыт формально, РЅРѕ СЃ противоречием", state_label: "Закрыт формально, но с противоречием",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["misclosed_or_cross_branch_conflict"], entry_conditions: ["misclosed_or_cross_branch_conflict"],
exit_conditions: ["close_completed"], exit_conditions: ["close_completed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Формальное закрытие РЅРµ согласовано СЃ фактическими ветками." business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
} }
], ],
transitions: [], transitions: [],
@ -488,7 +488,7 @@ const SHARED_DEFECTS = [
defect_code: "missing_expected_transition", defect_code: "missing_expected_transition",
defect_class: "path", defect_class: "path",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Ожидаемый переход РЅРµ произошел.", business_meaning: "Ожидаемый переход не произошел.",
evidence_requirements: ["expected_state", "missing_transition_signal"], evidence_requirements: ["expected_state", "missing_transition_signal"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -496,7 +496,7 @@ const SHARED_DEFECTS = [
defect_code: "invalid_transition", defect_code: "invalid_transition",
defect_class: "path", defect_class: "path",
severity_hint: "high", severity_hint: "high",
business_meaning: "Переход произошел РїРѕ некорректному пути.", business_meaning: "Переход произошел по некорректному пути.",
evidence_requirements: ["invalid_transition_signal"], evidence_requirements: ["invalid_transition_signal"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -504,7 +504,7 @@ const SHARED_DEFECTS = [
defect_code: "stale_active_state", defect_code: "stale_active_state",
defect_class: "timing", defect_class: "timing",
severity_hint: "high", severity_hint: "high",
business_meaning: "Объект завис РІ активном состоянии.", business_meaning: "Объект завис в активном состоянии.",
evidence_requirements: ["stale_marker", "missing_transition_signal"], evidence_requirements: ["stale_marker", "missing_transition_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -512,7 +512,7 @@ const SHARED_DEFECTS = [
defect_code: "contradictory_state", defect_code: "contradictory_state",
defect_class: "consistency", defect_class: "consistency",
severity_hint: "high", severity_hint: "high",
business_meaning: "Статусы объекта противоречат РґСЂСѓРі РґСЂСѓРіСѓ.", business_meaning: "Статусы объекта противоречат друг другу.",
evidence_requirements: ["contradiction_signal"], evidence_requirements: ["contradiction_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -520,7 +520,7 @@ const SHARED_DEFECTS = [
defect_code: "premature_terminal_state", defect_code: "premature_terminal_state",
defect_class: "closure", defect_class: "closure",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Терминальное состояние наступило преждевременно.", business_meaning: "Терминальное состояние наступило преждевременно.",
evidence_requirements: ["terminal_state", "missing_required_previous_state"], evidence_requirements: ["terminal_state", "missing_required_previous_state"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -528,7 +528,7 @@ const SHARED_DEFECTS = [
defect_code: "misclosed_state", defect_code: "misclosed_state",
defect_class: "closure", defect_class: "closure",
severity_hint: "high", severity_hint: "high",
business_meaning: "Контур формально закрыт, РЅРѕ закрыт неверно.", business_meaning: "Контур формально закрыт, но закрыт неверно.",
evidence_requirements: ["wrong_closure_path"], evidence_requirements: ["wrong_closure_path"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -536,7 +536,7 @@ const SHARED_DEFECTS = [
defect_code: "orphan_intermediate_state", defect_code: "orphan_intermediate_state",
defect_class: "path", defect_class: "path",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Промежуточная стадия осталась без корректного продолжения.", business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
evidence_requirements: ["intermediate_state_without_next"], evidence_requirements: ["intermediate_state_without_next"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -544,7 +544,7 @@ const SHARED_DEFECTS = [
defect_code: "cross_branch_state_conflict", defect_code: "cross_branch_state_conflict",
defect_class: "consistency", defect_class: "consistency",
severity_hint: "high", severity_hint: "high",
business_meaning: "Состояния соседних веток учета противоречат РґСЂСѓРі РґСЂСѓРіСѓ.", business_meaning: "Состояния соседних веток учета противоречат друг другу.",
evidence_requirements: ["cross_branch_conflict_signal"], evidence_requirements: ["cross_branch_conflict_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
} }
@ -845,23 +845,23 @@ function staleDurationHint(domain, defect, input) {
return "unknown_snapshot_window"; return "unknown_snapshot_window";
} }
function lifecycleInterpretation(input) { function lifecycleInterpretation(input) {
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`; const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
if (input.defect === "stale_active_state") { if (input.defect === "stale_active_state") {
return `${base} Объект завис РІРѕ времени Рё РЅРµ дошел РґРѕ ожидаемого перехода.`; return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
} }
if (input.defect === "misclosed_state") { if (input.defect === "misclosed_state") {
return `${base} Контур закрыт формально, РЅРѕ путь закрытия противоречит бухгалтерской логике.`; return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
} }
if (input.defect === "cross_branch_state_conflict") { if (input.defect === "cross_branch_state_conflict") {
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`; return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
} }
if (input.defect === "missing_expected_transition") { if (input.defect === "missing_expected_transition") {
return `${base} РќРµ зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`; return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
} }
if (input.defect === "invalid_transition") { if (input.defect === "invalid_transition") {
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`; return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
} }
return `${base} Lifecycle-разрешение РЅРµ выявило критичный дефект, РЅРѕ состояние требует наблюдения.`; return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
} }
function resolveLifecycle(input) { function resolveLifecycle(input) {
const lifecycle_domain = inferLifecycleDomain(input); const lifecycle_domain = inferLifecycleDomain(input);

View File

@ -67,9 +67,9 @@ const BUILTIN_PROMPT_PRESETS = {
}, },
normalizer_v1_1_2_1: { normalizer_v1_1_2_1: {
id: "default-normalizer-v1_1_2_1", id: "default-normalizer-v1_1_2_1",
name: "Стандартный пресет NDC v1.1.2.1", name: "Стандартный пресет NDC v1.1.2.1",
promptVersion: "normalizer_v1_1_2_1", promptVersion: "normalizer_v1_1_2_1",
schemaNotes: "v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.", schemaNotes: "v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.",
files: { files: {
system: path_1.default.join("system", "default.txt"), system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v1_1_2_1.txt"), developer: path_1.default.join("developer", "normalizer_v1_1_2_1.txt"),
@ -79,9 +79,9 @@ const BUILTIN_PROMPT_PRESETS = {
}, },
normalizer_v2: { normalizer_v2: {
id: "default-normalizer-v2", id: "default-normalizer-v2",
name: "Стандартный пресет NDC v2", name: "Стандартный пресет NDC v2",
promptVersion: "normalizer_v2", promptVersion: "normalizer_v2",
schemaNotes: "v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.", schemaNotes: "v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
files: { files: {
system: path_1.default.join("system", "default.txt"), system: path_1.default.join("system", "default.txt"),
developer: path_1.default.join("developer", "normalizer_v2.txt"), developer: path_1.default.join("developer", "normalizer_v2.txt"),

View File

@ -38,7 +38,7 @@ const PERIOD_IMPACT_PATTERN = /(?:period\s*close|month\s*close|month-end|residua
const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i; const CAUSAL_PATTERN = /(?:\bwhy\b|\bbecause\b|\breason\b|explain\s+mechanism|почему|объясни|механизм|причин)/i;
const AMBIGUITY_PATTERN = /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i; const AMBIGUITY_PATTERN = /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i; const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i; const DOMAIN_LEXICAL_ANCHOR_PATTERN = /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
exports.ROUTE_DISCIPLINE_RULE_TABLE = [ exports.ROUTE_DISCIPLINE_RULE_TABLE = [
{ {
query_class: "exact_object_trace", query_class: "exact_object_trace",

View File

@ -134,9 +134,28 @@ export const TRACES_DIR = path.resolve(DATA_DIR, "traces");
export const PRESETS_DIR = path.resolve(DATA_DIR, "presets"); export const PRESETS_DIR = path.resolve(DATA_DIR, "presets");
export const EVAL_CASES_DIR = path.resolve(DATA_DIR, "eval_cases"); export const EVAL_CASES_DIR = path.resolve(DATA_DIR, "eval_cases");
export const ASSISTANT_SESSIONS_DIR = path.resolve(DATA_DIR, "assistant_sessions"); export const ASSISTANT_SESSIONS_DIR = path.resolve(DATA_DIR, "assistant_sessions");
export const AUTORUN_ANNOTATIONS_DIR = path.resolve(DATA_DIR, "autorun_annotations");
export const AUTORUN_ANNOTATIONS_FILE = path.resolve(AUTORUN_ANNOTATIONS_DIR, "annotations.json");
export const AUTORUN_GENERATOR_DIR = path.resolve(DATA_DIR, "autorun_generators");
export const AUTORUN_GENERATOR_HISTORY_FILE = path.resolve(AUTORUN_GENERATOR_DIR, "history.json");
export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts"); export const PROMPTS_DIR = path.resolve(MODULE_ROOT, "prompts");
export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports"); export const REPORTS_DIR = path.resolve(MODULE_ROOT, "reports");
export const EVAL_DATASETS_DIR = path.resolve(MODULE_ROOT, "eval_cases"); export const EVAL_DATASETS_DIR = path.resolve(MODULE_ROOT, "eval_cases");
export const SCHEMAS_DIR = path.resolve(BACKEND_ROOT, "src", "schemas"); export const SCHEMAS_DIR = path.resolve(BACKEND_ROOT, "src", "schemas");
export const ARCH_EXPORT_2020_DIR = path.resolve(MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт"); export const ARCH_EXPORT_2020_DIR = path.resolve(MODULE_ROOT, "..", "docs", "ARCH", "2020экспорт");
export const ASSISTANT_CANON_FILE = path.resolve(MODULE_ROOT, "..", "docs", "TECH", "assistant_canon.md");
export const ASSISTANT_CAPABILITIES_REGISTRY_FILE = path.resolve(
MODULE_ROOT,
"..",
"docs",
"TECH",
"capabilities_registry.json"
);
export const MANUAL_CASE_DECISION_SCHEMA_FILE = path.resolve(
MODULE_ROOT,
"..",
"docs",
"TECH",
"manual_case_decision_schema.json"
);

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,17 @@
import "dotenv/config"; import "dotenv/config";
import cors from "cors"; import cors from "cors";
import express from "express"; import express from "express";
import { PORT, PRESETS_DIR, TRACES_DIR, EVAL_CASES_DIR, REPORTS_DIR, TIMEZONE, ASSISTANT_SESSIONS_DIR } from "./config"; import {
PORT,
PRESETS_DIR,
TRACES_DIR,
EVAL_CASES_DIR,
REPORTS_DIR,
TIMEZONE,
ASSISTANT_SESSIONS_DIR,
AUTORUN_ANNOTATIONS_DIR,
AUTORUN_GENERATOR_DIR
} from "./config";
import { buildAccountingAgentRouter } from "./routes/accountingAgent"; import { buildAccountingAgentRouter } from "./routes/accountingAgent";
import { buildAssistantRouter } from "./routes/assistant"; import { buildAssistantRouter } from "./routes/assistant";
import { buildAutoRunsRouter } from "./routes/autoRuns"; import { buildAutoRunsRouter } from "./routes/autoRuns";
@ -27,6 +37,8 @@ export function createApp(): express.Express {
ensureDir(EVAL_CASES_DIR); ensureDir(EVAL_CASES_DIR);
ensureDir(REPORTS_DIR); ensureDir(REPORTS_DIR);
ensureDir(ASSISTANT_SESSIONS_DIR); ensureDir(ASSISTANT_SESSIONS_DIR);
ensureDir(AUTORUN_ANNOTATIONS_DIR);
ensureDir(AUTORUN_GENERATOR_DIR);
const app = express(); const app = express();
app.use(cors()); app.use(cors());

View File

@ -1368,29 +1368,29 @@ function buildProblemCentricActions(input: {
} }
if (unitTypes.has("broken_chain_segment")) { if (unitTypes.has("broken_chain_segment")) {
actions.push("Проверьте СЃРІСЏР·РєСѓ выписка -> документ -> РїСЂРѕРІРѕРґРєР° РїРѕ проблемным участкам цепочки."); actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
} }
if (unitTypes.has("unresolved_settlement_cluster")) { if (unitTypes.has("unresolved_settlement_cluster")) {
actions.push("Сверьте хвосты РїРѕ расчетам: закрылся ли документ оплаты корректным закрывающим документом."); actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
} }
if (unitTypes.has("period_risk_cluster")) { if (unitTypes.has("period_risk_cluster")) {
actions.push("Оцените влияние дефекта РЅР° закрытие периода Рё корректность регламентных операций."); actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
} }
if (unitTypes.has("cross_branch_inconsistency_cluster")) { if (unitTypes.has("cross_branch_inconsistency_cluster")) {
actions.push("Сверьте противоречия между документами, проводками Рё регистрами РїРѕ НДС/межконтурным СЃРІСЏР·СЏРј."); actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
} }
if (unitTypes.has("lifecycle_anomaly_node")) { if (unitTypes.has("lifecycle_anomaly_node")) {
actions.push("Проверьте lifecycle объекта: ожидаемый этап РЅРµ должен оставаться РІ partially_linked состоянии."); actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
} }
for (const unit of input.units) { for (const unit of input.units) {
if (unit.lifecycle_defect_type === "stale_active_state") { if (unit.lifecycle_defect_type === "stale_active_state") {
actions.push("Проверьте, почему объект завис: ожидаемый переход РЅРµ должен оставаться РІ активной стадии."); actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
} }
if (unit.lifecycle_defect_type === "misclosed_state") { if (unit.lifecycle_defect_type === "misclosed_state") {
actions.push("Проверьте закрывающий документ Рё РїСЂРѕРІРѕРґРєРё: закрытие может быть формальным, РЅРѕ некорректным РїРѕ пути."); actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
} }
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") { if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
actions.push("Сверьте бухгалтерскую Рё смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния."); actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
} }
} }
@ -1400,21 +1400,21 @@ function buildProblemCentricActions(input: {
if (input.mode === "clarification_required") { if (input.mode === "clarification_required") {
if (input.missingAnchors.period) { if (input.missingAnchors.period) {
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура."); actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
actions.push("Уточните счет или РіСЂСѓРїРїСѓ счетов для предметной локализации дефекта."); actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения."); actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
actions.push("Укажите контрагента/РґРѕРіРѕРІРѕСЂ, чтобы проверить хвосты Рё разрывы РЅР° конкретной СЃРІСЏР·РєРµ."); actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
} }
} }
if (input.coverageReport.requirements_uncovered.length > 0) { if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`); actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
} }
return uniqueStrings(actions, 6); return uniqueStrings(actions, 6);
@ -1437,25 +1437,25 @@ function buildProblemCentricClarifications(input: {
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер."); questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
questions.push("Уточните счет или СЃРІСЏР·РєСѓ счетов (например, 51/60), РіРґРµ РІС РѕР¶РёРґР°РµС‚Рµ дефект."); questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/объект, РѕС РєРѕС‚РѕСЂРѕРіРѕ РЅСѓР¶РЅРѕ строить проверку цепочки."); questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или РґРѕРіРѕРІРѕСЂ, РїРѕ которому проверить незакрытую экспозицию."); questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
} }
if (unitTypes.has("broken_chain_segment")) { if (unitTypes.has("broken_chain_segment")) {
questions.push("Уточните участок цепочки: выписка, платежный документ или РїСЂРѕРІРѕРґРєР°."); questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
} }
if (unitTypes.has("period_risk_cluster")) { if (unitTypes.has("period_risk_cluster")) {
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок."); questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
} }
if (unitTypes.has("unresolved_settlement_cluster")) { if (unitTypes.has("unresolved_settlement_cluster")) {
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или РѕР±Р° направления."); questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
} }
if (input.coverageReport.clarification_needed_for.length > 0) { if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`); questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
} }
return uniqueStrings(questions, 6); return uniqueStrings(questions, 6);
@ -1641,13 +1641,13 @@ function detectMissingAnchors(
Boolean(options?.normalizationPeriodExplicit) || Boolean(options?.normalizationPeriodExplicit) ||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors); hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
const hasAccount = const hasAccount =
/(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test( /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(
lower lower
) || hasAccountAnchorInRetrieval(retrievalResults); ) || hasAccountAnchorInRetrieval(retrievalResults);
const hasDocumentOrObject = const hasDocumentOrObject =
/(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower); /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower); const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower); const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return { return {
period: !hasPeriod, period: !hasPeriod,
@ -1674,19 +1674,19 @@ function buildClarificationQuestions(input: {
questions.push("Уточните период проверки (например, июль 2020)."); questions.push("Уточните период проверки (например, июль 2020).");
} }
if (input.missingAnchors.account) { if (input.missingAnchors.account) {
questions.push("Уточните счет или РіСЂСѓРїРїСѓ счетов (например, 19, 60, 62)."); questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
} }
if (input.missingAnchors.documentOrObject) { if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/GUID/конкретный объект для трассировки."); questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
} }
if (input.missingAnchors.counterparty) { if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или РіСЂСѓРїРїСѓ контрагентов."); questions.push("Укажите контрагента или группу контрагентов.");
} }
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) { if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный СЂРёСЃРє."); questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
} }
if (input.coverageReport.clarification_needed_for.length > 0) { if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`); questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
} }
return uniqueStrings(questions, 6); return uniqueStrings(questions, 6);
@ -1701,31 +1701,31 @@ function buildRecommendedActions(input: {
}): string[] { }): string[] {
const actions: string[] = []; const actions: string[] = [];
if (input.mode === "focused_grounded") { if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи РІ учетной базе Рё зафиксируйте итог РІ рабочем файле проверки."); actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
} }
if (input.mode === "broad_partial") { if (input.mode === "broad_partial") {
actions.push("Сузьте запрос РґРѕ периода + счета или периода + документа Рё повторите проверку."); actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
} }
if (input.mode === "clarification_required") { if (input.mode === "clarification_required") {
actions.push("Дайте недостающие СЏРєРѕСЂСЏ (период/счет/объект), иначе сильный factual вывод невозможен."); actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
} }
if (input.coverageReport.requirements_uncovered.length > 0) { if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`); actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
} }
if (input.coverageReport.requirements_partially_covered.length > 0) { if (input.coverageReport.requirements_partially_covered.length > 0) {
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`); actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
} }
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") { if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
actions.push("Добавьте более СѓР·РєРёР№ контекст: тип отклонения, РіСЂСѓРїРїСѓ документов Рё бизнес-участок."); actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
} }
if (input.limitationReasonCodes.includes("snapshot_only")) { if (input.limitationReasonCodes.includes("snapshot_only")) {
actions.push("Сверьте критичные выводы СЃ live source-of-record РІ 1C."); actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
} }
if (input.limitationReasonCodes.includes("weak_source_mapping")) { if (input.limitationReasonCodes.includes("weak_source_mapping")) {
actions.push("Проверьте source mapping для связей document/register РїРѕ указанным ref."); actions.push("Проверьте source mapping для связей document/register по указанным ref.");
} }
if (input.sourceRefs.length > 0) { if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку СЃ ${input.sourceRefs.length} подтвержденных записей Рё сверьте РёС… СЃ первичными документами.`); actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
} }
return uniqueStrings(actions, 6); return uniqueStrings(actions, 6);

View File

@ -0,0 +1,38 @@
import fs from "fs";
import { ASSISTANT_CANON_FILE } from "../config";
const FALLBACK_CANON = [
"Не выдумывай возможности.",
"Не обещай настройку 1С и админ-действия.",
"Не показывай внутренние технические термины пользователю.",
"Говори по-человечески и предлагай ближайший полезный поддерживаемый шаг."
].join(" ");
let cache: { mtimeMs: number; excerpt: string } | null = null;
function stripMarkdown(input: string): string {
return input
.replace(/^#{1,6}\s+/gm, "")
.replace(/[`*_>\-\[\]\(\)]/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export function loadAssistantCanonExcerpt(maxChars = 900): string {
try {
const mtimeMs = fs.existsSync(ASSISTANT_CANON_FILE) ? fs.statSync(ASSISTANT_CANON_FILE).mtimeMs : -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.excerpt;
}
if (!fs.existsSync(ASSISTANT_CANON_FILE)) {
return FALLBACK_CANON;
}
const raw = fs.readFileSync(ASSISTANT_CANON_FILE, "utf-8");
const normalized = stripMarkdown(raw);
const excerpt = normalized.length > maxChars ? `${normalized.slice(0, maxChars).trim()}...` : normalized;
cache = { mtimeMs, excerpt: excerpt || FALLBACK_CANON };
return cache.excerpt;
} catch {
return cache?.excerpt ?? FALLBACK_CANON;
}
}

View File

@ -731,8 +731,8 @@ const P0_DOMAIN_CARDS: P0DomainCard[] = [
/закрыт[а-яё]*\s+период/i, /закрыт[а-яё]*\s+период/i,
/close\s+operation/i, /close\s+operation/i,
/allocation/i, /allocation/i,
/закр/i, /закр/i,
/перио/i, /перио/i,
/\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i, /\u0437\u0430\u043a\u0440\u044b\u0442(?:\u0438|\u0438\u0435|\u044b|)\s*(?:\u043c\u0435\u0441\u044f\u0446|\u0441\u0447\u0435\u0442)/i,
/\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i, /\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442/i,
/\u0437\u0430\u0442\u0440\u0430\u0442/i, /\u0437\u0430\u0442\u0440\u0430\u0442/i,
@ -756,14 +756,14 @@ function parseDateCandidate(value: unknown): number | null {
function extractDate(record: SnapshotRecord): string | null { function extractDate(record: SnapshotRecord): string | null {
const attrs = record.attributes ?? {}; const attrs = record.attributes ?? {};
const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"]; const directKeys = ["Period", "Date", "Дата", "Период", "ДатаСобытия"];
for (const key of directKeys) { for (const key of directKeys) {
if (attrs[key] !== undefined && attrs[key] !== null) { if (attrs[key] !== undefined && attrs[key] !== null) {
return String(attrs[key]); return String(attrs[key]);
} }
} }
for (const [key, value] of Object.entries(attrs)) { for (const [key, value] of Object.entries(attrs)) {
if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) { if (/period|date|дата|период/i.test(key) && typeof value === "string" && value.trim()) {
return value; return value;
} }
} }
@ -795,7 +795,7 @@ function countNavigationLinks(record: SnapshotRecord): number {
function findCounterpartyLinks(record: SnapshotRecord): SnapshotLink[] { function findCounterpartyLinks(record: SnapshotRecord): SnapshotLink[] {
return record.links.filter( return record.links.filter(
(link) => (link) =>
link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|РїРѕРєСѓРї/i.test(link.source_field) link.target_entity === "Counterparty" || /supplier|buyer|counterparty/i.test(link.relation) || /постав|покуп/i.test(link.source_field)
); );
} }
@ -1723,14 +1723,14 @@ function inferPeriodScope(fragmentText: string): SemanticPeriodScope {
granularity: "year" granularity: "year"
}; };
} }
if (/квартал|quarter/i.test(fragmentText)) { if (/квартал|quarter/i.test(fragmentText)) {
return { return {
from: null, from: null,
to: null, to: null,
granularity: "quarter" granularity: "quarter"
}; };
} }
if (/месяц|month|период/i.test(fragmentText)) { if (/месяц|month|период/i.test(fragmentText)) {
return { return {
from: null, from: null,
to: null, to: null,
@ -1808,7 +1808,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
const rankingBasis: string[] = ["closure_risk", "repeatability", "financial_impact"]; const rankingBasis: string[] = ["closure_risk", "repeatability", "financial_impact"];
const explanationFocus: string[] = ["why_selected", "where_chain_breaks", "what_business_risk"]; const explanationFocus: string[] = ["why_selected", "where_chain_breaks", "what_business_risk"];
if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) { if (/банк|выписк|расчетн|платеж|банк|выписк|расчетн|платеж|bank|payment|statement|platezh|vypisk/i.test(lower)) {
pushMany(domainScope, ["bank", "settlements"]); pushMany(domainScope, ["bank", "settlements"]);
pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]); pushMany(documentTypes, ["bank_statement", "payment_order", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
@ -1823,22 +1823,22 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
/(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test( /(?:закрыти[ея]\s+месяц|закрыт[а-яё]*\s+период|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|амортиз|финансовых\s+результат|month\s*close|period\s*close|close\s+period|close\s+operation)/i.test(
lower lower
) || ) ||
(/закр/i.test(lower) && /перио/i.test(lower)); (/закр/i.test(lower) && /перио/i.test(lower));
if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) { if (/постав|постав|supplier|vendor/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["suppliers", "settlements"]); pushMany(domainScope, ["suppliers", "settlements"]);
pushMany(documentTypes, ["supplier_receipt", "settlement_document"]); pushMany(documentTypes, ["supplier_receipt", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]); pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
} }
if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) { if (/покупат|покупат|customer|buyer/i.test(lower) || hasSettlementAccountScope) {
pushMany(domainScope, ["customers", "settlements"]); pushMany(domainScope, ["customers", "settlements"]);
pushMany(documentTypes, ["sales_document", "settlement_document"]); pushMany(documentTypes, ["sales_document", "settlement_document"]);
pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]); pushMany(entityTypes, ["counterparty", "contract", "document", "posting"]);
pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]); pushMany(relationPatterns, ["payment_to_settlement", "contract_to_documents"]);
} }
if ( if (
/РЅРґСЃ|ндс|vat|РєРЅРёРіР° РїРѕРєСѓРїРѕРє|РєРЅРёРіР° продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test( /ндс|vat|книга\s+покупок|книга\s+продаж|счет.?фактур|книг[аи]\s+покуп|книг[аи]\s+продаж|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект/i.test(
lower lower
) || ) ||
hasVatAccountScope hasVatAccountScope
@ -1849,7 +1849,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]); pushMany(relationPatterns, ["invoice_to_vat", "document_to_posting"]);
} }
if ( if (
/РѕСЃ|РѕСЃРЅРѕРІРЅ(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test( /ос|основн(ые|ых)\s+сред|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|основн(ые|ых|ым)?\s+средств|fixed asset|amort|амортиз|амортиз/i.test(
lower lower
) || ) ||
hasFixedAssetAccountScope hasFixedAssetAccountScope
@ -1860,7 +1860,7 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]); pushMany(relationPatterns, ["asset_card_to_depreciation", "document_to_posting"]);
} }
if ( if (
/СЂР±Рї|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) || /рбп|расходы будущих периодов|рбп|расходы\s+будущих\s+периодов|deferred|writeoff/i.test(lower) ||
hasDeferredExpenseAccountScope hasDeferredExpenseAccountScope
) { ) {
pushMany(domainScope, ["deferred_expense", "period_close"]); pushMany(domainScope, ["deferred_expense", "period_close"]);
@ -1868,17 +1868,17 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(entityTypes, ["document", "posting"]); pushMany(entityTypes, ["document", "posting"]);
pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]); pushMany(relationPatterns, ["deferred_expense_to_writeoff", "document_to_posting"]);
} }
if (/цепоч|разрыв|СЃРІСЏР·|документ.*РїСЂРѕРІРѕРґ|РіРґРµ рвет|Р¶РёРІСѓС‚ отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) { if (/цепоч|разрыв|связ|документ.*провод|где рвет|живут отдельно|цепоч|разрыв|связ|документ.*провод|chain|break/i.test(lower)) {
pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]); pushMany(relationPatterns, ["document_to_posting", "contract_to_documents"]);
pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]); pushMany(explanationFocus, ["what_conflicts_with_what", "why_not_closed"]);
} }
if (/аномал|СЂРёСЃРє|С…РІРѕСЃС‚|РїРѕРґРѕР·СЂ|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) { if (/аномал|риск|хвост|подозр|искаж|аномал|риск|хвост|подозр|искаж|suspic|risk/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]); pushMany(anomalyPatterns, ["missing_link", "broken_lifecycle", "amount_independent_risk"]);
} }
if (WRONG_DOCUMENT_MARKERS.test(lower)) { if (WRONG_DOCUMENT_MARKERS.test(lower)) {
pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]); pushMany(anomalyPatterns, ["wrong_document_type", "posting_mismatch", "broken_lifecycle"]);
} }
if (/Р¶РёРІСѓС‚ отдельно|РЅРµ СЃРІСЏР·|без СЃРІСЏР·Рё|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) { if (/живут отдельно|не связ|без связи|живут\s+отдельно|не\s+связ|без\s+связи|missing link/i.test(lower)) {
pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]); pushMany(anomalyPatterns, ["missing_link", "cross_domain_inconsistency"]);
} }
if (REPEATED_ANOMALY_MARKERS.test(lower)) { if (REPEATED_ANOMALY_MARKERS.test(lower)) {
@ -1890,10 +1890,10 @@ function buildSemanticRetrievalProfile(fragmentText: string): SemanticRetrievalP
pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]); pushMany(anomalyPatterns, ["closure_risk", "broken_lifecycle"]);
pushMany(documentTypes, ["period_close_document"]); pushMany(documentTypes, ["period_close_document"]);
} }
if (/РЅРµ РІ платеже|не\s+в\s+платеже|not payment/i.test(lower)) { if (/не\s+в\s+платеже|not payment/i.test(lower)) {
pushMany(excludedInterpretations, ["simple_payment_delay"]); pushMany(excludedInterpretations, ["simple_payment_delay"]);
} }
if (/РЅРµ РїРѕ СЃСѓРјРј|РЅРµ СЃСѓРјРјР°|не\s+по\s+сумм|не\s+сумм|not by amount/i.test(lower)) { if (/не\s+по\s+сумм|не\s+сумм|не\s+сумма|not by amount/i.test(lower)) {
pushMany(excludedInterpretations, ["amount_only_anomaly"]); pushMany(excludedInterpretations, ["amount_only_anomaly"]);
pushMany(rankingBasis, ["amount_independent_risk"]); pushMany(rankingBasis, ["amount_independent_risk"]);
} }
@ -2288,16 +2288,16 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
accounts.push(token.split(".")[0]); accounts.push(token.split(".")[0]);
} }
for (const key of Object.keys(record.attributes ?? {})) { for (const key of Object.keys(record.attributes ?? {})) {
if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) { if (/счетбанк|расчетн.*счет|bank account|банковскийсчет/i.test(key)) {
accounts.push("51"); accounts.push("51");
} }
if (/счетучетарасчетовсконтрагентом/i.test(key)) { if (/счетучетарасчетовсконтрагентом/i.test(key)) {
accounts.push("60"); accounts.push("60");
} }
if (/счетучетандс/i.test(key)) { if (/счетучетандс/i.test(key)) {
accounts.push("19"); accounts.push("19");
} }
if (/субконтодт/i.test(key)) { if (/субконтодт/i.test(key)) {
accounts.push("60"); accounts.push("60");
} }
} }
@ -2306,28 +2306,28 @@ function inferAccountsFromRecord(record: SnapshotRecord, corpus: string): string
function inferDocumentTypesFromRecord(record: SnapshotRecord, corpus: string): string[] { function inferDocumentTypesFromRecord(record: SnapshotRecord, corpus: string): string[] {
const items: string[] = []; const items: string[] = [];
if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) { if (/банковскиевыписки|выписк|расчетногосчета|spisaniesraschetnogoscheta|bank/i.test(corpus)) {
pushMany(items, ["bank_statement", "payment_order"]); pushMany(items, ["bank_statement", "payment_order"]);
} }
if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) { if (/поступлениетоваровуслуг|поступлен/i.test(corpus)) {
items.push("supplier_receipt"); items.push("supplier_receipt");
} }
if (/реализациятоваровуслуг|реализац/i.test(corpus)) { if (/реализациятоваровуслуг|реализац/i.test(corpus)) {
items.push("sales_document"); items.push("sales_document");
} }
if (/РЅРґСЃ|счетфактур|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
pushMany(items, ["invoice", "vat_document"]); pushMany(items, ["invoice", "vat_document"]);
} }
if (/корректировк|ручн|manual/i.test(corpus)) { if (/корректировк|ручн|manual/i.test(corpus)) {
items.push("manual_operation"); items.push("manual_operation");
} }
if (/закрытие|регламент/i.test(corpus)) { if (/закрытие|регламент/i.test(corpus)) {
items.push("period_close_document"); items.push("period_close_document");
} }
if (/РѕСЃРЅРѕРІРЅ|амортиз|fixed_asset/i.test(corpus)) { if (/основн|амортиз|fixed_asset/i.test(corpus)) {
pushMany(items, ["fixed_asset_card", "depreciation_document"]); pushMany(items, ["fixed_asset_card", "depreciation_document"]);
} }
if (/расходыбудущихпериодов|deferred|97/.test(corpus)) { if (/расходыбудущихпериодов|deferred|97/.test(corpus)) {
items.push("deferred_expense_document"); items.push("deferred_expense_document");
} }
if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) { if (record.source_entity.startsWith("Document") || record.source_entity.startsWith("DocumentJournal")) {
@ -2356,11 +2356,11 @@ function inferDomainsFromRecord(corpus: string, documentTypes: string[], record:
if (documentTypes.some((item) => item === "deferred_expense_document")) { if (documentTypes.some((item) => item === "deferred_expense_document")) {
pushMany(domains, ["deferred_expense", "period_close"]); pushMany(domains, ["deferred_expense", "period_close"]);
} }
if (/закрытие|регламент|period close/i.test(corpus)) { if (/закрытие|регламент|period close/i.test(corpus)) {
domains.push("period_close"); domains.push("period_close");
} }
const hasSettlementLexicalAnchor = const hasSettlementLexicalAnchor =
/(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|РїРѕРєСѓРї)/i.test( /(?:settlement|payment|bank|statement|supplier|customer|buyer|vendor|60\b|62\b|51\b|оплат|банк|выписк|расчет|постав|покуп)/i.test(
corpus corpus
); );
const hasSettlementDocAnchor = documentTypes.some( const hasSettlementDocAnchor = documentTypes.some(
@ -2386,13 +2386,13 @@ function inferEntityTypes(record: SnapshotRecord): string[] {
entities.push("counterparty"); entities.push("counterparty");
} }
const corpus = collectTextFromRecord(record); const corpus = collectTextFromRecord(record);
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus)) { if (/договор|contract/i.test(corpus)) {
entities.push("contract"); entities.push("contract");
} }
if (/РѕСЃРЅРѕРІРЅ|fixed_asset|инвентар/i.test(corpus)) { if (/основн|fixed_asset|инвентар/i.test(corpus)) {
entities.push("fixed_asset"); entities.push("fixed_asset");
} }
if (/РЅРґСЃ|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
entities.push("tax_entry"); entities.push("tax_entry");
} }
return uniqueStrings(entities); return uniqueStrings(entities);
@ -2406,25 +2406,25 @@ function inferRelationPatterns(record: SnapshotRecord, corpus: string): string[]
if (hasDocLinks) { if (hasDocLinks) {
patterns.push("document_to_posting"); patterns.push("document_to_posting");
} }
if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) { if (hasCounterparty && hasDocLinks && /платеж|bank|settlement|расчет/i.test(corpus)) {
patterns.push("payment_to_settlement"); patterns.push("payment_to_settlement");
} }
if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) { if (/банковскиевыписки|выписк|statement/i.test(corpus) && hasDocLinks) {
patterns.push("statement_to_document"); patterns.push("statement_to_document");
} }
if (/РѕСЃРЅРѕРІРЅ|fixed_asset|амортиз/i.test(corpus)) { if (/основн|fixed_asset|амортиз/i.test(corpus)) {
patterns.push("asset_card_to_depreciation"); patterns.push("asset_card_to_depreciation");
} }
if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) { if (/расходыбудущихпериодов|97|deferred/i.test(corpus)) {
patterns.push("deferred_expense_to_writeoff"); patterns.push("deferred_expense_to_writeoff");
} }
if (/РЅРґСЃ|счетфактур|РєРЅРёРіРёРїРѕРєСѓРїРѕРє|книгипродаж|vat|invoice/i.test(corpus)) { if (/ндс|счетфактур|книгипокупок|книгипродаж|vat|invoice/i.test(corpus)) {
patterns.push("invoice_to_vat"); patterns.push("invoice_to_vat");
} }
if (/РґРѕРіРѕРІРѕСЂ|contract/i.test(corpus) && hasDocLinks) { if (/договор|contract/i.test(corpus) && hasDocLinks) {
patterns.push("contract_to_documents"); patterns.push("contract_to_documents");
} }
if (/склад|товар|материал|receipt/i.test(corpus)) { if (/склад|товар|материал|receipt/i.test(corpus)) {
patterns.push("receipt_to_stock_movement"); patterns.push("receipt_to_stock_movement");
} }
return uniqueStrings(patterns); return uniqueStrings(patterns);
@ -2466,7 +2466,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) { if (relationPatterns.includes("document_to_posting") && !record.attributes.Recorder) {
anomalies.push("posting_mismatch"); anomalies.push("posting_mismatch");
} }
if (/ручн|manual|корректировк/.test(corpus)) { if (/ручн|manual|корректировк/.test(corpus)) {
anomalies.push("manual_intervention_suspicion"); anomalies.push("manual_intervention_suspicion");
} }
if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) { if (lifecycleMarkers.includes("period_boundary") && (unknownLinks > 0 || zeroGuidValues > 0)) {
@ -2478,7 +2478,7 @@ function inferAnomalyPatterns(record: SnapshotRecord, corpus: string, relationPa
if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) { if (!hasCounterparty && !hasDocLinks && zeroGuidValues > 0) {
anomalies.push("silent_orphan"); anomalies.push("silent_orphan");
} }
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|СЃСѓРјРј|итого|amount/i.test(key)); const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
if (!hasAmountSignal && anomalies.length > 0) { if (!hasAmountSignal && anomalies.length > 0) {
anomalies.push("amount_independent_risk"); anomalies.push("amount_independent_risk");
} }
@ -2530,15 +2530,15 @@ function evaluateExcludedInterpretations(
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"]; const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "wrong_document_type", "closure_risk"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item)); const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (!hasStructural) { if (!hasStructural) {
reasons.push("Исключено как simple_payment_delay без структурного дефекта."); reasons.push("Исключено как simple_payment_delay без структурного дефекта.");
} }
} }
if (interpretationSet.has("amount_only_anomaly")) { if (interpretationSet.has("amount_only_anomaly")) {
const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|СЃСѓРјРј|итого|amount/i.test(key)); const hasAmountSignal = Object.keys(record.attributes ?? {}).some((key) => /sum|сумм|итого|amount/i.test(key));
const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"]; const structural = ["missing_link", "broken_lifecycle", "posting_mismatch", "cross_domain_inconsistency", "silent_orphan"];
const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item)); const hasStructural = signals.anomaly_patterns.some((item) => structural.includes(item));
if (hasAmountSignal && !hasStructural) { if (hasAmountSignal && !hasStructural) {
reasons.push("Исключено как amount-only аномалия без структурных признаков."); reasons.push("Исключено как amount-only аномалия без структурных признаков.");
} }
} }
@ -2611,22 +2611,22 @@ function evaluateRecordAgainstProfile(record: SnapshotRecord, profile: SemanticR
const matchReasons: string[] = []; const matchReasons: string[] = [];
if (accountMatch && profile.account_scope.length > 0) { if (accountMatch && profile.account_scope.length > 0) {
matchReasons.push("Совпал account_scope."); matchReasons.push("Совпал account_scope.");
} }
if (domainMatch && profile.domain_scope.length > 0) { if (domainMatch && profile.domain_scope.length > 0) {
matchReasons.push("Совпал domain_scope."); matchReasons.push("Совпал domain_scope.");
} }
if (documentMatch && profile.document_types.length > 0) { if (documentMatch && profile.document_types.length > 0) {
matchReasons.push("Совпал document_types."); matchReasons.push("Совпал document_types.");
} }
if (relationMatch && profile.relation_patterns.length > 0) { if (relationMatch && profile.relation_patterns.length > 0) {
matchReasons.push("Совпали relation_patterns."); matchReasons.push("Совпали relation_patterns.");
} }
if (anomalyMatch && profile.anomaly_patterns.length > 0) { if (anomalyMatch && profile.anomaly_patterns.length > 0) {
matchReasons.push("Совпали anomaly_patterns."); matchReasons.push("Совпали anomaly_patterns.");
} }
if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) { if (lifecycleMatch && profile.lifecycle_stage_filters.length > 0) {
matchReasons.push("Совпал lifecycle_stage_filters."); matchReasons.push("Совпал lifecycle_stage_filters.");
} }
if (graphTraversal.domain_match) { if (graphTraversal.domain_match) {
matchReasons.push("Graph traversal domain matched."); matchReasons.push("Graph traversal domain matched.");
@ -2818,7 +2818,7 @@ export class AssistantDataLayer {
business_interpretation: [], business_interpretation: [],
confidence: "low", confidence: "low",
limitations: ["Snapshot data files could not be loaded."], limitations: ["Snapshot data files could not be loaded."],
errors: ["Слой данных недоступен: РЅРµ удалось загрузить snapshot-файлы."] errors: ["Слой данных недоступен: не удалось загрузить snapshot-файлы."]
}; };
} }
let result: RawRetrievalResult | null = null; let result: RawRetrievalResult | null = null;
@ -3718,8 +3718,8 @@ export class AssistantDataLayer {
: "low", : "low",
business_interpretation: business_interpretation:
group.risk_factors.size > 0 group.risk_factors.size > 0
? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно." ? "Есть признаки разрыва расчетной цепочки: часть связей/этапов lifecycle подтверждена неполно."
: "Есть связанная операционная цепочка, РЅРѕ явные СЂРёСЃРє-паттерны выражены слабо.", : "Есть связанная операционная цепочка, но явные риск-паттерны выражены слабо.",
relation_types: Array.from(group.relations.entries()) relation_types: Array.from(group.relations.entries())
.sort((left, right) => right[1] - left[1]) .sort((left, right) => right[1] - left[1])
.map((item) => item[0]), .map((item) => item[0]),
@ -3768,24 +3768,24 @@ export class AssistantDataLayer {
evidence: [], evidence: [],
why_included: [], why_included: [],
selection_reason: [ selection_reason: [
"РџРѕРёСЃРє строился РїРѕ semantic retrieval profile, РЅРѕ подходящие контрагенты РЅРµ найдены.", "Поиск строился по semantic retrieval profile, но подходящие контрагенты не найдены.",
"Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.", "Фильтрация использовала пересечение account/domain/document/relation/anomaly ограничений.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.", domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
guidFilter.length > 0 guidFilter.length > 0
? "GUID-фильтрация включена." ? "GUID-фильтрация включена."
: `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`, : `GUID отсутствовал, выполнено semantic narrowing (${filtered.length}/${sourceRecords.length}).`,
`Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`, `Graph planner mode=${graphTraversalRuntime.planner_mode}, eligible=${graphTraversalRuntime.graph_eligible}, applied=${graphTraversalRuntime.traversal_applied}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.` `Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
], ],
risk_factors: semanticProfile.anomaly_patterns, risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: [ business_interpretation: [
"РџРѕ текущему профилю запроса устойчивых разрывов цепочки РЅРµ обнаружено.", "По текущему профилю запроса устойчивых разрывов цепочки не обнаружено.",
"Для точечного drilldown добавьте GUID или уточните период/контрагента." "Для точечного drilldown добавьте GUID или уточните период/контрагента."
], ],
confidence: "medium", confidence: "medium",
limitations: [ limitations: [
guidFilter.length > 0 ? "РџРѕРёСЃРє ограничен переданными GUID." : "РџРѕРёСЃРє выполнен РїРѕ semantic narrowing без GUID.", guidFilter.length > 0 ? "Поиск ограничен переданными GUID." : "Поиск выполнен по semantic narrowing без GUID.",
"Источник данных — snapshot 2020 (read-only), Р° РЅРµ live состояние базы 1РЎ.", "Источник данных — snapshot 2020 (read-only), а не live состояние базы 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain записи на этапе source selection." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -3833,18 +3833,18 @@ export class AssistantDataLayer {
}, },
evidence: evidence.slice(0, 12), evidence: evidence.slice(0, 12),
why_included: [ why_included: [
`Семантическое сужение выполнено РїРѕ профилю ${semanticProfile.query_subject}.`, `Семантическое сужение выполнено по профилю ${semanticProfile.query_subject}.`,
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.", domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced.",
semanticProfile.account_scope.length > 0 semanticProfile.account_scope.length > 0
? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.` ? `Учитывались счета: ${semanticProfile.account_scope.join(", ")}.`
: "Счета РЅРµ были заданы СЏРІРЅРѕ, использованы domain/document/relation ограничения.", : "Счета не были заданы явно, использованы domain/document/relation ограничения.",
`После narrowing осталось ${filtered.length} РёР· ${sourceRecords.length} записей.`, `После narrowing осталось ${filtered.length} из ${sourceRecords.length} записей.`,
`Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.` `Graph traversal mode=${graphTraversalRuntime.planner_mode}, matched=${graphTraversalRuntime.matched_candidates}/${graphTraversalRuntime.evaluated_candidates}.`
], ],
selection_reason: [ selection_reason: [
"Отбор основан РЅР° пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.", "Отбор основан на пересечении account_scope + domain_scope + document_types + relation_patterns + anomaly_patterns.",
"GUID-mode отключен: full scan без ограничителей РЅРµ использовался.", "GUID-mode отключен: full scan без ограничителей не использовался.",
`Ранжирование выполнено РїРѕ basis: ${semanticProfile.ranking_basis.join(", ")}.`, `Ранжирование выполнено по basis: ${semanticProfile.ranking_basis.join(", ")}.`,
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.", domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied.",
`Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`, `Graph signal counts: ${JSON.stringify(graphTraversalRuntime.signal_counts)}.`,
`Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.` `Graph ranking signals: ${graphTraversalRuntime.ranking_shift_signals.join(",") || "none"}.`
@ -3852,15 +3852,15 @@ export class AssistantDataLayer {
risk_factors: risk_factors:
aggregatedRiskFactors.length > 0 aggregatedRiskFactors.length > 0
? aggregatedRiskFactors ? aggregatedRiskFactors
: ["Высокая плотность операций РїРѕ контрагенту может указывать РЅР° незакрытые цепочки."], : ["Высокая плотность операций по контрагенту может указывать на незакрытые цепочки."],
business_interpretation: [ business_interpretation: [
"Результат отражает РЅРµ просто объем операций, Р° структурные признаки разрыва цепочки Рё lifecycle-конфликта.", "Результат отражает не просто объем операций, а структурные признаки разрыва цепочки и lifecycle-конфликта.",
"Контрагенты РІ топе приоритетны для проверки РЅР° неверный тип закрывающего документа Рё незавершенные СЃРІСЏР·Рё." "Контрагенты в топе приоритетны для проверки на неверный тип закрывающего документа и незавершенные связи."
], ],
confidence: "high", confidence: "high",
limitations: [ limitations: [
guidFilter.length > 0 ? "Выборка ограничена GUID РёР· запроса." : "Выборка ограничена semantic retrieval profile.", guidFilter.length > 0 ? "Выборка ограничена GUID из запроса." : "Выборка ограничена semantic retrieval profile.",
"Источник данных — snapshot 2020 (read-only), РЅРµ live контур 1РЎ.", "Источник данных — snapshot 2020 (read-only), не live контур 1С.",
domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain элементы на этапе source selection." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -4177,12 +4177,12 @@ export class AssistantDataLayer {
})), })),
why_included: items.length > 0 why_included: items.length > 0
? [ ? [
"Показаны сущности СЃ максимальным количеством записей.", "Показаны сущности с максимальным количеством записей.",
domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced." domainCard ? `P0 domain purity enforced for ${domainCard.id}.` : "P0 domain purity was not enforced."
] ]
: [], : [],
selection_reason: [ selection_reason: [
"Ранжирование выполнено РїРѕ records_count РїРѕ убыванию.", "Ранжирование выполнено по records_count по убыванию.",
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied." domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
], ],
risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]), risk_factors: uniqueStrings(["entity_volume_spike", ...semanticProfile.anomaly_patterns]),
@ -4191,7 +4191,7 @@ export class AssistantDataLayer {
], ],
confidence: "medium", confidence: "medium",
limitations: [ limitations: [
"Ранжирование РїРѕ объему РЅРµ всегда эквивалентно бизнес-СЂРёСЃРєСѓ.", "Ранжирование по объему не всегда эквивалентно бизнес-риску.",
domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся." domainCard ? "Domain purity guardrail может исключить cross-domain записи на batch-слое." : "Domain purity guardrail не применялся."
], ],
errors: [] errors: []
@ -4354,7 +4354,7 @@ export class AssistantDataLayer {
domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied." domainCard ? `Domain gate source scope: ${sourceScope.join(", ")}.` : "Domain gate source scope not applied."
], ],
risk_factors: semanticProfile.anomaly_patterns, risk_factors: semanticProfile.anomaly_patterns,
business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."], business_interpretation: ["Слой отражает базовый factual-срез документов для оперативной сверки."],
confidence: "high", confidence: "high",
limitations: [ limitations: [
"Это read-only snapshot, а не онлайн-состояние 1С.", "Это read-only snapshot, а не онлайн-состояние 1С.",
@ -4385,11 +4385,11 @@ export class AssistantDataLayer {
}, },
evidence: [], evidence: [],
why_included: [], why_included: [],
selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/СЃСѓРјРјР°/счет)."], selection_reason: ["Для drilldown требуется GUID или достаточные business anchors (номер/дата/сумма/счет)."],
risk_factors: [], risk_factors: [],
business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."], business_interpretation: ["Без GUID или business anchors точечный drilldown невозможен."],
confidence: "low", confidence: "low",
limitations: ["Добавьте GUID или СЏРєРѕСЂСЏ: номер документа, дату, СЃСѓРјРјСѓ, счет."], limitations: ["Добавьте GUID или якоря: номер документа, дату, сумму, счет."],
errors: [] errors: []
}; };
} }
@ -4440,14 +4440,14 @@ export class AssistantDataLayer {
evidence: matches.slice(0, 10), evidence: matches.slice(0, 10),
why_included: why_included:
matches.length > 0 matches.length > 0
? ["Включены source-of-record записи, совпавшие РїРѕ business anchors (номер/дата/СЃСѓРјРјР°/счет)."] ? ["Включены source-of-record записи, совпавшие по business anchors (номер/дата/сумма/счет)."]
: [], : [],
selection_reason: [ selection_reason: [
"GUID отсутствует, использован business-anchor trace РїРѕ атрибутам документа Рё расчетов." "GUID отсутствует, использован business-anchor trace по атрибутам документа и расчетов."
], ],
risk_factors: [], risk_factors: [],
business_interpretation: [ business_interpretation: [
"Drilldown опирается РЅР° business anchors, поэтому вывод требует первичной проверки РІ source-of-record." "Drilldown опирается на business anchors, поэтому вывод требует первичной проверки в source-of-record."
], ],
confidence: matches.length > 0 ? "medium" : "low", confidence: matches.length > 0 ? "medium" : "low",
limitations: [ limitations: [
@ -4478,12 +4478,12 @@ export class AssistantDataLayer {
matched_records: matches.length matched_records: matches.length
}, },
evidence: matches.slice(0, 10), evidence: matches.slice(0, 10),
why_included: matches.length > 0 ? ["Включены записи, содержащие GUID РёР· запроса."] : [], why_included: matches.length > 0 ? ["Включены записи, содержащие GUID из запроса."] : [],
selection_reason: ["РџРѕРёСЃРє РїРѕ source_id, linked target_id Рё строковым атрибутам."], selection_reason: ["Поиск по source_id, linked target_id и строковым атрибутам."],
risk_factors: [], risk_factors: [],
business_interpretation: ["Результат показывает source-of-record объекты РїРѕ переданным идентификаторам."], business_interpretation: ["Результат показывает source-of-record объекты по переданным идентификаторам."],
confidence: matches.length > 0 ? "high" : "medium", confidence: matches.length > 0 ? "high" : "medium",
limitations: ["РџРѕРёСЃРє ограничен локальным snapshot-пакетом."], limitations: ["Поиск ограничен локальным snapshot-пакетом."],
errors: [] errors: []
}; };
} }

View File

@ -431,7 +431,7 @@ function parseDateLike(raw: string): string | null {
return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] }); return normalizeDateIso({ year: parseYear(dayMonthYear[3]), month: dayMonthYear[2], day: dayMonthYear[1] });
} }
const rusMonthYear = value.match( const rusMonthYear = value.match(
/\b(январь|февраль|март|апрель|май|РёСЋРЅСЊ|июль|август|сентябрь|октябрь|РЅРѕСЏР±СЂСЊ|декабрь)\s+(20\d{2})\b/i /\b(январь|февраль|март|апрель|май|июнь|июль|август|сентябрь|октябрь|ноябрь|декабрь)\s+(20\d{2})\b/i
); );
if (rusMonthYear) { if (rusMonthYear) {
const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()]; const month = RUS_MONTH_TO_NUMBER[String(rusMonthYear[1] ?? "").toLowerCase()];
@ -530,7 +530,7 @@ function normalizedAnchorFromFragments(normalized: NormalizedPayload | null | un
source: `normalized_time_scope:${type || "unknown"}` source: `normalized_time_scope:${type || "unknown"}`
}; };
} }
if (/(?:июл|july|РёСЋР»)/i.test(value)) { if (/(?:июл|july|июл)/i.test(value)) {
return { return {
value: `${JULY_YEAR}-${JULY_MONTH}`, value: `${JULY_YEAR}-${JULY_MONTH}`,
source: `normalized_time_scope:${type || "unknown"}` source: `normalized_time_scope:${type || "unknown"}`
@ -564,7 +564,7 @@ function resolveJulyAnchor(rawText: string): TemporalAnchorResolution {
const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null; const explicitYear = lower.match(/\b(20\d{2})\b/)?.[1] ?? null;
const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i); const dayByNamedJuly = lower.match(/(?:^|\D)(0?[1-9]|[12]\d|3[01])\s+(?:июл(?:я|ь)?|july)(?:\D|$)/i);
const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/); const dayByNumeric = lower.match(/\b(0?[1-9]|[12]\d|3[01])[./-](0?7)(?:[./-](\d{2}|\d{4}))?\b/);
const monthByNamed = /(?:июл|july|РёСЋР»)/i.test(lower); const monthByNamed = /(?:июл|july|июл)/i.test(lower);
const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower); const monthByNumeric = /\b20\d{2}[-/.]0?7\b/.test(lower);
if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) { if (!dayByNamedJuly && !dayByNumeric && !monthByNamed && !monthByNumeric) {
return { return {
@ -771,7 +771,7 @@ export function applyTemporalHintToExecutionPlan<
return item; return item;
} }
const text = String(item.fragment_text ?? "").trim(); const text = String(item.fragment_text ?? "").trim();
if (/2020-07|июл|РёСЋР»|july/i.test(text)) { if (/2020-07|июл|июл|july/i.test(text)) {
return item; return item;
} }
return { return {
@ -819,7 +819,7 @@ export function resolveDomainPolarityGuard(input: {
prefixes.has("62") || prefixes.has("62") ||
prefixes.has("51") || prefixes.has("51") ||
prefixes.has("76") || prefixes.has("76") ||
/(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|С…РІРѕСЃС‚)/i.test(lower); /(?:расч[её]т|оплат|аванс|долг|settlement|payment|tail|хвост|незакры|зач[её]т|расч|оплат|аванс|долг|хвост)/i.test(lower);
if (!settlementSignal) { if (!settlementSignal) {
return { return {
applied: false, applied: false,
@ -839,13 +839,13 @@ export function resolveDomainPolarityGuard(input: {
}; };
} }
const supplierScore = const supplierScore =
(/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) + (/(?:поставщ|supplier|vendor|кредитор|обязательств|payable|поставщ|кредитор|обязательств)/i.test(lower) ? 2 : 0) +
(prefixes.has("60") ? 2 : 0) + (prefixes.has("60") ? 2 : 0) +
(/(?:сч[её]т\s*60|по\s*60|счет\s*60|РїРѕ\s*60)/i.test(lower) ? 1 : 0); (/(?:сч[её]т\s*60|по\s*60|счет\s*60)/i.test(lower) ? 1 : 0);
const customerScore = const customerScore =
(/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) + (/(?:покупат|customer|buyer|дебитор|receivable|покупат|дебитор)/i.test(lower) ? 2 : 0) +
(prefixes.has("62") ? 2 : 0) + (prefixes.has("62") ? 2 : 0) +
(/(?:сч[её]т\s*62|по\s*62|счет\s*62|РїРѕ\s*62)/i.test(lower) ? 1 : 0); (/(?:сч[её]т\s*62|по\s*62|счет\s*62)/i.test(lower) ? 1 : 0);
let polarity: DomainPolarity = "mixed_or_unresolved"; let polarity: DomainPolarity = "mixed_or_unresolved";
if (supplierScore > 0 || customerScore > 0) { if (supplierScore > 0 || customerScore > 0) {
@ -903,10 +903,10 @@ export function applyPolarityHintToExecutionPlan<
return item; return item;
} }
const text = String(item.fragment_text ?? "").trim(); const text = String(item.fragment_text ?? "").trim();
if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60|РїРѕ\s*60)/i.test(text)) { if (polarity.polarity === "supplier_payable" && /(поставщ|supplier|сч[её]т\s*60|по\s*60|поставщ|счет\s*60)/i.test(text)) {
return item; return item;
} }
if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62|РїРѕ\s*62)/i.test(text)) { if (polarity.polarity === "customer_receivable" && /(покупат|customer|сч[её]т\s*62|по\s*62|покупат|счет\s*62)/i.test(text)) {
return item; return item;
} }
return { return {
@ -917,11 +917,11 @@ export function applyPolarityHintToExecutionPlan<
} }
function containsReceivableSignal(value: string): boolean { function containsReceivableSignal(value: string): boolean {
return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value); return /(?:customer_settlement|stale_receivable|receivable_closed|receivable|дебитор)/i.test(value);
} }
function containsPayableSignal(value: string): boolean { function containsPayableSignal(value: string): boolean {
return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value); return /(?:bank_settlement|payable|обязательств|supplier|поставщ|счет\s*60|\b60(?:\.\d{2})?\b)/i.test(value);
} }
function problemUnitCorpus(unit: ProblemUnit): string { function problemUnitCorpus(unit: ProblemUnit): string {
@ -1590,15 +1590,15 @@ export function applyEligibilityToGroundingCheck<T extends { status: string; rea
? "no_grounded_answer" ? "no_grounded_answer"
: "partial"; : "partial";
const reasonMap: Record<string, string> = { const reasonMap: Record<string, string> = {
admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.", admissible_evidence_count_zero: "Недостаточно допустимого evidence для обоснованного ответа.",
critical_domain_or_account_contradiction: "Есть критическое противоречие РїРѕ domain/account scope.", critical_domain_or_account_contradiction: "Есть критическое противоречие по domain/account scope.",
temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел Р·Р° РѕРєРЅРѕ company snapshot (июль 2020).", temporal_guard_failed_out_of_snapshot_window: "Temporal anchor вышел за окно company snapshot (июль 2020).",
temporal_guard_ambiguous_limited: "Temporal anchor РЅРµ разрешен надежно РІ пределах company snapshot.", temporal_guard_ambiguous_limited: "Temporal anchor не разрешен надежно в пределах company snapshot.",
business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.", business_scope_generic_unresolved: "Business scope остался generic и не подтвержден как company-specific для доказательного ответа.",
polarity_guard_limited_unresolved_polarity: "РќРµ удалось надежно определить supplier/customer polarity.", polarity_guard_limited_unresolved_polarity: "Не удалось надежно определить supplier/customer polarity.",
polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity РІ retrieval-контуре.", polarity_guard_blocked_conflict: "Обнаружен конфликт supplier/customer polarity в retrieval-контуре.",
claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.", claim_anchor_coverage_insufficient: "Недостаточно покрытия required anchors для claim-bound grounding.",
targeted_evidence_hit_rate_zero: "Targeted evidence acquisition РЅРµ дал допустимых попаданий РїРѕ claim target path." targeted_evidence_hit_rate_zero: "Targeted evidence acquisition не дал допустимых попаданий по claim target path."
}; };
const reasons = [ const reasons = [
...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []), ...(Array.isArray(groundingCheck.reasons) ? groundingCheck.reasons : []),

View File

@ -19,6 +19,8 @@ import * as addressFilterExtractor_1 from "./addressFilterExtractor";
import * as predecomposeContract_1 from "./address_runtime/predecomposeContract"; import * as predecomposeContract_1 from "./address_runtime/predecomposeContract";
import * as openaiResponsesClient_1 from "./openaiResponsesClient"; import * as openaiResponsesClient_1 from "./openaiResponsesClient";
import * as addressMcpClient_1 from "./addressMcpClient"; import * as addressMcpClient_1 from "./addressMcpClient";
import * as capabilitiesRegistry_1 from "./capabilitiesRegistry";
import * as assistantCanon_1 from "./assistantCanon";
import iconv from "iconv-lite"; import iconv from "iconv-lite";
const DATA_SCOPE_CACHE_TTL_MS = 60_000; const DATA_SCOPE_CACHE_TTL_MS = 60_000;
const dataScopeProbeCache = new Map(); const dataScopeProbeCache = new Map();
@ -3859,17 +3861,7 @@ function buildLivingChatPrompt(userMessage, conversationWindow) {
return `${contextBlock}Сообщение пользователя:\n${userMessage}`; return `${contextBlock}Сообщение пользователя:\n${userMessage}`;
} }
function buildAssistantCapabilityContractReply() { function buildAssistantCapabilityContractReply() {
return [ return (0, capabilitiesRegistry_1.buildCapabilityContractReplyFromRegistry)();
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею сейчас:",
"1. Находить документы, операции, договоры и остатки по контрагенту/договору/периоду.",
"2. Делать агрегаты по базе: активность, роли контрагентов, top-срезы по суммам и операциям.",
"3. Кратко объяснять результат и подсказывать следующий точный запрос.",
"Что не умею:",
"1. Не настраиваю 1С и не меняю конфигурацию.",
"2. Не создаю и не провожу документы в базе.",
"3. Не выполняю админские действия на сервере."
].join("\n");
} }
function normalizeScopeLabel(value) { function normalizeScopeLabel(value) {
const repaired = repairAddressMojibake(String(value ?? "")); const repaired = repairAddressMojibake(String(value ?? ""));
@ -4971,6 +4963,7 @@ export class AssistantService {
else { else {
const conversationWindow = buildLivingChatContextWindow(session.items); const conversationWindow = buildLivingChatContextWindow(session.items);
const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow); const userPrompt = buildLivingChatPrompt(userMessage, conversationWindow);
const canonExcerpt = (0, assistantCanon_1.loadAssistantCanonExcerpt)(520);
const chatResponse = await this.chatClient.chat({ const chatResponse = await this.chatClient.chat({
llmProvider: payload.llmProvider, llmProvider: payload.llmProvider,
apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""), apiKey: String(payload.apiKey ?? process.env.OPENAI_API_KEY ?? ""),
@ -4984,7 +4977,8 @@ export class AssistantService {
"Работай честно: не заявляй действия, которые недоступны в этом рантайме.", "Работай честно: не заявляй действия, которые недоступны в этом рантайме.",
"Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.", "Разрешено: анализ и объяснение данных, формулировка запросов, подсказки по следующему шагу.",
"Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.", "Запрещено: обещать настройку 1С, админ-действия, создание/проведение документов или любые изменения в базе.",
"Если пользователь спрашивает про возможности, отвечай только по этому контракту." "Если пользователь спрашивает про возможности, отвечай только по этому контракту.",
`Канон поведения: ${canonExcerpt}`
].join(" "), ].join(" "),
developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.", developerPrompt: "Формат: коротко и по сути, без JSON и без служебных блоков. Пиши человеко-понятно.",
userMessage: userPrompt, userMessage: userPrompt,

View File

@ -0,0 +1,205 @@
import fs from "fs";
import { ASSISTANT_CAPABILITIES_REGISTRY_FILE } from "../config";
export type CapabilityMaturity = "production_ready" | "partial" | "planned" | "deprecated";
export interface CapabilityGroup {
group_code: string;
group_title: string;
description: string;
risk_level: "low" | "medium" | "high";
maturity_status: CapabilityMaturity;
supported_operations: string[];
unsupported_operations: string[];
required_entities: string[];
optional_entities: string[];
typical_queries: string[];
related_routes: string[];
safe_alternatives: string[];
one_c_hints: string[];
}
export interface CapabilityRegistry {
schema_version: string;
updated_at: string;
assistant_mode: "read_only" | "mixed";
groups: CapabilityGroup[];
}
const FALLBACK_REGISTRY: CapabilityRegistry = {
schema_version: "capabilities_registry_fallback_v1",
updated_at: "2026-04-09T00:00:00.000Z",
assistant_mode: "read_only",
groups: [
{
group_code: "vat",
group_title: "НДС",
description: "Срезы и расчеты НДС на базе данных 1С.",
risk_level: "high",
maturity_status: "partial",
supported_operations: ["vat_period_snapshot", "vat_payable_forecast"],
unsupported_operations: ["submit_tax_declaration"],
required_entities: ["period", "organization"],
optional_entities: ["counterparty"],
typical_queries: ["Сколько НДС к уплате за период?"],
related_routes: [],
safe_alternatives: ["Показать движения по 68/19 за период"],
one_c_hints: ["РегистрБухгалтерии.Хозрасчетный"]
},
{
group_code: "counterparties",
group_title: "Контрагенты",
description: "Документы, операции, договоры и срезы по контрагентам.",
risk_level: "medium",
maturity_status: "production_ready",
supported_operations: ["list_documents_by_counterparty", "list_contracts_by_counterparty"],
unsupported_operations: ["edit_counterparty_card"],
required_entities: ["counterparty_scope_or_contract"],
optional_entities: ["period", "organization"],
typical_queries: ["Покажи документы по контрагенту"],
related_routes: [],
safe_alternatives: ["Уточнить ИНН/наименование контрагента"],
one_c_hints: ["Справочник.Контрагенты"]
},
{
group_code: "boundaries",
group_title: "Ограничения",
description: "Операции, которые ассистент не выполняет.",
risk_level: "high",
maturity_status: "production_ready",
supported_operations: ["explain_boundary", "suggest_safe_next_step"],
unsupported_operations: ["configure_1c", "admin_server_actions", "create_or_post_documents"],
required_entities: [],
optional_entities: [],
typical_queries: ["Можешь настроить 1С?"],
related_routes: [],
safe_alternatives: ["Сформировать план диагностики для 1С/ИТ-админа"],
one_c_hints: []
}
]
};
let cache: { mtimeMs: number; value: CapabilityRegistry } | null = null;
function toRecord(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function toStringSafe(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function toArray(value: unknown): unknown[] {
return Array.isArray(value) ? value : [];
}
function readRegistryFromFile(): CapabilityRegistry | null {
if (!fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)) return null;
try {
const raw = fs.readFileSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE, "utf-8");
const parsed = JSON.parse(raw) as unknown;
const root = toRecord(parsed);
if (!root) return null;
const groups = toArray(root.groups)
.map((item) => toRecord(item))
.filter((item): item is Record<string, unknown> => item !== null)
.map((item) => ({
group_code: toStringSafe(item.group_code) ?? "unknown_group",
group_title: toStringSafe(item.group_title) ?? "Группа",
description: toStringSafe(item.description) ?? "",
risk_level: (toStringSafe(item.risk_level) as "low" | "medium" | "high" | null) ?? "medium",
maturity_status:
(toStringSafe(item.maturity_status) as CapabilityMaturity | null) ??
("partial" as CapabilityMaturity),
supported_operations: toArray(item.supported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
unsupported_operations: toArray(item.unsupported_operations)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
required_entities: toArray(item.required_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
optional_entities: toArray(item.optional_entities)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
typical_queries: toArray(item.typical_queries)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
related_routes: toArray(item.related_routes)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
safe_alternatives: toArray(item.safe_alternatives)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null),
one_c_hints: toArray(item.one_c_hints)
.map((v) => toStringSafe(v))
.filter((v): v is string => v !== null)
}));
if (groups.length === 0) return null;
return {
schema_version: toStringSafe(root.schema_version) ?? "capabilities_registry_v1",
updated_at: toStringSafe(root.updated_at) ?? new Date().toISOString(),
assistant_mode: (toStringSafe(root.assistant_mode) as "read_only" | "mixed" | null) ?? "read_only",
groups
};
} catch {
return null;
}
}
export function loadCapabilitiesRegistry(): CapabilityRegistry {
try {
const mtimeMs = fs.existsSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE)
? fs.statSync(ASSISTANT_CAPABILITIES_REGISTRY_FILE).mtimeMs
: -1;
if (cache && cache.mtimeMs === mtimeMs) {
return cache.value;
}
const value = readRegistryFromFile() ?? FALLBACK_REGISTRY;
cache = { mtimeMs, value };
return value;
} catch {
return cache?.value ?? FALLBACK_REGISTRY;
}
}
export function buildCapabilityContractReplyFromRegistry(): string {
const registry = loadCapabilitiesRegistry();
const topGroups = registry.groups.slice(0, 6);
const groupLines = topGroups.map((group, index) => {
const ops = group.supported_operations.slice(0, 3).join(", ");
return `${index + 1}. ${group.group_title}: ${group.description}${ops ? ` (например: ${ops})` : ""}.`;
});
return [
"Я ассистент по анализу данных 1С в режиме чтения.",
"Что умею по группам:",
...groupLines,
"Если хотите, раскрою любую группу точечно и дам готовую формулировку запроса.",
"Что не делаю: не настраиваю 1С, не меняю конфигурацию, не создаю и не провожу документы, не выполняю админ-действия на сервере."
].join("\n");
}
export function resolveNearestCapabilityGroup(input: { domain?: string | null; queryClass?: string | null }): CapabilityGroup | null {
const registry = loadCapabilitiesRegistry();
const haystack = `${String(input.domain ?? "")} ${String(input.queryClass ?? "")}`.toLowerCase();
if (!haystack.trim()) return null;
const scoring: Array<{ group: CapabilityGroup; score: number }> = registry.groups.map((group) => {
let score = 0;
const bucket = `${group.group_code} ${group.group_title} ${group.description} ${group.supported_operations.join(" ")}`.toLowerCase();
for (const token of haystack.split(/[\s._/-]+/g).filter(Boolean)) {
if (bucket.includes(token)) score += 1;
}
return { group, score };
});
scoring.sort((a, b) => b.score - a.score);
return scoring[0] && scoring[0].score > 0 ? scoring[0].group : null;
}

View File

@ -150,53 +150,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "initiated_payment", state_code: "initiated_payment",
state_label: "Платеж инициирован", state_label: "Платеж инициирован",
state_class: "initial", state_class: "initial",
entry_conditions: ["payment_order_created"], entry_conditions: ["payment_order_created"],
exit_conditions: ["bank_recorded"], exit_conditions: ["bank_recorded"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Есть инициирование платежа." business_meaning: "Есть инициирование платежа."
}, },
{ {
state_code: "bank_recorded", state_code: "bank_recorded",
state_label: "Платеж отражен банком", state_label: "Платеж отражен банком",
state_class: "active", state_class: "active",
entry_conditions: ["bank_statement_recorded"], entry_conditions: ["bank_statement_recorded"],
exit_conditions: ["settlement_linked"], exit_conditions: ["settlement_linked"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие." business_meaning: "Движение денег зафиксировано, ожидается расчетное закрытие."
}, },
{ {
state_code: "settlement_closed", state_code: "settlement_closed",
state_label: "Расчет закрыт", state_label: "Расчет закрыт",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["payment_to_settlement_linked"], entry_conditions: ["payment_to_settlement_linked"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Платеж доведен РґРѕ расчетного результата." business_meaning: "Платеж доведен до расчетного результата."
}, },
{ {
state_code: "stale_unlinked_payment", state_code: "stale_unlinked_payment",
state_label: "Платеж завис без закрытия", state_label: "Платеж завис без закрытия",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["bank_recorded", "missing_link"], entry_conditions: ["bank_recorded", "missing_link"],
exit_conditions: ["settlement_closed"], exit_conditions: ["settlement_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Платеж отражен, РЅРѕ ожидаемая СЃРІСЏР·СЊ РїРѕ расчету РЅРµ завершена." business_meaning: "Платеж отражен, но ожидаемая связь по расчету не завершена."
}, },
{ {
state_code: "misclosed_payment", state_code: "misclosed_payment",
state_label: "Платеж закрыт некорректно", state_label: "Платеж закрыт некорректно",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["wrong_document_type_or_posting_mismatch"], entry_conditions: ["wrong_document_type_or_posting_mismatch"],
exit_conditions: ["settlement_closed"], exit_conditions: ["settlement_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Формальное закрытие есть, РЅРѕ путь закрытия неверный." business_meaning: "Формальное закрытие есть, но путь закрытия неверный."
} }
], ],
transitions: [ transitions: [
@ -207,7 +207,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
required_evidence: ["bank_statement_recorded"], required_evidence: ["bank_statement_recorded"],
optional_evidence: ["payment_order"], optional_evidence: ["payment_order"],
forbidden_conditions: [], forbidden_conditions: [],
business_meaning: "Платеж должен появиться РІРѕ выписке." business_meaning: "Платеж должен появиться во выписке."
}, },
{ {
from_state: "bank_recorded", from_state: "bank_recorded",
@ -216,7 +216,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
required_evidence: ["payment_to_settlement_link"], required_evidence: ["payment_to_settlement_link"],
optional_evidence: ["document_to_posting"], optional_evidence: ["document_to_posting"],
forbidden_conditions: ["wrong_document_type"], forbidden_conditions: ["wrong_document_type"],
business_meaning: "После выписки должен закрываться расчет." business_meaning: "После выписки должен закрываться расчет."
} }
], ],
defects: [] defects: []
@ -228,43 +228,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "invoice_issued", state_code: "invoice_issued",
state_label: "Реализация отражена", state_label: "Реализация отражена",
state_class: "initial", state_class: "initial",
entry_conditions: ["realization_document_exists"], entry_conditions: ["realization_document_exists"],
exit_conditions: ["payment_recorded"], exit_conditions: ["payment_recorded"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Возникла дебиторская позиция." business_meaning: "Возникла дебиторская позиция."
}, },
{ {
state_code: "payment_recorded", state_code: "payment_recorded",
state_label: "Оплата отражена", state_label: "Оплата отражена",
state_class: "active", state_class: "active",
entry_conditions: ["payment_document_exists"], entry_conditions: ["payment_document_exists"],
exit_conditions: ["receivable_closed"], exit_conditions: ["receivable_closed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Оплата есть, ожидается корректное закрытие." business_meaning: "Оплата есть, ожидается корректное закрытие."
}, },
{ {
state_code: "receivable_closed", state_code: "receivable_closed",
state_label: "Дебиторка закрыта", state_label: "Дебиторка закрыта",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["closing_document_linked"], entry_conditions: ["closing_document_linked"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Дебиторская позиция закрыта корректно." business_meaning: "Дебиторская позиция закрыта корректно."
}, },
{ {
state_code: "stale_receivable", state_code: "stale_receivable",
state_label: "Дебиторка зависла", state_label: "Дебиторка зависла",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["unresolved_settlement"], entry_conditions: ["unresolved_settlement"],
exit_conditions: ["receivable_closed"], exit_conditions: ["receivable_closed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Позиция остается незавершенной дольше ожидаемого." business_meaning: "Позиция остается незавершенной дольше ожидаемого."
} }
], ],
transitions: [ transitions: [
@ -275,7 +275,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
required_evidence: ["payment_document_exists"], required_evidence: ["payment_document_exists"],
optional_evidence: [], optional_evidence: [],
forbidden_conditions: [], forbidden_conditions: [],
business_meaning: "После реализации ожидается оплата/зачет." business_meaning: "После реализации ожидается оплата/зачет."
}, },
{ {
from_state: "payment_recorded", from_state: "payment_recorded",
@ -284,7 +284,7 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
required_evidence: ["closing_document_linked"], required_evidence: ["closing_document_linked"],
optional_evidence: ["register_movement_exists"], optional_evidence: ["register_movement_exists"],
forbidden_conditions: ["cross_branch_inconsistency"], forbidden_conditions: ["cross_branch_inconsistency"],
business_meaning: "Оплата должна завершаться корректным закрытием расчета." business_meaning: "Оплата должна завершаться корректным закрытием расчета."
} }
], ],
defects: [] defects: []
@ -296,43 +296,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "recognized", state_code: "recognized",
state_label: "РБП признан", state_label: "РБП признан",
state_class: "initial", state_class: "initial",
entry_conditions: ["deferred_expense_created"], entry_conditions: ["deferred_expense_created"],
exit_conditions: ["writeoff_started"], exit_conditions: ["writeoff_started"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "РБП поставлен РЅР° учет." business_meaning: "РБП поставлен на учет."
}, },
{ {
state_code: "partially_written_off", state_code: "partially_written_off",
state_label: "Частичное списание", state_label: "Частичное списание",
state_class: "active", state_class: "active",
entry_conditions: ["partial_writeoff_exists"], entry_conditions: ["partial_writeoff_exists"],
exit_conditions: ["fully_written_off"], exit_conditions: ["fully_written_off"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Списание идет РїРѕ графику." business_meaning: "Списание идет по графику."
}, },
{ {
state_code: "fully_written_off", state_code: "fully_written_off",
state_label: "РБП полностью списан", state_label: "РБП полностью списан",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["full_writeoff_exists"], entry_conditions: ["full_writeoff_exists"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "РБП завершил lifecycle." business_meaning: "РБП завершил lifecycle."
}, },
{ {
state_code: "overdue_writeoff", state_code: "overdue_writeoff",
state_label: "Просроченное списание", state_label: "Просроченное списание",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["period_boundary", "missing_link"], entry_conditions: ["period_boundary", "missing_link"],
exit_conditions: ["fully_written_off"], exit_conditions: ["fully_written_off"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "РБП живет дольше допустимого РѕРєРЅР°." business_meaning: "РБП живет дольше допустимого окна."
} }
], ],
transitions: [], transitions: [],
@ -345,53 +345,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "capitalized", state_code: "capitalized",
state_label: "Капвложения отражены", state_label: "Капвложения отражены",
state_class: "initial", state_class: "initial",
entry_conditions: ["capitalization_document_exists"], entry_conditions: ["capitalization_document_exists"],
exit_conditions: ["accepted_for_accounting"], exit_conditions: ["accepted_for_accounting"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Объект зафиксирован как вложение." business_meaning: "Объект зафиксирован как вложение."
}, },
{ {
state_code: "accepted_for_accounting", state_code: "accepted_for_accounting",
state_label: "РџСЂРёРЅСЏС‚ Рє учету", state_label: "Принят к учету",
state_class: "active", state_class: "active",
entry_conditions: ["acceptance_document_exists"], entry_conditions: ["acceptance_document_exists"],
exit_conditions: ["depreciation_active"], exit_conditions: ["depreciation_active"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Объект переведен РІ РѕСЃРЅРѕРІРЅРѕР№ контур учета." business_meaning: "Объект переведен в основной контур учета."
}, },
{ {
state_code: "depreciation_active", state_code: "depreciation_active",
state_label: "Амортизация активна", state_label: "Амортизация активна",
state_class: "active", state_class: "active",
entry_conditions: ["depreciation_register_movement"], entry_conditions: ["depreciation_register_movement"],
exit_conditions: ["disposed"], exit_conditions: ["disposed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Жизненный цикл РћРЎ идет штатно." business_meaning: "Жизненный цикл ОС идет штатно."
}, },
{ {
state_code: "contradictory_asset_state", state_code: "contradictory_asset_state",
state_label: "Противоречивый статус РћРЎ", state_label: "Противоречивый статус ОС",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["posting_mismatch_or_wrong_path"], entry_conditions: ["posting_mismatch_or_wrong_path"],
exit_conditions: ["depreciation_active"], exit_conditions: ["depreciation_active"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Статус РћРЎ формально есть, РЅРѕ смыслово противоречив." business_meaning: "Статус ОС формально есть, но смыслово противоречив."
}, },
{ {
state_code: "disposed", state_code: "disposed",
state_label: "Выбыл", state_label: "Выбыл",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["disposal_document_exists"], entry_conditions: ["disposal_document_exists"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Жизненный цикл РћРЎ завершен." business_meaning: "Жизненный цикл ОС завершен."
} }
], ],
transitions: [], transitions: [],
@ -404,43 +404,43 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "vat_registered", state_code: "vat_registered",
state_label: "НДС отражен документно", state_label: "НДС отражен документно",
state_class: "initial", state_class: "initial",
entry_conditions: ["invoice_registered"], entry_conditions: ["invoice_registered"],
exit_conditions: ["vat_reflected"], exit_conditions: ["vat_reflected"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Сформирован первичный документный слой НДС." business_meaning: "Сформирован первичный документный слой НДС."
}, },
{ {
state_code: "vat_reflected", state_code: "vat_reflected",
state_label: "НДС отражен РІ учете", state_label: "НДС отражен в учете",
state_class: "active", state_class: "active",
entry_conditions: ["vat_register_movement"], entry_conditions: ["vat_register_movement"],
exit_conditions: ["vat_deducted"], exit_conditions: ["vat_deducted"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "НДС РїСЂРѕС…РѕРґРёС‚ штатную стадию отражения." business_meaning: "НДС проходит штатную стадию отражения."
}, },
{ {
state_code: "vat_deducted", state_code: "vat_deducted",
state_label: "НДС РїСЂРёРЅСЏС‚ Рє вычету", state_label: "НДС принят к вычету",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["deduction_confirmed"], entry_conditions: ["deduction_confirmed"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "НДС-цепочка завершена корректно." business_meaning: "НДС-цепочка завершена корректно."
}, },
{ {
state_code: "vat_conflict", state_code: "vat_conflict",
state_label: "Конфликт НДС-цепочки", state_label: "Конфликт НДС-цепочки",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["cross_branch_inconsistency"], entry_conditions: ["cross_branch_inconsistency"],
exit_conditions: ["vat_reflected"], exit_conditions: ["vat_reflected"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Бухгалтерская Рё налоговая ветки расходятся." business_meaning: "Бухгалтерская и налоговая ветки расходятся."
} }
], ],
transitions: [], transitions: [],
@ -453,53 +453,53 @@ const LIFECYCLE_DOMAIN_MODELS: Record<LifecycleDomain, LifecycleDomainModel> = {
states: [ states: [
{ {
state_code: "preclose_checks", state_code: "preclose_checks",
state_label: "Предзакрытие", state_label: "Предзакрытие",
state_class: "active", state_class: "active",
entry_conditions: ["period_scope_detected"], entry_conditions: ["period_scope_detected"],
exit_conditions: ["close_ready"], exit_conditions: ["close_ready"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Идет проверка готовности периода." business_meaning: "Идет проверка готовности периода."
}, },
{ {
state_code: "close_ready", state_code: "close_ready",
state_label: "Готов Рє закрытию", state_label: "Готов к закрытию",
state_class: "active", state_class: "active",
entry_conditions: ["no_blockers_detected"], entry_conditions: ["no_blockers_detected"],
exit_conditions: ["close_completed"], exit_conditions: ["close_completed"],
is_terminal: false, is_terminal: false,
is_problematic: false, is_problematic: false,
business_meaning: "Период может быть закрыт." business_meaning: "Период может быть закрыт."
}, },
{ {
state_code: "close_completed", state_code: "close_completed",
state_label: "Закрытие завершено", state_label: "Закрытие завершено",
state_class: "terminal", state_class: "terminal",
entry_conditions: ["close_operation_done"], entry_conditions: ["close_operation_done"],
exit_conditions: [], exit_conditions: [],
is_terminal: true, is_terminal: true,
is_problematic: false, is_problematic: false,
business_meaning: "Период закрыт." business_meaning: "Период закрыт."
}, },
{ {
state_code: "close_blocked", state_code: "close_blocked",
state_label: "Закрытие заблокировано", state_label: "Закрытие заблокировано",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["period_close_risk_or_stale_state"], entry_conditions: ["period_close_risk_or_stale_state"],
exit_conditions: ["close_ready"], exit_conditions: ["close_ready"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Есть lifecycle-дефекты, влияющие РЅР° закрытие." business_meaning: "Есть lifecycle-дефекты, влияющие на закрытие."
}, },
{ {
state_code: "close_contradicted", state_code: "close_contradicted",
state_label: "Закрыт формально, РЅРѕ СЃ противоречием", state_label: "Закрыт формально, но с противоречием",
state_class: "problematic", state_class: "problematic",
entry_conditions: ["misclosed_or_cross_branch_conflict"], entry_conditions: ["misclosed_or_cross_branch_conflict"],
exit_conditions: ["close_completed"], exit_conditions: ["close_completed"],
is_terminal: false, is_terminal: false,
is_problematic: true, is_problematic: true,
business_meaning: "Формальное закрытие РЅРµ согласовано СЃ фактическими ветками." business_meaning: "Формальное закрытие не согласовано с фактическими ветками."
} }
], ],
transitions: [], transitions: [],
@ -512,7 +512,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "missing_expected_transition", defect_code: "missing_expected_transition",
defect_class: "path", defect_class: "path",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Ожидаемый переход РЅРµ произошел.", business_meaning: "Ожидаемый переход не произошел.",
evidence_requirements: ["expected_state", "missing_transition_signal"], evidence_requirements: ["expected_state", "missing_transition_signal"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -520,7 +520,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "invalid_transition", defect_code: "invalid_transition",
defect_class: "path", defect_class: "path",
severity_hint: "high", severity_hint: "high",
business_meaning: "Переход произошел РїРѕ некорректному пути.", business_meaning: "Переход произошел по некорректному пути.",
evidence_requirements: ["invalid_transition_signal"], evidence_requirements: ["invalid_transition_signal"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -528,7 +528,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "stale_active_state", defect_code: "stale_active_state",
defect_class: "timing", defect_class: "timing",
severity_hint: "high", severity_hint: "high",
business_meaning: "Объект завис РІ активном состоянии.", business_meaning: "Объект завис в активном состоянии.",
evidence_requirements: ["stale_marker", "missing_transition_signal"], evidence_requirements: ["stale_marker", "missing_transition_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -536,7 +536,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "contradictory_state", defect_code: "contradictory_state",
defect_class: "consistency", defect_class: "consistency",
severity_hint: "high", severity_hint: "high",
business_meaning: "Статусы объекта противоречат РґСЂСѓРі РґСЂСѓРіСѓ.", business_meaning: "Статусы объекта противоречат друг другу.",
evidence_requirements: ["contradiction_signal"], evidence_requirements: ["contradiction_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -544,7 +544,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "premature_terminal_state", defect_code: "premature_terminal_state",
defect_class: "closure", defect_class: "closure",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Терминальное состояние наступило преждевременно.", business_meaning: "Терминальное состояние наступило преждевременно.",
evidence_requirements: ["terminal_state", "missing_required_previous_state"], evidence_requirements: ["terminal_state", "missing_required_previous_state"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -552,7 +552,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "misclosed_state", defect_code: "misclosed_state",
defect_class: "closure", defect_class: "closure",
severity_hint: "high", severity_hint: "high",
business_meaning: "Контур формально закрыт, РЅРѕ закрыт неверно.", business_meaning: "Контур формально закрыт, но закрыт неверно.",
evidence_requirements: ["wrong_closure_path"], evidence_requirements: ["wrong_closure_path"],
period_impact_potential: "direct" period_impact_potential: "direct"
}, },
@ -560,7 +560,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "orphan_intermediate_state", defect_code: "orphan_intermediate_state",
defect_class: "path", defect_class: "path",
severity_hint: "medium", severity_hint: "medium",
business_meaning: "Промежуточная стадия осталась без корректного продолжения.", business_meaning: "Промежуточная стадия осталась без корректного продолжения.",
evidence_requirements: ["intermediate_state_without_next"], evidence_requirements: ["intermediate_state_without_next"],
period_impact_potential: "indirect" period_impact_potential: "indirect"
}, },
@ -568,7 +568,7 @@ const SHARED_DEFECTS: LifecycleDefectDefinition[] = [
defect_code: "cross_branch_state_conflict", defect_code: "cross_branch_state_conflict",
defect_class: "consistency", defect_class: "consistency",
severity_hint: "high", severity_hint: "high",
business_meaning: "Состояния соседних веток учета противоречат РґСЂСѓРі РґСЂСѓРіСѓ.", business_meaning: "Состояния соседних веток учета противоречат друг другу.",
evidence_requirements: ["cross_branch_conflict_signal"], evidence_requirements: ["cross_branch_conflict_signal"],
period_impact_potential: "direct" period_impact_potential: "direct"
} }
@ -905,23 +905,23 @@ function lifecycleInterpretation(input: {
missingTransition: string | null; missingTransition: string | null;
invalidTransition: string | null; invalidTransition: string | null;
}): string { }): string {
const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`; const base = `Текущая стадия: ${input.currentState}; ожидаемая стадия: ${input.expectedState}.`;
if (input.defect === "stale_active_state") { if (input.defect === "stale_active_state") {
return `${base} Объект завис РІРѕ времени Рё РЅРµ дошел РґРѕ ожидаемого перехода.`; return `${base} Объект завис во времени и не дошел до ожидаемого перехода.`;
} }
if (input.defect === "misclosed_state") { if (input.defect === "misclosed_state") {
return `${base} Контур закрыт формально, РЅРѕ путь закрытия противоречит бухгалтерской логике.`; return `${base} Контур закрыт формально, но путь закрытия противоречит бухгалтерской логике.`;
} }
if (input.defect === "cross_branch_state_conflict") { if (input.defect === "cross_branch_state_conflict") {
return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`; return `${base} Между ветками домена ${input.domain} обнаружено противоречие состояний.`;
} }
if (input.defect === "missing_expected_transition") { if (input.defect === "missing_expected_transition") {
return `${base} РќРµ зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`; return `${base} Не зафиксирован ожидаемый переход (${input.missingTransition ?? "unknown_transition"}).`;
} }
if (input.defect === "invalid_transition") { if (input.defect === "invalid_transition") {
return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`; return `${base} Зафиксирован некорректный переход (${input.invalidTransition ?? "invalid_transition"}).`;
} }
return `${base} Lifecycle-разрешение РЅРµ выявило критичный дефект, РЅРѕ состояние требует наблюдения.`; return `${base} Lifecycle-разрешение не выявило критичный дефект, но состояние требует наблюдения.`;
} }
export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution { export function resolveLifecycle(input: LifecycleResolverInput): LifecycleResolution {

View File

@ -78,10 +78,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
}, },
normalizer_v1_1_2_1: { normalizer_v1_1_2_1: {
id: "default-normalizer-v1_1_2_1", id: "default-normalizer-v1_1_2_1",
name: "Стандартный пресет NDC v1.1.2.1", name: "Стандартный пресет NDC v1.1.2.1",
promptVersion: "normalizer_v1_1_2_1", promptVersion: "normalizer_v1_1_2_1",
schemaNotes: schemaNotes:
"v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.", "v1.1.2.1: stable prompt baseline v1.1.2 + accounting-review phrasing anchors for 30-case validation pack. Схема normalized_query_v1 без изменений.",
files: { files: {
system: path.join("system", "default.txt"), system: path.join("system", "default.txt"),
developer: path.join("developer", "normalizer_v1_1_2_1.txt"), developer: path.join("developer", "normalizer_v1_1_2_1.txt"),
@ -91,10 +91,10 @@ const BUILTIN_PROMPT_PRESETS: Record<PromptVersion, BuiltinPromptPresetDefinitio
}, },
normalizer_v2: { normalizer_v2: {
id: "default-normalizer-v2", id: "default-normalizer-v2",
name: "Стандартный пресет NDC v2", name: "Стандартный пресет NDC v2",
promptVersion: "normalizer_v2", promptVersion: "normalizer_v2",
schemaNotes: schemaNotes:
"v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.", "v2: decomposition-first pre-router. LLM returns fragments + scope + flags; deterministic routing happens in code. Схема normalized_query_v2.",
files: { files: {
system: path.join("system", "default.txt"), system: path.join("system", "default.txt"),
developer: path.join("developer", "normalizer_v2.txt"), developer: path.join("developer", "normalizer_v2.txt"),

View File

@ -79,7 +79,7 @@ const AMBIGUITY_PATTERN =
/(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i; /(?:\bmaybe\b|\bperhaps\b|not\s+sure|i\s+only\s+know|part\s+may\s+be\s+missing|возможно|может\s+быть|не\s+уверен|не\s+знаю|часть\s+цепочки\s+не\s+подтвержд)/i;
const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i; const TRANSLIT_PROBLEM_PATTERN = /(?:raschet|oplata|zakryt|nds|vychet|zatrat|ostatok|cepoch|perehod|pochemu|prichin|period)/i;
const DOMAIN_LEXICAL_ANCHOR_PATTERN = const DOMAIN_LEXICAL_ANCHOR_PATTERN =
/(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|РЅРґСЃ|СЃС‡[её]С.?фактур|РєРЅРёРі[аи]|затрат|закрыт|остатк)/i; /(?:\b(?:settlement|payment|bank|supplier|customer|vat|nds|invoice|register|book|period\s*close|month\s*close|close\s*operation|allocation|residual|cost|expenses?)\b|оплат|расчет|ндс|сч[её]С.?фактур|книг[аи]|затрат|закрыт|остатк)/i;
export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [ export const ROUTE_DISCIPLINE_RULE_TABLE: RouteDisciplineRule[] = [
{ {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NDC AI Normalizer Playground</title> <title>NDC AI Normalizer Playground</title>
<script type="module" crossorigin src="/assets/index-D6Y_lHrc.js"></script> <script type="module" crossorigin src="/assets/index-BDtb8kxy.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BMWPMdQA.css"> <link rel="stylesheet" crossorigin href="/assets/index-iFxz5cXp.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@ -10,6 +10,7 @@ import { PromptPanel } from "./components/PromptPanel";
import { QueryPanel } from "./components/QueryPanel"; import { QueryPanel } from "./components/QueryPanel";
import { RuntimePanel } from "./components/RuntimePanel"; import { RuntimePanel } from "./components/RuntimePanel";
import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults"; import { DEFAULT_CONNECTION, DEFAULT_PROMPTS, DEFAULT_QUERY } from "./state/defaults";
import { designConfig } from "../../../designconfig";
import type { import type {
AssistantConversationItem, AssistantConversationItem,
ConnectionState, ConnectionState,
@ -23,7 +24,7 @@ import type {
} from "./state/types"; } from "./state/types";
const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1"; const SESSION_CONFIG_KEY = "ndc_normalizer_session_config_v1";
const ASSISTANT_STAGES = ["Analyzing request", "Fetching data", "Composing answer"]; const ASSISTANT_STAGES = ["Анализ запроса", "Получение данных", "Подготовка ответа"];
const DEFAULT_UI_MODE: UiMode = "assistant"; const DEFAULT_UI_MODE: UiMode = "assistant";
const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2"; const AUTOLOAD_PROMPT_VERSION = "normalizer_v2_0_2";
const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1"; const ASSISTANT_PROMPT_VERSION = "address_query_runtime_v1";
@ -79,6 +80,9 @@ export default function App() {
const [lastError, setLastError] = useState(""); const [lastError, setLastError] = useState("");
const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE); const [uiMode, setUiMode] = useState<UiMode>(DEFAULT_UI_MODE);
const [showAutorunsAssistantMode, setShowAutorunsAssistantMode] = useState(true);
const [showAutorunsDecompositionMode, setShowAutorunsDecompositionMode] = useState(true);
const [showAutorunsProgressMode, setShowAutorunsProgressMode] = useState(true);
const [assistantSessionId, setAssistantSessionId] = useState(""); const [assistantSessionId, setAssistantSessionId] = useState("");
const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]); const [assistantConversation, setAssistantConversation] = useState<AssistantConversationItem[]>([]);
const [assistantInput, setAssistantInput] = useState(""); const [assistantInput, setAssistantInput] = useState("");
@ -87,6 +91,23 @@ export default function App() {
const [assistantError, setAssistantError] = useState(""); const [assistantError, setAssistantError] = useState("");
const presetAutoloadDoneRef = useRef(false); const presetAutoloadDoneRef = useRef(false);
useEffect(() => {
const root = document.documentElement;
const { colors } = designConfig;
root.style.setProperty("--rgb-background", colors.backgroundRgb);
root.style.setProperty("--rgb-surface-main", colors.mainSurfaceRgb);
root.style.setProperty("--rgb-surface-horizontal", colors.horizontalSurfaceRgb);
root.style.setProperty("--rgb-surface-focus", colors.focusSurfaceRgb);
root.style.setProperty("--rgb-active", colors.activeRgb);
root.style.setProperty("--rgb-active-text", colors.activeTextRgb);
root.style.setProperty("--rgb-text-main", colors.textMainRgb);
root.style.setProperty("--rgb-text-muted", colors.textMutedRgb);
root.style.setProperty("--rgb-danger", colors.dangerRgb);
root.style.setProperty("--rgb-scrollbar-track", colors.scrollbarTrackRgb);
root.style.setProperty("--rgb-scrollbar-thumb", colors.scrollbarThumbRgb);
root.style.setProperty("--rgb-scrollbar-thumb-hover", colors.scrollbarThumbHoverRgb);
}, []);
const log = (message: string) => { const log = (message: string) => {
setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300)); setAppLogs((prev) => [withTs(message), ...prev].slice(0, 300));
}; };
@ -509,12 +530,12 @@ export default function App() {
}); });
setAssistantSessionId(response.session_id); setAssistantSessionId(response.session_id);
setAssistantConversation(response.conversation); setAssistantConversation(response.conversation);
setAssistantStatus("Reply is ready"); setAssistantStatus("Ответ готов");
log(`Assistant reply received: trace=${response.debug.trace_id}`); log(`Assistant reply received: trace=${response.debug.trace_id}`);
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);
setAssistantError(message); setAssistantError(message);
setAssistantStatus("Assistant error"); setAssistantStatus("Ошибка ассистента");
log(`Assistant error: ${message}`); log(`Assistant error: ${message}`);
} finally { } finally {
stopTicker(); stopTicker();
@ -534,24 +555,47 @@ export default function App() {
}, [selectedRunId]); }, [selectedRunId]);
return ( return (
<main className="app-root"> <main className={`app-root ${uiMode === "autoruns" ? "app-root-autoruns" : ""}`}>
<div className="hero"> <header className="app-topbar">
<h1>NDC AI First Layer</h1>
<p>Three modes in one UI: assistant, decomposition diagnostics, and auto-run history with regression visibility.</p>
</div>
<div className="mode-switch-row"> <div className="mode-switch-row">
<button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}> <button type="button" className={uiMode === "assistant" ? "tab active" : "tab"} onClick={() => setUiMode("assistant")}>
Assistant Ассистент
</button> </button>
<button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}> <button type="button" className={uiMode === "decomposition" ? "tab active" : "tab"} onClick={() => setUiMode("decomposition")}>
Decomposition Декомпозиция
</button> </button>
<button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}> <button type="button" className={uiMode === "autoruns" ? "tab active" : "tab"} onClick={() => setUiMode("autoruns")}>
AutoRun History История автопрогонов
</button> </button>
</div> </div>
{uiMode === "autoruns" ? (
<div className="mode-switch-row mode-switch-row-right">
<button
type="button"
className={showAutorunsAssistantMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsAssistantMode((prev) => !prev)}
>
Режим ассистента
</button>
<button
type="button"
className={showAutorunsDecompositionMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsDecompositionMode((prev) => !prev)}
>
Режим декомпозиции
</button>
<button
type="button"
className={showAutorunsProgressMode ? "tab active" : "tab"}
onClick={() => setShowAutorunsProgressMode((prev) => !prev)}
>
Прогресс/регресс
</button>
</div>
) : null}
</header>
{uiMode === "assistant" ? ( {uiMode === "assistant" ? (
<div className="layout-grid"> <div className="layout-grid">
<ConnectionPanel <ConnectionPanel
@ -660,12 +704,15 @@ export default function App() {
/> />
</div> </div>
) : ( ) : (
<div className="layout-grid"> <div className="layout-grid layout-grid-autoruns">
<AutoRunsHistoryPanel <AutoRunsHistoryPanel
connection={connection} connection={connection}
prompts={prompts} prompts={prompts}
assistantPromptVersion={ASSISTANT_PROMPT_VERSION} assistantPromptVersion={ASSISTANT_PROMPT_VERSION}
decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION} decompositionPromptVersion={AUTOLOAD_PROMPT_VERSION}
showAssistantMode={showAutorunsAssistantMode}
showDecompositionMode={showAutorunsDecompositionMode}
showProgressMode={showAutorunsProgressMode}
onLog={log} onLog={log}
/> />
</div> </div>

View File

@ -1,11 +1,17 @@
import type { import type {
AutoGenHistoryResponse,
AutoGenMode,
AutoRunAnnotationsResponse,
AutoRunAnnotationRecord,
AutoRunDetailResponse, AutoRunDetailResponse,
AutoRunDialogResponse, AutoRunDialogResponse,
AutoRunHistoryResponse, AutoRunHistoryResponse,
AutoRunPostAnalysisResponse,
AssistantMessageResultState, AssistantMessageResultState,
AssistantConversationItem, AssistantConversationItem,
ConnectionState, ConnectionState,
HistoryItem, HistoryItem,
ManualCaseDecision,
NormalizeResultState, NormalizeResultState,
PromptState, PromptState,
RuntimeRun RuntimeRun
@ -281,5 +287,95 @@ export const apiClient = {
async loadAutoRunCaseDialog(runId: string, caseId: string): Promise<AutoRunDialogResponse> { async loadAutoRunCaseDialog(runId: string, caseId: string): Promise<AutoRunDialogResponse> {
return request(`/autoruns/history/${encodeURIComponent(runId)}/case/${encodeURIComponent(caseId)}/dialog`); return request(`/autoruns/history/${encodeURIComponent(runId)}/case/${encodeURIComponent(caseId)}/dialog`);
},
async loadAutoRunAnnotations(input?: {
run_id?: string;
case_id?: string;
min_rating?: number;
manual_case_decision?: ManualCaseDecision | "all";
limit?: number;
}): Promise<AutoRunAnnotationsResponse> {
const params = new URLSearchParams();
if (input?.run_id) params.set("run_id", input.run_id);
if (input?.case_id) params.set("case_id", input.case_id);
if (typeof input?.min_rating === "number") params.set("min_rating", String(input.min_rating));
if (input?.manual_case_decision) params.set("manual_case_decision", input.manual_case_decision);
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
const query = params.toString();
return request(`/autoruns/annotations${query ? `?${query}` : ""}`);
},
async saveAutoRunAnnotation(input: {
run_id: string;
case_id: string;
message_index: number;
rating: number;
comment: string;
manual_case_decision: ManualCaseDecision;
annotation_author?: string;
}): Promise<{ ok: boolean; annotation: AutoRunAnnotationRecord; case_annotation_stats: { count: number; latest_at: string | null; avg_rating: number | null } | null }> {
return request("/autoruns/annotations", {
method: "POST",
body: JSON.stringify(input)
});
},
async loadAutoRunPostAnalysis(input?: {
run_id?: string;
limit_per_queue?: number;
annotation_limit?: number;
scan_limit?: number;
from?: string;
to?: string;
target?: string;
mode?: string;
use_mock?: "any" | "true" | "false";
prompt_contains?: string;
}): Promise<AutoRunPostAnalysisResponse> {
const params = new URLSearchParams();
if (input?.run_id) params.set("run_id", input.run_id);
if (typeof input?.limit_per_queue === "number") params.set("limit_per_queue", String(input.limit_per_queue));
if (typeof input?.annotation_limit === "number") params.set("annotation_limit", String(input.annotation_limit));
if (typeof input?.scan_limit === "number") params.set("scan_limit", String(input.scan_limit));
if (input?.from) params.set("from", input.from);
if (input?.to) params.set("to", input.to);
if (input?.target) params.set("target", input.target);
if (input?.mode) params.set("mode", input.mode);
if (input?.use_mock) params.set("use_mock", input.use_mock);
if (input?.prompt_contains) params.set("prompt_contains", input.prompt_contains);
const query = params.toString();
return request(`/autoruns/post-analysis${query ? `?${query}` : ""}`);
},
async loadAutoRunAutogenHistory(input?: {
mode?: AutoGenMode;
limit?: number;
}): Promise<AutoGenHistoryResponse> {
const params = new URLSearchParams();
if (input?.mode) params.set("mode", input.mode);
if (typeof input?.limit === "number") params.set("limit", String(input.limit));
const query = params.toString();
return request(`/autoruns/autogen/history${query ? `?${query}` : ""}`);
},
async generateAutoRunQuestions(input: {
mode: AutoGenMode;
count: number;
domain?: string;
persist_to_eval_cases?: boolean;
generated_by?: string;
context?: {
llm_provider?: string;
model?: string;
assistant_prompt_version?: string;
decomposition_prompt_version?: string;
prompt_fingerprint?: string;
};
}): Promise<{ ok: boolean; generation: { generation_id: string; created_at: string; mode: AutoGenMode; count: number; domain: string | null; questions: string[]; generated_by: string | null; saved_case_set_file: string | null; context: Record<string, unknown> | null } }> {
return request("/autoruns/autogen/generate", {
method: "POST",
body: JSON.stringify(input)
});
} }
}; };

View File

@ -4,11 +4,14 @@ interface PanelFrameProps {
title: string; title: string;
subtitle?: string; subtitle?: string;
actions?: ReactNode; actions?: ReactNode;
className?: string;
hideHeader?: boolean;
} }
export function PanelFrame({ title, subtitle, actions, children }: PropsWithChildren<PanelFrameProps>) { export function PanelFrame({ title, subtitle, actions, className, hideHeader, children }: PropsWithChildren<PanelFrameProps>) {
return ( return (
<section className="panel-frame"> <section className={className ? `panel-frame ${className}` : "panel-frame"}>
{!hideHeader ? (
<header className="panel-header"> <header className="panel-header">
<div> <div>
<h2>{title}</h2> <h2>{title}</h2>
@ -16,6 +19,7 @@ export function PanelFrame({ title, subtitle, actions, children }: PropsWithChil
</div> </div>
{actions ? <div className="panel-actions">{actions}</div> : null} {actions ? <div className="panel-actions">{actions}</div> : null}
</header> </header>
) : null}
<div className="panel-body">{children}</div> <div className="panel-body">{children}</div>
</section> </section>
); );

View File

@ -70,6 +70,17 @@ export type UiMode = "assistant" | "decomposition" | "autoruns";
export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown"; export type AutoRunTarget = "normalizer" | "assistant_stage1" | "assistant_stage2" | "assistant_p0" | "unknown";
export type AutoRunTrend = "up" | "down" | "flat"; export type AutoRunTrend = "up" | "down" | "flat";
export type AutoGenMode = "qwen_seed" | "codex_creative";
export type ManualCaseDecision =
| "covered_ok"
| "covered_but_bad_answer"
| "candidate_for_implementation"
| "needs_routing_extension"
| "out_of_scope_but_answer_softly"
| "unsafe_question_limit_strictly"
| "needs_dialog_policy_fix"
| "needs_capability_registry_update"
| "bad_test_case";
export interface AutoRunDomainCoverage { export interface AutoRunDomainCoverage {
domain: string; domain: string;
@ -109,6 +120,9 @@ export interface AutoRunCaseSummary {
reply_type: string | null; reply_type: string | null;
session_id: string; session_id: string;
dialog_available: boolean; dialog_available: boolean;
commented_count: number;
latest_annotation_at: string | null;
avg_rating: number | null;
checks: Record<string, unknown> | null; checks: Record<string, unknown> | null;
metric_subscores: Record<string, unknown> | null; metric_subscores: Record<string, unknown> | null;
} }
@ -158,15 +172,79 @@ export interface AutoRunDetailResponse {
run: AutoRunSummary; run: AutoRunSummary;
coverage: AutoRunCoverage; coverage: AutoRunCoverage;
cases: AutoRunCaseSummary[]; cases: AutoRunCaseSummary[];
annotations_summary?: {
total: number;
};
report: Record<string, unknown>; report: Record<string, unknown>;
} }
export interface AutoRunAnnotationRecord {
annotation_id: string;
run_id: string;
case_id: string;
session_id: string;
message_index: number;
rating: number;
comment: string;
manual_case_decision: ManualCaseDecision;
annotation_author: string | null;
created_at: string;
updated_at: string;
context: {
message_id: string | null;
trace_id: string | null;
reply_type: string | null;
eval_target: AutoRunTarget | "unknown";
prompt_version: string | null;
domain: string | null;
query_class: string | null;
};
}
export interface AutoRunAnnotationListItem extends AutoRunAnnotationRecord {
run: AutoRunSummary | null;
case_summary: AutoRunCaseSummary | null;
technical_context: {
report_path: string | null;
trace_id: string | null;
reply_type: string | null;
domain: string | null;
query_class: string | null;
checks: Record<string, unknown> | null;
metric_subscores: Record<string, unknown> | null;
};
}
export interface AutoRunAnnotationsResponse {
ok: boolean;
generated_at: string;
filters_applied: {
run_id: string | null;
case_id: string | null;
min_rating: number | null;
manual_case_decision: ManualCaseDecision | "all";
limit: number;
};
stats: {
total: number;
avg_rating: number | null;
by_decision: Record<string, number>;
};
available_manual_case_decisions: ManualCaseDecision[];
manual_case_decision_schema: Record<string, unknown>;
items: AutoRunAnnotationListItem[];
}
export interface AutoRunDialogMessage { export interface AutoRunDialogMessage {
message_id: string | null;
role: string; role: string;
text: string; text: string;
created_at: string | null; created_at: string | null;
trace_id: string | null; trace_id: string | null;
reply_type: string | null; reply_type: string | null;
message_index: number;
commented: boolean;
annotation: AutoRunAnnotationRecord | null;
} }
export interface AutoRunDialogResponse { export interface AutoRunDialogResponse {
@ -178,6 +256,77 @@ export interface AutoRunDialogResponse {
messages: AutoRunDialogMessage[]; messages: AutoRunDialogMessage[];
decomposition: string[]; decomposition: string[];
assistant_mode: Record<string, unknown> | null; assistant_mode: Record<string, unknown> | null;
annotations: AutoRunAnnotationRecord[];
}
export interface AutoRunPostAnalysisQueueItem {
annotation_id: string;
run_id: string;
case_id: string;
message_index: number;
rating: number;
comment: string;
manual_case_decision: ManualCaseDecision;
annotation_author: string | null;
updated_at: string;
domain: string | null;
query_class: string | null;
trace_id: string | null;
reply_type: string | null;
nearest_capability_group: {
group_code: string;
group_title: string;
maturity_status: string;
} | null;
}
export interface AutoRunPostAnalysisResponse {
ok: boolean;
generated_at: string;
filters_applied: {
run_id: string | null;
run_filters_applied: boolean;
limit_per_queue: number;
annotation_limit: number;
scan_limit: number;
};
runs_considered: AutoRunSummary[];
manual_case_decision_schema: Record<string, unknown>;
post_analysis: {
stats: {
annotations_total: number;
by_decision: Record<string, number>;
by_queue: Record<string, number>;
domains_total: number;
};
domain_summary: Array<{ domain: string; total: number }>;
queues: Record<string, AutoRunPostAnalysisQueueItem[]>;
recommended_regression_candidates: AutoRunPostAnalysisQueueItem[];
};
}
export interface AutoGenHistoryRecord {
generation_id: string;
created_at: string;
mode: AutoGenMode;
count: number;
domain: string | null;
questions: string[];
generated_by: string | null;
saved_case_set_file: string | null;
context: {
llm_provider: string | null;
model: string | null;
assistant_prompt_version: string | null;
decomposition_prompt_version: string | null;
prompt_fingerprint: string | null;
} | null;
}
export interface AutoGenHistoryResponse {
ok: boolean;
generated_at: string;
items: AutoGenHistoryRecord[];
} }
export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown"; export type AssistantFallbackType = "none" | "out_of_scope" | "clarification" | "partial" | "unknown";

View File

@ -1,64 +1,96 @@
@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Space+Grotesk:wght@500;700&display=swap"); @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&family=Space+Grotesk:wght@500;700&display=swap");
:root { :root {
--bg-main: #060a08; --rgb-background: 16, 16, 19;
--bg-soft: #0e1511; --rgb-surface-main: 26, 26, 31;
--bg-panel: rgba(15, 24, 19, 0.85); --rgb-surface-horizontal: 32, 32, 38;
--bg-panel-accent: rgba(38, 62, 45, 0.45); --rgb-surface-focus: 40, 40, 47;
--line: rgba(143, 255, 173, 0.3); --rgb-active: 228, 142, 92;
--line-strong: rgba(143, 255, 173, 0.72); --rgb-active-text: 18, 18, 18;
--text-main: #f3ffee; --rgb-text-main: 240, 240, 240;
--text-muted: #9bb8a5; --rgb-text-muted: 166, 166, 170;
--lime-main: #8fffad; --rgb-danger: 255, 126, 126;
--lime-press: #59db83; --rgb-scrollbar-track: 31, 31, 36;
--danger: #ff7e7e; --rgb-scrollbar-thumb: 74, 74, 82;
--rgb-scrollbar-thumb-hover: 90, 90, 100;
--bg-main: rgb(var(--rgb-background));
--bg-soft: rgb(var(--rgb-surface-main));
--bg-panel: rgb(var(--rgb-surface-main));
--bg-panel-accent: rgb(var(--rgb-surface-horizontal));
--surface-horizontal: rgb(var(--rgb-surface-horizontal));
--surface-focus: rgb(var(--rgb-surface-focus));
--line: transparent;
--line-strong: rgba(var(--rgb-active), 0.48);
--text-main: rgb(var(--rgb-text-main));
--text-muted: rgb(var(--rgb-text-muted));
--lime-main: rgb(var(--rgb-text-main));
--lime-press: rgb(var(--rgb-text-main));
--danger: rgb(var(--rgb-danger));
--radius-lg: 20px; --radius-lg: 20px;
--radius-md: 14px; --radius-md: 14px;
--shadow: 0 16px 44px rgba(0, 0, 0, 0.35); --shadow: none;
--autoruns-col-width: 360px;
} }
* { * {
box-sizing: border-box; box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: rgb(var(--rgb-scrollbar-thumb)) rgb(var(--rgb-scrollbar-track));
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: rgb(var(--rgb-scrollbar-track));
}
*::-webkit-scrollbar-thumb {
background: rgb(var(--rgb-scrollbar-thumb));
border-radius: 999px;
border: 2px solid rgb(var(--rgb-scrollbar-track));
}
*::-webkit-scrollbar-thumb:hover {
background: rgb(var(--rgb-scrollbar-thumb-hover));
} }
html, html,
body, body,
#root { #root {
margin: 0; margin: 0;
min-height: 100%; min-height: 100dvh;
font-family: "Manrope", "Segoe UI", sans-serif; font-family: "Manrope", "Segoe UI", sans-serif;
background: radial-gradient(circle at 15% -10%, #1f3b2f 0%, transparent 40%), background: var(--bg-main);
radial-gradient(circle at 90% 10%, #1d2f24 0%, transparent 35%),
linear-gradient(165deg, var(--bg-main) 0%, #070d0a 100%);
color: var(--text-main); color: var(--text-main);
} }
.app-root { .app-root {
max-width: 1720px; max-width: 1720px;
margin: 0 auto; margin: 0 auto;
padding: 28px 24px 42px; padding: 12px 16px 16px;
} }
.hero { .app-root.app-root-autoruns {
margin-bottom: 20px; max-width: none;
padding: 20px 22px; width: 100%;
border: 1px solid var(--line); min-height: 100dvh;
border-radius: var(--radius-lg); max-height: 100dvh;
background: linear-gradient(145deg, rgba(17, 29, 22, 0.94), rgba(10, 16, 13, 0.92)); display: flex;
box-shadow: var(--shadow); flex-direction: column;
animation: rise 0.5s ease-out; overflow: hidden;
} }
.hero h1 { .app-topbar {
font-family: "Space Grotesk", "Manrope", sans-serif; display: flex;
margin: 0 0 8px; align-items: center;
font-size: clamp(1.5rem, 3vw, 2.3rem); justify-content: space-between;
letter-spacing: 0.04em; gap: 12px;
} margin: 0 0 12px;
padding: 0;
.hero p { min-height: 38px;
margin: 0;
color: var(--text-muted);
} }
.layout-grid { .layout-grid {
@ -67,20 +99,35 @@ body,
gap: 16px; gap: 16px;
} }
.layout-grid.layout-grid-autoruns {
min-height: 0;
flex: 1 1 auto;
grid-template-columns: minmax(0, 1fr);
}
.mode-switch-row { .mode-switch-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin: 0 0 14px; margin: 0;
padding: 0;
}
.mode-switch-row.mode-switch-row-right {
margin-left: auto;
justify-content: flex-end;
} }
.panel-frame { .panel-frame {
grid-column: span 12; grid-column: span 12;
border: 1px solid var(--line); border: none;
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: linear-gradient(165deg, var(--bg-panel), rgba(10, 17, 13, 0.88)); background: var(--bg-panel);
overflow: hidden; overflow: hidden;
box-shadow: var(--shadow); box-shadow: none;
animation: rise 0.4s ease-out; animation: rise 0.4s ease-out;
display: flex;
flex-direction: column;
min-height: 0;
} }
.panel-header { .panel-header {
@ -89,8 +136,8 @@ body,
justify-content: space-between; justify-content: space-between;
gap: 14px; gap: 14px;
padding: 14px 18px 10px; padding: 14px 18px 10px;
border-bottom: 1px solid var(--line); border-bottom: none;
background: linear-gradient(90deg, var(--bg-panel-accent), transparent 70%); background: var(--bg-panel-accent);
} }
.panel-header h2 { .panel-header h2 {
@ -106,15 +153,31 @@ body,
} }
.panel-body { .panel-body {
padding: 14px 18px 18px; padding: 10px 12px 12px;
min-height: 0;
}
.app-root-autoruns .autoruns-frame {
height: 100%;
}
.app-root-autoruns .autoruns-frame .panel-body {
flex: 1 1 auto;
overflow: hidden;
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px 12px 12px;
background: rgb(var(--rgb-background));
} }
.status-chip { .status-chip {
border: 1px solid var(--line); border: none;
border-radius: 999px; border-radius: 999px;
padding: 4px 10px; padding: 4px 10px;
color: var(--lime-main); color: var(--lime-main);
font-size: 0.78rem; font-size: 0.78rem;
background: rgb(var(--rgb-surface-focus));
} }
.assistant-panel-actions { .assistant-panel-actions {
@ -127,14 +190,14 @@ body,
.assistant-copy-btn { .assistant-copy-btn {
background: transparent; background: transparent;
border-color: var(--line); border-color: transparent;
color: var(--text-main); color: var(--text-main);
box-shadow: none; box-shadow: none;
transform: none; transform: none;
} }
.assistant-copy-btn:hover { .assistant-copy-btn:hover {
background: rgba(143, 255, 173, 0.14); background: rgb(var(--rgb-surface-focus));
filter: none; filter: none;
box-shadow: none; box-shadow: none;
transform: none; transform: none;
@ -171,20 +234,22 @@ label {
input, input,
select, select,
textarea { textarea {
border: 1px solid rgba(170, 255, 194, 0.26); border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: rgba(7, 13, 10, 0.92); background: rgb(var(--rgb-surface-horizontal));
color: var(--text-main); color: var(--text-main);
padding: 10px 12px; padding: 10px 12px;
outline: none; outline: none;
transition: border-color 0.18s ease, box-shadow 0.18s ease; transition: background-color 0.18s ease;
} }
input:focus, input:focus,
select:focus, select:focus,
textarea:focus { textarea:focus {
border-color: var(--line-strong); border-color: transparent;
box-shadow: 0 0 0 3px rgba(143, 255, 173, 0.2); box-shadow: none;
outline: none;
background: rgb(var(--rgb-surface-focus));
} }
textarea { textarea {
@ -193,28 +258,28 @@ textarea {
} }
button { button {
border: 1px solid rgba(176, 255, 199, 0.35); border: none;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(180deg, #8fffad 0%, #62e286 100%); background: rgb(var(--rgb-surface-horizontal));
color: #08100a; color: rgb(var(--rgb-text-main));
font-weight: 700; font-weight: 700;
font-size: 0.83rem; font-size: 0.83rem;
letter-spacing: 0.02em; letter-spacing: 0.02em;
cursor: pointer; cursor: pointer;
padding: 9px 14px; padding: 9px 14px;
transition: transform 0.16s ease, filter 0.2s ease, box-shadow 0.18s ease; transition: background 0.2s ease, color 0.2s ease;
outline: none;
box-shadow: none;
} }
button:hover { button:hover {
filter: brightness(1.04); border-color: transparent;
transform: translateY(-1px); background: rgb(var(--rgb-surface-focus));
box-shadow: 0 8px 20px rgba(127, 255, 170, 0.32);
} }
button:disabled { button:disabled {
opacity: 0.5; opacity: 0.52;
cursor: not-allowed; cursor: not-allowed;
transform: none;
} }
.button-row { .button-row {
@ -250,14 +315,15 @@ button:disabled {
} }
.tab { .tab {
background: transparent; background: rgb(var(--rgb-surface-main));
color: var(--text-main); color: var(--text-main);
border: 1px solid var(--line); border: none;
} }
.tab.active { .tab.active {
border-color: var(--line-strong); border-color: transparent;
background: linear-gradient(180deg, rgba(143, 255, 173, 0.25), rgba(143, 255, 173, 0.1)); background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
} }
.assistant-chat-list { .assistant-chat-list {
@ -266,9 +332,9 @@ button:disabled {
display: grid; display: grid;
gap: 10px; gap: 10px;
padding: 4px; padding: 4px;
border: 1px solid rgba(156, 255, 189, 0.2); border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
background: rgba(6, 10, 8, 0.65); background: rgb(var(--rgb-surface-horizontal));
} }
.assistant-empty { .assistant-empty {
@ -277,19 +343,19 @@ button:disabled {
} }
.assistant-msg { .assistant-msg {
border: 1px solid rgba(157, 255, 190, 0.2); border: none;
border-radius: 12px; border-radius: 12px;
background: rgba(11, 18, 14, 0.74); background: rgb(var(--rgb-surface-main));
padding: 10px; padding: 10px;
} }
.assistant-msg.user { .assistant-msg.user {
border-color: rgba(110, 198, 255, 0.38); border-color: transparent;
background: rgba(13, 20, 24, 0.76); background: rgb(var(--rgb-surface-focus));
} }
.assistant-msg.assistant { .assistant-msg.assistant {
border-color: rgba(157, 255, 190, 0.25); border-color: transparent;
} }
.assistant-msg-head { .assistant-msg-head {
@ -335,11 +401,11 @@ button:disabled {
min-height: 180px; min-height: 180px;
max-height: 420px; max-height: 420px;
overflow: auto; overflow: auto;
background: #050806; background: rgb(var(--rgb-surface-horizontal));
border: 1px solid rgba(156, 255, 189, 0.2); border: none;
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: 12px; padding: 12px;
color: #d6ffdf; color: rgb(var(--rgb-text-main));
font-family: "JetBrains Mono", "Consolas", monospace; font-family: "JetBrains Mono", "Consolas", monospace;
font-size: 0.78rem; font-size: 0.78rem;
line-height: 1.45; line-height: 1.45;
@ -352,8 +418,8 @@ button:disabled {
} }
.metrics-grid div { .metrics-grid div {
background: rgba(11, 18, 14, 0.7); background: rgba(var(--rgb-surface-main), 0.8);
border: 1px solid rgba(157, 255, 190, 0.24); border: none;
border-radius: 12px; border-radius: 12px;
padding: 10px; padding: 10px;
display: flex; display: flex;
@ -382,8 +448,8 @@ button:disabled {
width: 100%; width: 100%;
text-align: left; text-align: left;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(157, 255, 190, 0.24); border: none;
background: rgba(11, 18, 14, 0.78); background: rgb(var(--rgb-surface-main));
color: var(--text-main); color: var(--text-main);
padding: 10px; padding: 10px;
} }
@ -455,16 +521,32 @@ button:disabled {
} }
.autoruns-columns { .autoruns-columns {
display: grid; display: flex;
gap: 12px; gap: 12px;
width: 100%;
min-height: 0;
flex: 1 1 auto;
overflow-x: auto;
overflow-y: hidden;
padding-bottom: 4px;
} }
.autoruns-col { .autoruns-col {
border: 1px solid rgba(157, 255, 190, 0.2); flex: 0 0 var(--autoruns-col-width);
width: var(--autoruns-col-width);
height: 100%;
min-height: 0;
overflow: auto;
border: none;
border-radius: 14px; border-radius: 14px;
background: rgba(8, 13, 10, 0.72); background: rgb(var(--rgb-surface-main));
padding: 12px; padding: 12px;
min-height: 220px; scrollbar-gutter: stable;
}
.autoruns-col:nth-child(3) {
flex-basis: 440px;
width: 440px;
} }
.autoruns-col h3 { .autoruns-col h3 {
@ -493,9 +575,9 @@ button:disabled {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 8px; gap: 8px;
border: 1px solid rgba(157, 255, 190, 0.14); border: none;
border-radius: 10px; border-radius: 10px;
background: rgba(10, 18, 13, 0.65); background: rgb(var(--rgb-surface-horizontal));
padding: 8px 9px; padding: 8px 9px;
font-size: 0.79rem; font-size: 0.79rem;
} }
@ -506,7 +588,7 @@ button:disabled {
.autoruns-prompt-details summary { .autoruns-prompt-details summary {
cursor: pointer; cursor: pointer;
color: var(--lime-main); color: var(--text-main);
font-size: 0.8rem; font-size: 0.8rem;
margin-bottom: 8px; margin-bottom: 8px;
} }
@ -523,9 +605,9 @@ button:disabled {
} }
.autoruns-stats-grid > div { .autoruns-stats-grid > div {
border: 1px solid rgba(157, 255, 190, 0.2); border: none;
border-radius: 10px; border-radius: 10px;
background: rgba(10, 18, 13, 0.7); background: rgb(var(--rgb-surface-horizontal));
padding: 8px; padding: 8px;
display: grid; display: grid;
gap: 3px; gap: 3px;
@ -544,7 +626,9 @@ button:disabled {
.autoruns-run-list { .autoruns-run-list {
display: grid; display: grid;
gap: 8px; gap: 8px;
max-height: 760px; max-height: none;
min-height: 0;
flex: 1 1 auto;
overflow: auto; overflow: auto;
padding-right: 2px; padding-right: 2px;
} }
@ -553,8 +637,8 @@ button:disabled {
width: 100%; width: 100%;
text-align: left; text-align: left;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(157, 255, 190, 0.23); border: none;
background: rgba(11, 18, 14, 0.75); background: rgb(var(--rgb-surface-horizontal));
color: var(--text-main); color: var(--text-main);
padding: 10px; padding: 10px;
display: grid; display: grid;
@ -589,7 +673,7 @@ button:disabled {
margin-top: 8px; margin-top: 8px;
display: grid; display: grid;
gap: 6px; gap: 6px;
max-height: 170px; max-height: 180px;
overflow: auto; overflow: auto;
} }
@ -597,8 +681,8 @@ button:disabled {
width: 100%; width: 100%;
text-align: left; text-align: left;
border-radius: 10px; border-radius: 10px;
border: 1px solid rgba(157, 255, 190, 0.22); border: none;
background: rgba(9, 14, 11, 0.72); background: rgb(var(--rgb-surface-horizontal));
color: var(--text-main); color: var(--text-main);
padding: 7px 8px; padding: 7px 8px;
display: flex; display: flex;
@ -613,20 +697,22 @@ button:disabled {
.autoruns-dialog-view { .autoruns-dialog-view {
margin-top: 10px; margin-top: 10px;
border: 1px solid rgba(157, 255, 190, 0.2); border: none;
border-radius: 12px; border-radius: 12px;
background: rgba(5, 8, 6, 0.7); background: rgb(var(--rgb-surface-horizontal));
padding: 10px; padding: 10px;
max-height: 570px; max-height: none;
min-height: 0;
flex: 1 1 auto;
overflow: auto; overflow: auto;
display: grid; display: grid;
gap: 8px; gap: 8px;
} }
.autoruns-msg { .autoruns-msg {
border: 1px solid rgba(157, 255, 190, 0.22); border: none;
border-radius: 12px; border-radius: 12px;
background: rgba(11, 18, 14, 0.8); background: rgb(var(--rgb-surface-focus));
padding: 8px 10px; padding: 8px 10px;
display: grid; display: grid;
gap: 6px; gap: 6px;
@ -641,6 +727,12 @@ button:disabled {
color: var(--text-muted); color: var(--text-muted);
} }
.autoruns-msg-head-actions {
display: flex;
align-items: center;
gap: 8px;
}
.autoruns-msg p { .autoruns-msg p {
margin: 0; margin: 0;
white-space: pre-wrap; white-space: pre-wrap;
@ -648,14 +740,122 @@ button:disabled {
font-size: 0.84rem; font-size: 0.84rem;
} }
.autoruns-comment-icon {
border: none;
background: rgb(var(--rgb-surface-horizontal));
color: var(--text-main);
border-radius: 999px;
min-width: 28px;
min-height: 28px;
padding: 0 8px;
line-height: 1;
box-shadow: none;
transform: none;
}
.autoruns-comment-icon:hover {
border-color: var(--line-strong);
box-shadow: none;
transform: none;
}
.autoruns-comment-icon.commented {
border-color: var(--line-strong);
color: var(--lime-main);
background: rgb(var(--rgb-active));
color: rgb(var(--rgb-active-text));
}
.autoruns-msg-annotation {
display: grid;
gap: 4px;
border: none;
border-radius: 10px;
background: rgb(var(--rgb-surface-horizontal));
padding: 7px 8px;
font-size: 0.78rem;
}
.autoruns-comments-list {
display: grid;
gap: 8px;
max-height: none;
min-height: 0;
flex: 1 1 auto;
overflow: auto;
padding-right: 2px;
}
.autoruns-autogen-list {
display: grid;
gap: 8px;
max-height: none;
min-height: 0;
overflow: auto;
padding-right: 2px;
}
.autoruns-autogen-item {
border: none;
border-radius: 10px;
background: rgb(var(--rgb-surface-horizontal));
padding: 8px;
display: grid;
gap: 5px;
}
.autoruns-autogen-item header {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.76rem;
}
.autoruns-autogen-item p {
margin: 0;
color: var(--text-muted);
white-space: pre-wrap;
font-size: 0.8rem;
}
.autoruns-comment-item {
width: 100%;
text-align: left;
border-radius: 12px;
border: none;
background: rgb(var(--rgb-surface-horizontal));
color: var(--text-main);
padding: 9px;
display: grid;
gap: 6px;
}
.autoruns-comment-item p {
margin: 0;
white-space: pre-wrap;
color: var(--text-muted);
font-size: 0.79rem;
}
.autoruns-comment-item.selected {
border-color: var(--line-strong);
}
.autoruns-comment-head {
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 0.75rem;
}
.autoruns-msg.assistant { .autoruns-msg.assistant {
margin-right: 12%; margin-right: 12%;
} }
.autoruns-msg.user { .autoruns-msg.user {
margin-left: 12%; margin-left: 12%;
border-color: rgba(95, 179, 255, 0.35); border-color: transparent;
background: rgba(10, 18, 24, 0.75); background: rgb(var(--rgb-surface-focus));
} }
.autoruns-decomposition-list { .autoruns-decomposition-list {
@ -666,15 +866,83 @@ button:disabled {
font-size: 0.8rem; font-size: 0.8rem;
} }
.autoruns-comment-modal-backdrop {
position: fixed;
inset: 0;
background: rgba(var(--rgb-background), 0.74);
display: grid;
place-items: center;
z-index: 1800;
padding: 12px;
}
.autoruns-comment-modal {
width: min(660px, 100%);
border: none;
border-radius: 16px;
background: rgb(var(--rgb-surface-horizontal));
box-shadow: var(--shadow);
padding: 14px;
display: grid;
gap: 10px;
}
.autoruns-comment-modal h3 {
margin: 0;
font-size: 0.95rem;
}
.autoruns-comment-quote {
margin: 0;
border: none;
border-radius: 10px;
background: rgb(var(--rgb-surface-focus));
padding: 8px;
white-space: pre-wrap;
max-height: 150px;
overflow: auto;
font-size: 0.82rem;
}
.autoruns-rating-row {
display: flex;
gap: 8px;
}
.autoruns-rating-dot {
width: 34px;
height: 34px;
border-radius: 999px;
padding: 0;
border: none;
background: rgb(var(--rgb-surface-focus));
color: var(--text-muted);
font-size: 0.95rem;
box-shadow: none;
transform: none;
}
.autoruns-rating-dot:hover {
border-color: var(--line-strong);
box-shadow: none;
transform: none;
}
.autoruns-rating-dot.active {
border-color: var(--line-strong);
color: rgb(var(--rgb-active-text));
background: rgb(var(--rgb-active));
}
.autoruns-coverage-list { .autoruns-coverage-list {
display: grid; display: grid;
gap: 8px; gap: 8px;
} }
.autoruns-coverage-item { .autoruns-coverage-item {
border: 1px solid rgba(157, 255, 190, 0.2); border: none;
border-radius: 10px; border-radius: 10px;
background: rgba(11, 18, 14, 0.68); background: rgb(var(--rgb-surface-horizontal));
padding: 8px; padding: 8px;
} }
@ -693,27 +961,31 @@ button:disabled {
.autoruns-coverage-bar { .autoruns-coverage-bar {
height: 7px; height: 7px;
border-radius: 999px; border-radius: 999px;
background: rgba(157, 255, 190, 0.14); background: rgb(var(--rgb-surface-focus));
overflow: hidden; overflow: hidden;
} }
.autoruns-coverage-bar > div { .autoruns-coverage-bar > div {
height: 100%; height: 100%;
border-radius: 999px; border-radius: 999px;
background: linear-gradient(90deg, #6ee0ff, #8fffad); background: rgb(var(--rgb-active));
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.metrics-grid { :root {
grid-template-columns: repeat(3, minmax(0, 1fr)); --autoruns-col-width: 340px;
} }
.autoruns-columns { .metrics-grid {
grid-template-columns: 1fr !important; grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
@media (max-width: 920px) { @media (max-width: 920px) {
:root {
--autoruns-col-width: 320px;
}
.grid-two, .grid-two,
.runtime-grid { .runtime-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@ -731,6 +1003,10 @@ button:disabled {
} }
@media (max-width: 640px) { @media (max-width: 640px) {
:root {
--autoruns-col-width: 300px;
}
.app-root { .app-root {
padding: 18px 12px 24px; padding: 18px 12px 24px;
} }
@ -750,3 +1026,6 @@ button:disabled {
transform: translateY(0); transform: translateY(0);
} }
} }